--- a/plugins/single_plugins/move.cpp +++ b/plugins/single_plugins/move.cpp @@ -56,7 +56,8 @@ wf::option_wrapper_t move_enable_snap_off{"move/enable_snap_off"}; wf::option_wrapper_t move_snap_off_threshold{"move/snap_off_threshold"}; - wf::wl_timer workspace_switch_timer; + wf::option_wrapper_t workspace_switch_repeat{"move/workspace_switch_repeat"}; + wf::wl_timer workspace_switch_timer; wf::grid::slot_t workspace_switch_slot_id = wf::grid::SLOT_NONE; wf::shared_data::ref_ptr_t drag_helper; @@ -104,15 +105,23 @@ if ((ev->focused_output == output) && can_handle_drag(ev->main_view) && !drag_helper->is_view_held_in_place()) { - // Mark the last windowed geometry (which is the geometry before the view was grabbed: grabs work - // not by moving the view, but by translating it with a transformer. Therefore, the view geometry - // is still the geometry before the drag. - wf::get_core().default_wm->update_last_windowed_geometry(ev->main_view); + // Keep the windowed size from before a provisional live resize. + // Move's transformer retains the original position until release. + if (ev->main_view->has_data() && + !ev->main_view->pending_tiled_edges() && !ev->main_view->pending_fullscreen()) + { + wf::get_core().default_wm->update_last_windowed_geometry( + ev->main_view, move_windowed_geometry(ev->main_view)); + } else + { + wf::get_core().default_wm->update_last_windowed_geometry(ev->main_view); + } // Artificially continue the grab for a little bit more, so that the last windowed geometry does // not get overwritten. wf::get_core().default_wm->set_view_grabbed(ev->main_view, true); wf::move_drag::adjust_view_on_output(ev); + restore_move_size(ev); wf::update_window_scope_after_move(ev->main_view); // Move owns delayed snapping and the provisional held-window preview. @@ -366,6 +375,7 @@ wf::get_core().seat->focus_view(grabbed_view); } + view->erase_data(); drag_helper->set_pending_drag(grab_position); drag_helper->start_drag(view, opts); drag_helper->handle_motion(get_global_input_coords()); @@ -375,6 +385,8 @@ void deactivate() { + workspace_switch_timer.disconnect(); + workspace_switch_slot_id = wf::grid::SLOT_NONE; input_grab->ungrab_input(); output->deactivate_plugin(&grab_interface); } @@ -435,17 +447,22 @@ return; } - workspace_switch_timer.set_timeout(workspace_switch_after, [this, dx, dy] () - { + workspace_switch_timer.set_timeout(std::max(1, workspace_switch_after.value()), [this, dx, dy, slot_id] () + { + if (!drag_helper->view || drag_helper->current_output != output || + workspace_switch_after < 0 || wf::grid::calc_slot(output, get_input_coords(), + snap_threshold, quarter_snap_threshold) != slot_id) return false; auto current = output->wset()->get_current_workspace(); auto size = output->wset()->get_workspace_grid_size(); - if (size.width < 1 || size.height < 1) return; + if (size.width < 1 || size.height < 1) return false; // Continue at the opposite edge, including the last-to-first and // first-to-last transitions of a horizontal workspace row. wf::point_t target{ (current.x + dx + size.width) % size.width, (current.y + dy + size.height) % size.height}; - if (target != current) output->wset()->request_workspace(target); + if (target == current) return false; + output->wset()->request_workspace(target); + return workspace_switch_repeat.value(); }); } --- a/plugins/single_plugins/move-snap.hpp +++ b/plugins/single_plugins/move-snap.hpp @@ -18,9 +18,55 @@ using Clock = std::chrono::steady_clock; constexpr const char *preview_name = "move-held-preview"; -// This changes only the rendered drag texture. The client keeps its original -// geometry until release. Wobbly, above this node, supplies the existing spring -// animation; there is no separate animation clock or destination overlay. +// The preview fixes the rendered destination while the client redraws at that +// size. Wobbly supplies the spring animation; Move retains the input grab. +struct move_snap_anchor_t : wf::custom_data_t {}; + +struct move_snap_restore_t : wf::custom_data_t +{ + wf::dimensionsf_t size, surface_size; + wf::pointf_t wm_offset; +}; + +wf::geometry_t move_windowed_geometry(wayfire_toplevel_view view) +{ + auto geometry = view->get_pending_geometry(); + if (auto restore = view->get_data()) + { + geometry.width = restore->size.width; + geometry.height = restore->size.height; + } + return geometry; +} + +// A client may still be painting the preview when the pointer pulls away and +// immediately releases. Place the original size around the original grab point, +// independently of whichever client buffer has arrived at that moment. +void restore_move_size(wf::move_drag::drag_done_signal *ev) +{ + auto view = ev->main_view; + auto saved = view->get_data(); + if (!saved || !view->is_mapped() || !ev->focused_output) return; + auto grab = ev->grab_position - wf::origin(ev->focused_output->get_layout_geometry()); + for (const auto& moved : ev->all_views) + if (moved.view == view) + { + auto g = view->get_pending_geometry(); + g.x = grab.x - moved.relative_grab.x * saved->surface_size.width + saved->wm_offset.x; + g.y = grab.y - moved.relative_grab.y * saved->surface_size.height + saved->wm_offset.y; + g.width = saved->size.width; g.height = saved->size.height; + view->set_geometry(g); + // A late client reply must not let the released Wobbly model's + // fling inertia move the restored window. Transition through its + // tiled state to the ordinary position-preserving resize state. + set_tiled_wobbly(view, true); + set_tiled_wobbly(view, false); + view->get_data_safe(); + break; + } + view->erase_data(); +} + class held_preview_t : public wf::scene::transformer_base_node_t { class instance_t : public wf::scene::transformer_render_instance_t @@ -146,7 +192,11 @@ void clear() { - dwell.disconnect(); remove_preview(); destination = {}; ready = false; + dwell.disconnect(); + if (auto view = preview_view.lock(); view && drag->view.get() == view.get()) + if (auto restore = view->get_data(); restore && view->is_mapped()) + view->resize(restore->size.width, restore->size.height); + remove_preview(); destination = {}; ready = false; } void show_preview(wayfire_toplevel_view view, wf::geometry_t geometry) @@ -173,8 +223,21 @@ { auto view = destination.view.lock(); if (!enabled || !eligible(view) || drag->view.get() != view.get() || drag->is_view_held_in_place()) { clear(); return; } + if (!view->has_data()) + { + auto saved = view->get_data_safe(); + const auto geometry = view->get_geometry(); + const auto surface = view->get_surface_root_node()->get_bounding_box(); + saved->size = wf::fdimensions(view->get_pending_geometry()); + saved->surface_size = {surface.width + saved->size.width - geometry.width, + surface.height + saved->size.height - geometry.height}; + saved->wm_offset = wf::origin(geometry) - wf::origin(surface); + } ready = true; show_preview(view, destination.geometry + wf::origin(destination.output->get_layout_geometry())); + // Normal transactions ask Wayland/Xwayland clients to render the new + // dimensions now. Keep position and tile state provisional until drop. + view->resize(destination.geometry.width, destination.geometry.height); } void place(wayfire_toplevel_view view) @@ -213,7 +276,8 @@ if (ev->operation == wf::grid::MOVE_OP_FINISH_PREVIEW) { ready = same(destination, next) && Clock::now() >= deadline; - dwell.disconnect(); remove_preview(!ready); + if (!ready) clear(); + else { dwell.disconnect(); remove_preview(false); } return; } if (ev->operation == wf::grid::MOVE_OP_DROP) @@ -250,6 +314,16 @@ wf::signal::connection_t on_geometry = [this] (auto *ev) { if (handing_off && preview_view.lock().get() == ev->view.get()) remove_preview(); + wayfire_toplevel_view view = wf::toplevel_cast(ev->view); + if (view && view->has_data() && drag->view.get() != view.get()) + { + // The free resize model follows size changes but does not follow + // position-only changes. Keep its base aligned if the restored + // window is moved again before its springs finish settling. + if (view->get_transformed_node()->get_transformer("wobbly")) + modify_wobbly(view, view->get_surface_root_node()->get_bounding_box()); + else view->erase_data(); + } }; wf::signal::connection_t on_unmap = [this] (auto *ev) { @@ -279,7 +353,7 @@ reply["ready"] = ready; reply["preview"] = bool(preview); reply["handoff"] = handing_off; reply["slot"] = destination.slot; reply["monitor"] = destination.monitor; reply["target"] = wf::ipc::geometry_to_json(preview ? preview->target : destination.geometry); - reply["delay"] = delay.value(); reply["preview_style"] = "held-window-wobbly"; + reply["delay"] = delay.value(); reply["preview_style"] = "held-window-wobbly"; reply["live_resize"] = true; return reply; }); } --- a/metadata/move.xml +++ b/metadata/move.xml @@ -36,6 +36,11 @@ -1 -1 + <_short>Snapping --- a/tests/move-snapping.py +++ b/tests/move-snapping.py @@ -4,7 +4,7 @@ from PIL import Image, ImageChops from pathlib import Path -stage=Path(os.environ.get('WCM_TEST_STAGE','/var/tmp/wayfire-move-snap-20261001')) +stage=Path(os.environ.get('WCM_TEST_STAGE','/var/tmp/wayfire-held-redraw-20261001')) runtime=Path(tempfile.mkdtemp(prefix='scope-toggle-test-')) env=os.environ.copy() for key in ['WAYLAND_DISPLAY','DISPLAY','WAYFIRE_SOCKET','_WAYFIRE_SOCKET','WAYFIRE_CONFIG_FILE','WAYFIRE_DEFAULT_CONFIG_BACKEND']: @@ -21,6 +21,9 @@ default_output = VIRTUAL-1 window_scope = physical window_scope_rules = [{"app_id":"tile-wide*","scope":"virtual"},{"app_id":"scope-pinned","scope":"HEADLESS-3"}] +[wobbly] +spring_k = 10 +friction = 10 [simple-tile] key_toggle = none tile_by_default = none @@ -227,20 +230,20 @@ return next(r['target'] for r in monitor['regions'] if r['slot']==slot) def arm_at(vid,m,x,y): prepare(vid,m);before=geometry(vid).copy();begin(vid);move((m-1)*640+x,y) - wait(lambda:status()['ready']);assert geometry(vid)==before + wait(lambda:status()['ready']);wait(lambda:all(geometry(vid)[k]==status()['target'][k] for k in ['width','height'])) return before try: env['WAYFIRE_SOCKET']=str(wait(lambda:next(runtime.glob('wayfire-*.socket'),None))) env['WAYLAND_DISPLAY']=next(p.name for p in runtime.glob('wayland-*') if not p.name.endswith('.lock')) print('Runtime:',runtime,flush=True);wait(lambda:names()==['VIRTUAL-1']) if os.environ.get('MOVE_TEST_UPGRADE'): - wait(lambda:'grid/snap-zones' in methods()) + wait(lambda:'move/snap-regions' in methods()) warm,wid=spawn_at('warm-grid',1);prepare(wid);begin(wid);move(320,350);button('release');time.sleep(.2) ipc('grid/slot_r',{'view-id':wid});time.sleep(.3);saved=geometry(wid) current=ipc('wayfire/get-config-option',{'option':'core/plugins'})['value'] current=current.replace(str(stage/'libgrid-old.so'),'grid').replace(str(stage/'libmove-old.so'),'move') ipc('wayfire/set-config-options',{'core/plugins':current}) - wait(lambda:'move/snap-regions' in methods());assert geometry(wid)==saved + wait(lambda:'move/snap-regions' in methods());assert status()['live_resize'];assert geometry(wid)==saved ipc('window-rules/close-view',{'id':wid});time.sleep(.2) print('PASS old resident Grid/Move replacement preserves windows and shared drag state',flush=True) wait(lambda:'move/snap-regions' in methods());assert 'grid/snap-zones' not in methods() @@ -264,10 +267,10 @@ app,vid=spawn_at('zone-drag',1);prepare(vid);before=geometry(vid).copy();begin(vid);move(320,1) time.sleep(.10);assert not status()['ready'] and not status()['preview'];assert geometry(vid)==before shot('before-delay');wait(lambda:status()['ready']);time.sleep(.8) - assert geometry(vid)==before and status()['target']==destination(1,'t') + assert all(geometry(vid)[k]==destination(1,'t')[k] for k in ['width','height']) and status()['target']==destination(1,'t') settled_preview(vid,destination(1,'t'),'held-top-preview') move(400,1);time.sleep(.3);settled_preview(vid,destination(1,'t'),'held-top-after-motion') - assert geometry(vid)==before + assert geometry(vid)['width']==destination(1,'t')['width'] print('PASS preview fully settles while button remains down and stays settled during in-zone motion',flush=True) move(360,350);wait(lambda:not status()['preview']);time.sleep(.6) assert geometry(vid)==before @@ -276,17 +279,19 @@ wait(lambda:all(abs(info(vid)['bbox'][k]-v)<=1 for k,v in rect(270,330,300,180).items())) shot('pulled-away');button('release');time.sleep(.3) assert geometry(vid)['width']==300 and geometry(vid)['height']==180 - print('PASS dwell, held-window preview without client resize, pull-away restoration and free release',flush=True) + print('PASS dwell, held-window redraw before release, pull-away restoration and free release',flush=True) arm_at(vid,1,320,1);settled_preview(vid,destination(1,'t'),'before-zone-change') move(638,350);assert not status()['ready'];wait(lambda:status()['ready']) settled_preview(vid,destination(1,'r'),'after-zone-change');end(vid,destination(1,'r')) print('PASS moving from a settled preview to another zone restarts dwell and settles again',flush=True) + ipc('grid/restore',{'view-id':vid});expect(vid,width=300,height=180) + print('PASS un-snapping after live redraw restores the original window size',flush=True) for m in [1,2,3]: for name,x,y in [('t',320,1),('b',320,718),('l',1,350),('r',638,350),('tl',1,1),('tr',638,1),('bl',1,718),('br',638,718)]: before=arm_at(vid,m,x,y) if m==1 or name=='t': settled_preview(vid,destination(m,name),f'held-{m}-{name}') - assert geometry(vid)==before + assert all(geometry(vid)[k]==destination(m,name)[k] for k in ['width','height']) end(vid,destination(m,name));wait(lambda:not status()['preview']) assert info(vid)['geometry']==destination(m,name) print(f'PASS all eight delayed destinations commit on physical monitor {m}',flush=True) @@ -306,7 +311,7 @@ ipc('grid/slot_r',{'view-id':vid});expect(vid,x=380,y=109.5,width=240,height=525) print('PASS Grid keyboard placement and size cycling remain independent',flush=True) prepare(vid);plugin('grid',False);options(enable_snap=True);options(snap_delay=100) - before=geometry(vid).copy();begin(vid);move(320,1);wait(lambda:status()['ready']);assert geometry(vid)==before + before=geometry(vid).copy();begin(vid);move(320,1);wait(lambda:status()['ready']);wait(lambda:all(geometry(vid)[k]==status()['target'][k] for k in ['width','height'])) end(vid,destination(1,'t'));wait(lambda:not status()['preview']) print('PASS Move snapping works with Grid disabled',flush=True) plugin('grid',True) @@ -322,7 +327,7 @@ plugin('wobbly',False);arm_at(vid,1,320,1);end(vid,destination(1,'t'));wait(lambda:not status()['preview']) plugin('wobbly',True);print('PASS preview and placement also work when Wobbly is disabled',flush=True) # Existing workspace dwell is independent of snap dwell and wraps a row. - options(enable_snap=False,workspace_switch_after=350) + options(enable_snap=False,workspace_switch_after=350,workspace_switch_repeat=False) ipc('wayfire/set-config-options',{'core/vwidth':6,'core/vheight':1}) def output_state():return next(o for o in ipc('window-rules/list-outputs') if o['name']=='VIRTUAL-1') def workspace():return output_state()['workspace']['x'] @@ -349,6 +354,19 @@ settled_preview(vid,destination(1,'l'),'wrapped-preview-left') end(vid,destination(1,'l'));wait(lambda:not status()['preview']) print('PASS wrapping in both directions also preserves held previews and drop placement',flush=True) + options(enable_snap=False,workspace_switch_repeat=True,workspace_switch_after=250) + prepare(vid,3);begin(vid);move(1918,350) + for expected_ws in [0,1,2]:wait(lambda:workspace()==expected_ws,2) + move(1600,350);stopped=workspace();time.sleep(.65);assert workspace()==stopped + move(1918,350);wait(lambda:workspace()!=(stopped),2);button('release') + stopped=workspace();time.sleep(.65);assert workspace()==stopped + prepare(vid);begin(vid);move(1,350) + for _ in range(3): + expected_ws=(workspace()-1)%6;wait(lambda:workspace()==expected_ws,2) + button('release');stopped=workspace();time.sleep(.65);assert workspace()==stopped + options(workspace_switch_repeat=False) + print('PASS repeated drag switching traverses both directions and stops on edge exit or release',flush=True) + options(enable_snap=False) ipc('vswitch/set-workspace',{'x':0,'y':0,'output-id':output_state()['id'],'view-id':vid}) wait(lambda:workspace()==0);time.sleep(.6) @@ -363,6 +381,35 @@ assert geometry(vid)['width']==300 and geometry(vid)['height']==180 plugin('move',True);wait(lambda:'move/snap-regions' in methods()) print('PASS disabling Move during preview cancels safely and reloads cleanly',flush=True) + patterned,pid=spawn_at('redraw-pattern',1) + command(patterned,'pattern 1');time.sleep(.1) + arm_at(pid,1,320,1) + target=destination(1,'t') + def contents_redrawn(): + image=shot('live-redraw-pattern');x,y=int(target['x']),int(target['y']) + return image.getpixel((x+8,y+8))==(34,68,170) and image.getpixel((x+24,y+8))==(238,204,51) and image.getpixel((x+40,y+8))==(34,68,170) + wait(contents_redrawn,5) + assert status()['ready'] and status()['preview'] + assert f"CONFIG main {int(target['width'])} {int(target['height'])}" in (runtime/'redraw-pattern.log').read_text() + move(350,350);button('release');expect(pid,width=300,height=180) + ipc('window-rules/close-view',{'id':pid}) + print('PASS client redraws real 16px content at snap size before release; immediate pull-away restores size',flush=True) + import signal + options(snap_delay=0);prepare(vid);begin(vid) + os.kill(app.pid,signal.SIGSTOP) + try: + move(320,1);wait(lambda:status()['ready']) + move(360,350);button('release') + finally:os.kill(app.pid,signal.SIGCONT) + def restored_drag(): + g=geometry(vid) + # Wobbly's rounded grab bounding box can shift the fractional origin by + # less than a pixel. Size must be exact, and late replies cannot drift. + return g['width']==300 and g['height']==180 and abs(g['x']-210)<=1 and abs(g['y']-290)<=1 + wait(restored_drag);time.sleep(.6);assert restored_drag() + + print('PASS late client redraw cannot overwrite immediate pull-away/release geometry',flush=True) + prepare(vid);begin(vid);move(1,350);wait(lambda:status()['ready']) ipc('window-rules/close-view',{'id':vid});wait(lambda:not status()['preview']);button('release') print('PASS closing a held window cancels preview and timer safely',flush=True) --- /dev/null +++ b/tests/held-redraw-client.c @@ -0,0 +1,64 @@ +#define _GNU_SOURCE +#include +#include "xdg-shell-client.h" +#include "wlr-layer-shell-client.h" +#include +#include +#include +#include +#include +#include +#include +static struct wl_display *display; +static struct wl_compositor *compositor; +static struct wl_shm *shm; +static struct xdg_wm_base *wm; +static struct zwlr_layer_shell_v1 *layer_shell; +struct window {struct wl_surface *surface; struct xdg_surface *xdg; struct xdg_toplevel *top; int w,h,fs,max;}; +static struct window main_window,dialog; +static const char *app; +static int pattern; +static void release(void *data,struct wl_buffer *buffer){wl_buffer_destroy(buffer);} +static const struct wl_buffer_listener releases={release}; +static void draw(struct wl_surface *s,int w,int h,uint32_t color){ + int fd=memfd_create("scope-test",MFD_CLOEXEC);size_t size=(size_t)w*h*4;assert(fd>=0&&ftruncate(fd,size)==0); + uint32_t *p=mmap(NULL,size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);assert(p!=MAP_FAILED); + for(size_t i=0;iw)w->w=300;if(!w->h)w->h=180;printf("CONFIG %s %d %d fs=%d max=%d\n",w==&main_window?"main":"dialog",w->w,w->h,w->fs,w->max);draw(w->surface,w->w,w->h,0xff2244aa);} +static const struct xdg_surface_listener conf={configured}; +static void top_config(void *data,struct xdg_toplevel *t,int32_t width,int32_t height,struct wl_array *states){struct window *w=data;w->w=width;w->h=height;w->fs=w->max=0;uint32_t *v;wl_array_for_each(v,states){if(*v==XDG_TOPLEVEL_STATE_FULLSCREEN)w->fs=1;if(*v==XDG_TOPLEVEL_STATE_MAXIMIZED)w->max=1;}} +static void top_close(void*d,struct xdg_toplevel*t){exit(0);} +static const struct xdg_toplevel_listener top_events={.configure=top_config,.close=top_close}; +static void ping(void*d,struct xdg_wm_base*w,uint32_t serial){xdg_wm_base_pong(w,serial);} +static const struct xdg_wm_base_listener wm_events={ping}; +static void global(void*d,struct wl_registry*r,uint32_t id,const char*name,uint32_t version){ + if(!strcmp(name,"wl_compositor"))compositor=wl_registry_bind(r,id,&wl_compositor_interface,4); + if(!strcmp(name,"wl_shm"))shm=wl_registry_bind(r,id,&wl_shm_interface,1); + if(!strcmp(name,"xdg_wm_base"))wm=wl_registry_bind(r,id,&xdg_wm_base_interface,2); + if(!strcmp(name,"zwlr_layer_shell_v1"))layer_shell=wl_registry_bind(r,id,&zwlr_layer_shell_v1_interface,1); +} +static void removed(void*d,struct wl_registry*r,uint32_t id){} +static const struct wl_registry_listener registry_events={global,removed}; +static void create(struct window*w,int child){w->surface=wl_compositor_create_surface(compositor);w->xdg=xdg_wm_base_get_xdg_surface(wm,w->surface);xdg_surface_add_listener(w->xdg,&conf,w);w->top=xdg_surface_get_toplevel(w->xdg);xdg_toplevel_add_listener(w->top,&top_events,w);xdg_toplevel_set_app_id(w->top,child?"scope-dialog":app);xdg_toplevel_set_title(w->top,child?"Scope dialog":"Scope client");if(child)xdg_toplevel_set_parent(w->top,main_window.top);} +static struct wl_surface *bar; +static void layer_config(void*d,struct zwlr_layer_surface_v1*s,uint32_t serial,uint32_t w,uint32_t h){zwlr_layer_surface_v1_ack_configure(s,serial);draw(bar,w,h,0xffee2222);} +static void layer_close(void*d,struct zwlr_layer_surface_v1*s){} +static const struct zwlr_layer_surface_v1_listener layer_events={layer_config,layer_close}; +int main(int argc,char**argv){setbuf(stdout,NULL);setbuf(stdin,NULL);app=argc>1?argv[1]:"scope-normal";display=wl_display_connect(NULL);assert(display);struct wl_registry*r=wl_display_get_registry(display);wl_registry_add_listener(r,®istry_events,NULL);wl_display_roundtrip(display);assert(compositor&&shm&&wm);xdg_wm_base_add_listener(wm,&wm_events,NULL); + if(argc>2&&!strcmp(argv[2],"bar")){assert(layer_shell);bar=wl_compositor_create_surface(compositor);struct zwlr_layer_surface_v1*l=zwlr_layer_shell_v1_get_layer_surface(layer_shell,bar,NULL,ZWLR_LAYER_SHELL_V1_LAYER_TOP,"scope-bar");zwlr_layer_surface_v1_add_listener(l,&layer_events,NULL);zwlr_layer_surface_v1_set_size(l,0,24);zwlr_layer_surface_v1_set_anchor(l,ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP|ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT|ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT);zwlr_layer_surface_v1_set_exclusive_zone(l,24);wl_surface_commit(bar); + }else{create(&main_window,0);if(argc>2&&!strcmp(argv[2],"fullscreen"))xdg_toplevel_set_fullscreen(main_window.top,NULL);if(argc>2&&!strcmp(argv[2],"maximize"))xdg_toplevel_set_maximized(main_window.top);wl_surface_commit(main_window.surface);} + while(1){wl_display_dispatch_pending(display);wl_display_flush(display);struct pollfd fds[2]={{wl_display_get_fd(display),POLLIN,0},{STDIN_FILENO,POLLIN,0}};assert(poll(fds,2,-1)>=0);if(fds[0].revents&POLLIN){if(wl_display_dispatch(display)<0)break;}if(fds[1].revents&POLLIN){char cmd[256];if(!fgets(cmd,sizeof(cmd),stdin))break; + if(!strncmp(cmd,"pattern 1",9)){pattern=1;draw(main_window.surface,main_window.w,main_window.h,0xff2244aa);} + else if(!strncmp(cmd,"fullscreen 1",12))xdg_toplevel_set_fullscreen(main_window.top,NULL); + else if(!strncmp(cmd,"fullscreen 0",12))xdg_toplevel_unset_fullscreen(main_window.top); + else if(!strncmp(cmd,"maximize 1",10))xdg_toplevel_set_maximized(main_window.top); + else if(!strncmp(cmd,"maximize 0",10))xdg_toplevel_unset_maximized(main_window.top); + else if(!strncmp(cmd,"dialog",6)){create(&dialog,1);wl_surface_commit(dialog.surface);} + else if(!strncmp(cmd,"appid ",6)){cmd[strcspn(cmd,"\n")]=0;xdg_toplevel_set_app_id(main_window.top,cmd+6);} + if(main_window.surface)wl_surface_commit(main_window.surface); + }}return 0;}