1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
|
--- a/plugins/single_plugins/move.cpp
+++ b/plugins/single_plugins/move.cpp
@@ -56,7 +56,8 @@
wf::option_wrapper_t<bool> move_enable_snap_off{"move/enable_snap_off"};
wf::option_wrapper_t<int> move_snap_off_threshold{"move/snap_off_threshold"};
- wf::wl_timer<false> workspace_switch_timer;
+ wf::option_wrapper_t<bool> workspace_switch_repeat{"move/workspace_switch_repeat"};
+ wf::wl_timer<true> workspace_switch_timer;
wf::grid::slot_t workspace_switch_slot_id = wf::grid::SLOT_NONE;
wf::shared_data::ref_ptr_t<wf::move_drag::core_drag_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<move_snap_restore_t>() &&
+ !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<move_snap_anchor_t>();
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<move_snap_restore_t>())
+ {
+ 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<move_snap_restore_t>();
+ 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<move_snap_anchor_t>();
+ break;
+ }
+ view->erase_data<move_snap_restore_t>();
+}
+
class held_preview_t : public wf::scene::transformer_base_node_t
{
class instance_t : public wf::scene::transformer_render_instance_t<held_preview_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<move_snap_restore_t>(); 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<move_snap_restore_t>())
+ {
+ auto saved = view->get_data_safe<move_snap_restore_t>();
+ 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<wf::view_geometry_changed_signal> 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<move_snap_anchor_t>() && 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<move_snap_anchor_t>();
+ }
};
wf::signal::connection_t<wf::view_pre_unmap_signal> 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 @@
<default>-1</default>
<min>-1</min>
</option>
+ <option name="workspace_switch_repeat" type="bool">
+ <_short>Repeat workspace switching</_short>
+ <_long>Keep switching at the workspace-switch interval while holding a dragged window at an output edge. Wrap after the last workspace. Leaving the edge or releasing stops the repeat.</_long>
+ <default>true</default>
+ </option>
<group>
<_short>Snapping</_short>
<option name="enable_snap" type="bool"><_short>Snap</_short><_long>Preview the held window after a short dwell at an edge or corner. Release to snap, or pull away to keep moving. Top and bottom edges select halves.</_long><default>true</default></option>
--- 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 <wayland-client.h>
+#include "xdg-shell-client.h"
+#include "wlr-layer-shell-client.h"
+#include <sys/mman.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <poll.h>
+#include <assert.h>
+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;i<size/4;i++)p[i]=pattern && s==main_window.surface && ((i%w/16+i/w/16)%2) ? 0xffeecc33 : color;
+ struct wl_shm_pool *pool=wl_shm_create_pool(shm,fd,size);
+ struct wl_buffer *b=wl_shm_pool_create_buffer(pool,0,w,h,w*4,WL_SHM_FORMAT_XRGB8888);
+ wl_buffer_add_listener(b,&releases,NULL);wl_shm_pool_destroy(pool);close(fd);munmap(p,size);
+ wl_surface_attach(s,b,0,0);wl_surface_damage(s,0,0,w,h);wl_surface_commit(s);
+}
+static void configured(void *data,struct xdg_surface *s,uint32_t serial){struct window *w=data;xdg_surface_ack_configure(s,serial);if(!w->w)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;}
|