foreign-toplevel-management: server does not send an initial output_enter event
When using the wlr-foreign-toplevel-management-unstable-v1 protocol to obtain the initial details of all toplevels, wlroots sends the client events for title, app_id, and state. However, it doesn't send an output_enter event.
The protocol says:
Each toplevel has a list of outputs it is visible on, conveyed to the client with the output_enter and output_leave events.
My reading of this is that sending the events is the only way to get this information, and as with the other toplevel details, I would have assumed an initial output_enter event would be sent along with all the other initial information to let the client know what the current state of the toplevel is.
wlroots has migrated to gitlab.freedesktop.org. This issue has been moved to:
https://gitlab.freedesktop.org/wlroots/wlroots/-/issues/1567
Oh, I can't read. According to the code I linked, wlroots totally does send that event.
I'll go back to my code and see if I can figure out why I am not receiving it before blaming wlroots further.
OK so I can't see that this is my fault, so here are more details. I'm using WayFire and the following Cython code (for brevity I've removed all the header stuff - in Cython you kind of have to reproduce large swathes of the header files), it is imported from another python script and the init() function is called.
cdef void global_add(
void *data,
wl_registry *registry,
uint32_t name,
const char *interface,
uint32_t version
):
global toplevel_manager
if strcmp(interface, zwlr_foreign_toplevel_manager_v1_interface.name) == 0:
toplevel_manager = <zwlr_foreign_toplevel_manager_v1*>wl_registry_bind(
registry, name, &zwlr_foreign_toplevel_manager_v1_interface, version
);
cdef void global_remove(void *data, wl_registry *registry, uint32_t name):
print('global remove!')
cdef wl_registry_listener registry_listener
registry_listener.global_ = global_add
registry_listener.global_remove = global_remove
cdef void toplevel(
void *data,
zwlr_foreign_toplevel_manager_v1 *manager,
zwlr_foreign_toplevel_handle_v1 *handle
):
print("toplevel!")
zwlr_foreign_toplevel_handle_v1_add_listener(handle, &handle_listener, NULL)
cdef void finished(
void *data,
zwlr_foreign_toplevel_manager_v1 *manager
):
print("finished!")
cdef zwlr_foreign_toplevel_manager_v1_listener toplevel_manager_listener
toplevel_manager_listener.toplevel = toplevel
toplevel_manager_listener.finished = finished
cdef void title(
void *data, zwlr_foreign_toplevel_handle_v1 *handle, const char *title
):
print("title!", title)
cdef void app_id(
void *data, zwlr_foreign_toplevel_handle_v1 *handle, const char *app_id
):
print("app_id!", app_id)
cdef void output_enter(
void *data, zwlr_foreign_toplevel_handle_v1 *handle, wl_output *output
):
print("output enter!")
cdef void output_leave(
void *data, zwlr_foreign_toplevel_handle_v1 *handle, wl_output *output
):
print("output leave!")
cdef void state(
void *data, zwlr_foreign_toplevel_handle_v1 *handle, wl_array *state
):
print("state!")
cdef void done(void *data, zwlr_foreign_toplevel_handle_v1 *handle):
print("done!")
cdef void closed(void *data, zwlr_foreign_toplevel_handle_v1 *handle):
print("closed!")
cdef zwlr_foreign_toplevel_handle_v1_listener handle_listener
handle_listener.title = title
handle_listener.app_id = app_id
handle_listener.output_enter = output_enter
handle_listener.output_leave = output_leave
handle_listener.state = state
handle_listener.done = done
handle_listener.closed = closed
cdef wl_display* display
cdef wl_registry* registry
cdef zwlr_foreign_toplevel_manager_v1* toplevel_manager
def init():
global display
global registry
display = wl_display_connect(NULL)
if display is NULL:
raise RuntimeError("wl_display_connect failed")
registry = wl_display_get_registry(display)
if registry is NULL:
raise RuntimeError("wl_display_get_registry failed")
wl_registry_add_listener(registry, ®istry_listener, NULL)
wl_display_roundtrip(display)
wl_registry_destroy(registry)
if toplevel_manager is NULL:
raise ValueError("failed to get zwlr_foreign_toplevel_manager_v1_interface")
zwlr_foreign_toplevel_manager_v1_add_listener(
toplevel_manager, &toplevel_manager_listener, NULL
)
wl_display_roundtrip(display)
If I open a single window of Alacritty and run this, I get:
toplevel!
title! b'bilbo@bilbo-arch:~'
app_id! b'Alacritty'
state!
done!
As you can see, no output_enter event. I'm using wayfire 0.1 and wlroots 0.3.
@ammen99?
@chrisjbillington Could you provide a WAYLAND_DEBUG? (Run WAYLAND_DEBUG=1 <your_client>)
Oh, I think I found the problem in your code. Try binding wl_output in your global_add handler. If you don't bind the outputs, then there is no wl_resource for your client and the compositor can't (and won't) send the event.
Thanks, that makes sense! Appreciate the tech-support. I've tried doing so, however, I'm still not getting the event. I now have:
cdef void global_add(
void *data,
wl_registry *registry,
uint32_t name,
const char *interface,
uint32_t version
):
global toplevel_manager
global output
if strcmp(interface, wl_output_interface.name) == 0:
print('got output!')
output = <wl_output*>wl_registry_bind(
registry, name, &wl_output_interface, version
);
if strcmp(interface, zwlr_foreign_toplevel_manager_v1_interface.name) == 0:
print('got toplevel manager!')
toplevel_manager = <zwlr_foreign_toplevel_manager_v1*>wl_registry_bind(
registry, name, &zwlr_foreign_toplevel_manager_v1_interface, version
);
cdef void global_remove(void *data, wl_registry *registry, uint32_t name):
print('global remove!')
cdef wl_registry_listener registry_listener
registry_listener.global_ = global_add
registry_listener.global_remove = global_remove
cdef void toplevel(
void *data,
zwlr_foreign_toplevel_manager_v1 *manager,
zwlr_foreign_toplevel_handle_v1 *handle
):
print("toplevel!")
zwlr_foreign_toplevel_handle_v1_add_listener(handle, &handle_listener, NULL)
cdef void finished(
void *data,
zwlr_foreign_toplevel_manager_v1 *manager
):
print("finished!")
cdef zwlr_foreign_toplevel_manager_v1_listener toplevel_manager_listener
toplevel_manager_listener.toplevel = toplevel
toplevel_manager_listener.finished = finished
cdef void title(
void *data, zwlr_foreign_toplevel_handle_v1 *handle, const char *title
):
print("title!", title)
cdef void app_id(
void *data, zwlr_foreign_toplevel_handle_v1 *handle, const char *app_id
):
print("app_id!", app_id)
cdef void output_enter(
void *data, zwlr_foreign_toplevel_handle_v1 *handle, wl_output *output
):
print("output enter!")
cdef void output_leave(
void *data, zwlr_foreign_toplevel_handle_v1 *handle, wl_output *output
):
print("output leave!")
cdef void state(
void *data, zwlr_foreign_toplevel_handle_v1 *handle, wl_array *state
):
print("state!")
cdef void done(void *data, zwlr_foreign_toplevel_handle_v1 *handle):
print("done!")
cdef void closed(void *data, zwlr_foreign_toplevel_handle_v1 *handle):
print("closed!")
cdef zwlr_foreign_toplevel_handle_v1_listener handle_listener
handle_listener.title = title
handle_listener.app_id = app_id
handle_listener.output_enter = output_enter
handle_listener.output_leave = output_leave
handle_listener.state = state
handle_listener.done = done
handle_listener.closed = closed
cdef void geometry(
void *data,
wl_output *wl_output,
int32_t x,
int32_t y,
int32_t physical_width,
int32_t physical_height,
int32_t subpixel,
const char *make,
const char *model,
int32_t transform
):
print('geometry!')
cdef void mode(
void *data,
wl_output *wl_output,
uint32_t flags,
int32_t width,
int32_t height,
int32_t refresh
):
print('mode!')
cdef void output_done(
void *data,
wl_output *wl_output
):
print('output_done!')
cdef void scale(
void *data,
wl_output *wl_output,
int32_t factor
):
print('scale!')
cdef wl_output_listener output_listener
output_listener.geometry = geometry
output_listener.mode = mode
output_listener.done = output_done
output_listener.scale = scale
cdef wl_display* display
cdef wl_registry* registry
cdef zwlr_foreign_toplevel_manager_v1* toplevel_manager
cdef wl_output* output
def init():
global display
global registry
display = wl_display_connect(NULL)
if display is NULL:
raise RuntimeError("wl_display_connect failed")
registry = wl_display_get_registry(display)
if registry is NULL:
raise RuntimeError("wl_display_get_registry failed")
wl_registry_add_listener(registry, ®istry_listener, NULL)
wl_display_roundtrip(display)
wl_registry_destroy(registry)
if toplevel_manager is NULL:
raise ValueError("failed to get toplevel manager")
if output is NULL:
raise ValueError("failed to get output")
wl_output_add_listener(output, &output_listener, NULL)
zwlr_foreign_toplevel_manager_v1_add_listener(
toplevel_manager, &toplevel_manager_listener, NULL
)
wl_display_roundtrip(display)
resulting in (with WAYLAND_DEBUG):
[1974124.388] -> [email protected]_registry(new id wl_registry@2)
[1974124.434] -> [email protected](new id wl_callback@3)
[1974124.562] [email protected]_id(3)
[1974124.580] [email protected](1, "wl_data_device_manager", 3)
[1974124.608] [email protected](2, "zwlr_data_control_manager_v1", 1)
[1974124.645] [email protected](3, "wl_shm", 1)
[1974124.673] [email protected](4, "zwp_linux_dmabuf_v1", 3)
[1974124.700] [email protected](5, "wl_drm", 2)
[1974124.727] [email protected](6, "wl_compositor", 4)
[1974124.756] [email protected](7, "wl_subcompositor", 1)
[1974124.786] [email protected](8, "xdg_wm_base", 2)
[1974124.814] [email protected](9, "zwlr_layer_shell_v1", 1)
[1974124.843] [email protected](10, "zxdg_shell_v6", 1)
[1974124.874] [email protected](11, "wl_seat", 6)
[1974124.905] [email protected](12, "orbital_screenshooter", 1)
[1974124.935] [email protected](13, "zwlr_screencopy_manager_v1", 1)
[1974124.962] [email protected](14, "gamma_control_manager", 1)
[1974124.992] [email protected](15, "zwlr_gamma_control_manager_v1", 1)
[1974125.020] [email protected](16, "zwp_linux_dmabuf_v1", 3)
[1974125.052] [email protected](17, "zwlr_export_dmabuf_manager_v1", 1)
[1974125.081] [email protected](18, "org_kde_kwin_server_decoration_manager", 1)
[1974125.110] [email protected](19, "zwlr_input_inhibit_manager_v1", 1)
[1974125.138] [email protected](20, "zxdg_output_manager_v1", 2)
[1974125.167] [email protected](21, "zwp_virtual_keyboard_manager_v1", 1)
[1974125.195] [email protected](22, "org_kde_kwin_idle", 1)
[1974125.224] [email protected](23, "zwp_idle_inhibit_manager_v1", 1)
[1974125.252] [email protected](24, "zwf_shell_manager_v1", 1)
[1974125.281] [email protected](25, "gtk_shell1", 1)
[1974125.308] [email protected](26, "zwlr_foreign_toplevel_manager_v1", 1)
got toplevel manager!
[1974125.353] -> [email protected](26, "zwlr_foreign_toplevel_manager_v1", 1, new id [unknown]@4)
[1974125.396] [email protected](27, "zwp_pointer_gestures_v1", 1)
[1974125.423] [email protected](28, "wl_output", 3)
got output!
[1974125.455] -> [email protected](28, "wl_output", 3, new id [unknown]@5)
[1974125.496] [email protected](135)
[1974125.509] -> [email protected](new id wl_callback@3)
[1974125.625] [email protected]_id(3)
[1974125.660] [email protected](new id zwlr_foreign_toplevel_handle_v1@3651081376)
toplevel!
[1974125.688] [email protected]("bilbo@bilbo-arch:~")
title! b'bilbo@bilbo-arch:~'
[1974125.710] [email protected]_id("Alacritty")
app_id! b'Alacritty'
[1974125.727] [email protected](array)
state!
[1974125.744] [email protected]()
done!
[1974125.757] [email protected](0, 0, 0, 0, 0, "The X.Org Foundation", "11.0", 0)
geometry!
[1974125.827] [email protected](1, 1024, 768, 60000)
mode!
[1974125.870] [email protected](1)
scale!
[1974125.918] [email protected]()
output_done!
[1974125.931] [email protected](135)
As you might be able to tell from [email protected](0, 0, 0, 0, 0, "The X.Org Foundation", "11.0", 0), I ran this in an X11-embedded wayfire session, i.e. running 'wayfire' from within an X session and having it run within its own little window (which is super slick by the way). Before the initial bug report I checked what happened running wayfire as a regular session, but I'm having issues with that right now (unrelated to wayfire itself - GDM keeps switching back to X and not showing wayland sessions so I can't easily start a wayfire session without jumping through some hoops) so am doing my testing with the X11-embedded session for now. So that could be relevant - I'll try to get a regular wayfire session running again to see if it makes a difference.
Hmm, that's annoying. You probably need to bind to the zwlr_foreign_toplevel_manager_v1 after all wl_outputs have been bound.
Indeed, I just checked how I do it in my client (wf-dock) - I first go through all the globals, bind outputs, then on a second get_registry + roundtrip I bind the wlr-foreign-toplevel-manager.
To fix the issue we could change the protocol wording a bit: https://github.com/swaywm/wlr-protocols/blob/master/unstable/wlr-foreign-toplevel-management-unstable-v1.xml#L45 (dropping the "immediately" part). Then, when a new client binds, add a new idle source in wlroots and delay the sending of details. I'm not sure if this is a good strategy, although I can't come up with a better idea.
PS. @chrisjbillington To run Wayfire from DRM you can just switch to a TTY and run wayfire if you have issues with GDM or whatever you're using.
To fix the issue we could change the protocol wording a bit
-1. This wouldn't imply compositors must not send it immediately. In other words, after this protocol change a compositor sending immediately toplevels wouldn't violate the protocol, but it would break clients.
Ah, excellent. That is annoying to not be able to bind everything in one loop through the globals, but it works. I'm now saving the name and version of the foreign toplevel manager, and then calling wl_registry_bind later, after the outputs are bound. I get my output_enter event.
The fix of sending later sounds good from my perspective (it will still all happen in a single wl_display_roundtrip, right?), though I am not familiar enough with the inner workings of wlroots to know if there is any major downside to that.
Thanks for all the help, I'm new to wayland and am trying to make a traditional window-list panel type app for it - some competition for wf-dock maybe!
To fix the issue we could change the protocol wording a bit
-1. This wouldn't imply compositors must not send it immediately. In other words, after this protocol change a compositor sending immediately toplevels wouldn't violate the protocol, but it would break clients.
I agree. But then we need to figure out when the right time to send those events is ... Would it be that bad to just leave things as they are now?
Could you send it immediately if and only if the output is bound, and otherwise, immediately upon the output being bound?
Edit: just the output_enter event. The rest would always be immediate.
The problem is that currently the wlroots' implementations of wl_output and foreign-toplevel-manager are more or less decoupled so we don't know when the client binds a new wl_output resource, not to mention that a client might have multiple resources for the same wl_output
Ah, I see. Maybe not worth the added complexity then. This is workable in the client, you just have to know about it. Perhaps the protocol documentation should just say that the initial output_enter event will only be sent if the output is bound.
The only thing I'm concerned about is that AFAIK "the output is bound" is more or less libwayland-specific, whereas we try to make the protocols implementation-independent.
The protocol for wl_registry mentions binding as a thing, so it sounds like part of the protocol to me:
<interface name="wl_registry" version="1">
<description summary="global registry object">
The singleton global registry object. The server has a number of
global objects that are available to all clients. These objects
typically represent an actual object in the server (for example,
an input device) or they are singleton objects that provide
extension functionality.
When a client creates a registry object, the registry object
will emit a global event for each global currently in the
registry. Globals come and go as a result of device or
monitor hotplugs, reconfiguration or other events, and the
registry will send out global and global_remove events to
keep the client up to date with the changes. To mark the end
of the initial burst of events, the client can use the
wl_display.sync request immediately after calling
wl_display.get_registry.
A client can bind to a global object by using the bind
request. This creates a client-side handle that lets the object
emit events to the client and lets the client invoke requests on
the object.
</description>
<request name="bind">
<description summary="bind an object to the display">
Binds a new, client-created object to the server using the
specified name as the identifier.
</description>
<arg name="name" type="uint" summary="unique numeric name of the object"/>
<arg name="id" type="new_id" summary="bounded object"/>
</request>
A downside to emitting the event when idle is that it will somewhat break wl_display_sync, since the 'done' event from the sync request will be delivered before the output_enter event, defeating the purpose of the sync.
One thing that would obviate the need for two passes through the globals is the order of the global events. If the outputs came first, then if the client is binding them at all, they will be bound before the foreign-toplevel-manager is. The protocol should still say that the output_enter events are only sent if the client has bound the outputs, but it would simplify client code.
I'm guessing this is pretty fraught though, since probably the order the globals are returned is currently arbitrary, and locking into anything in particular is probably a bad idea long-term since there could be all sorts of inter-dependencies. You could introspect all the globals and their events, and see what handles are used in their events, in order to compile a directed graph of which globals would benefit from having other globals being higher in the list so that they could be bound to earlier and in a single pass. But the graph probably wouldn't be acyclic, making that not possible.
I think things are okay as they are right now. It's not perfect, but sending "after but not immediately" is fragile and ill-defined behaviour.
Well, I'll just leave this open if you want to treat it as a request for documenting the behaviour, otherwise feel free to close.