1 #pragma once
2
3 #include <wayfire/nonstd/reverse.hpp>
4 #include <wayfire/plugins/wobbly/wobbly-signal.hpp>
5 #include <wayfire/object.hpp>
6 #include <wayfire/output-layout.hpp>
7 #include <wayfire/nonstd/observer_ptr.h>
8 #include <wayfire/render-manager.hpp>
9 #include <wayfire/workspace-manager.hpp>
10 #include <wayfire/view-transform.hpp>
11 #include <wayfire/util/duration.hpp>
12 #include <wayfire/util/log.hpp>
13 #include <cmath>
14
15
16 namespace wf
17 {
18 /**
19 * A collection of classes and interfaces which can be used by plugins which
20 * support dragging views to move them.
21 *
22 * A plugin using these APIs would get support for:
23 *
24 * - Moving views on the same output, following the pointer or touch position.
25 * - Holding views in place until a certain threshold is reached
26 * - Wobbly windows (if enabled)
27 * - Move the view freely between different outputs with different plugins active
28 * on them, as long as all of these plugins support this interface.
29 * - Show smooth transitions of the moving view when moving between different
30 * outputs.
31 *
32 * A plugin using these APIs is expected to:
33 * - Grab input on its respective output and forward any events to the core_drag_t
34 * singleton.
35 * - Have activated itself with CAPABILITY_MANAGE_COMPOSITOR
36 * - Connect to and handle the signals described below.
37 */
38 namespace move_drag
39 {
40 /**
41 * name: focus-output
42 * on: core_drag_t
43 * when: Emitted output whenever the output where the drag happens changes,
44 * including when the drag begins.
45 */
46 struct drag_focus_output_signal : public signal_data_t
47 {
48 /** The output which was focused up to now, might be null. */
49 wf::output_t *previous_focus_output;
50 /** The output which was focused now. */
51 wf::output_t *focus_output;
52 };
53
54 /**
55 * name: snap-off
56 * on: core_drag_t
57 * when: Emitted if snap-off is enabled and the view was moved more than the
58 * threshold.
59 */
60 struct snap_off_signal : public signal_data_t
61 {
62 /** The output which is focused now. */
63 wf::output_t *focus_output;
64 };
65
66 /**
67 * name: done
68 * on: core_drag_t
69 * when: Emitted after the drag operation has ended, and if the view is unmapped
70 * while being dragged.
71 */
72 struct drag_done_signal : public signal_data_t
73 {
74 /** The output where the view was dropped. */
75 wf::output_t *focused_output;
76
77 /** Whether join-views was enabled for this drag. */
78 bool join_views;
79
80 struct view_t
81 {
82 /** Dragged view. */
83 wayfire_view view;
84
85 /**
86 * The position relative to the view where the grab was.
87 * See scale_around_grab_t::relative_grab
88 */
89 wf::pointf_t relative_grab;
90 };
91
92 /** All views which were dragged. */
93 std::vector<view_t> all_views;
94
95 /** The main view which was dragged. */
96 wayfire_view main_view;
97
98 /**
99 * The position of the input when the view was dropped.
100 * In output-layout coordinates.
101 */
102 wf::point_t grab_position;
103 };
104
105 /**
106 * Find the geometry of a view, if it has size @size, it is grabbed at point @grab,
107 * and the grab is at position @relative relative to the view.
108 */
find_geometry_around(wf::dimensions_t size,wf::point_t grab,wf::pointf_t relative)109 inline static wf::geometry_t find_geometry_around(
110 wf::dimensions_t size, wf::point_t grab, wf::pointf_t relative)
111 {
112 return wf::geometry_t{
113 grab.x - (int)std::floor(relative.x * size.width),
114 grab.y - (int)std::floor(relative.y * size.height),
115 size.width,
116 size.height,
117 };
118 }
119
120 /**
121 * Find the position of grab relative to the view.
122 * Example: returns [0.5, 0.5] if the grab is the midpoint of the view.
123 */
find_relative_grab(wf::geometry_t view,wf::point_t grab)124 inline static wf::pointf_t find_relative_grab(
125 wf::geometry_t view, wf::point_t grab)
126 {
127 return wf::pointf_t{
128 1.0 * (grab.x - view.x) / view.width,
129 1.0 * (grab.y - view.y) / view.height,
130 };
131 }
132
133 /**
134 * A transformer used while dragging.
135 *
136 * It is primarily used to scale the view is a plugin needs it, and also to keep it
137 * centered around the `grab_position`.
138 */
139 class scale_around_grab_t : public wf::view_transformer_t
140 {
141 public:
142 /**
143 * Factor for scaling down the view.
144 * A factor 2.0 means that the view will have half of its width and height.
145 */
146 wf::animation::simple_animation_t scale_factor{wf::create_option(300)};
147
148 /**
149 * A place relative to the view, where it is grabbed.
150 *
151 * Coordinates are [0, 1]. A grab at (0.5, 0.5) means that the view is grabbed
152 * at its center.
153 */
154 wf::pointf_t relative_grab;
155
156 /**
157 * The position where the grab appears on the outputs, in output-layout
158 * coordinates.
159 */
160 wf::point_t grab_position;
161
get_z_order()162 uint32_t get_z_order() override
163 {
164 return wf::TRANSFORMER_HIGHLEVEL - 1;
165 }
166
transform_opaque_region(wf::geometry_t box,wf::region_t region)167 wf::region_t transform_opaque_region(
168 wf::geometry_t box, wf::region_t region) override
169 {
170 // TODO: figure out a way to take opaque region into account
171 return {};
172 }
173
scale_around_grab(wf::geometry_t view,wf::pointf_t point,double factor)174 wf::pointf_t scale_around_grab(wf::geometry_t view, wf::pointf_t point,
175 double factor)
176 {
177 auto gx = view.x + view.width * relative_grab.x;
178 auto gy = view.y + view.height * relative_grab.y;
179
180 return {
181 (point.x - gx) * factor + gx,
182 (point.y - gy) * factor + gy,
183 };
184 }
185
transform_point(wf::geometry_t view,wf::pointf_t point)186 wf::pointf_t transform_point(wf::geometry_t view, wf::pointf_t point) override
187 {
188 LOGE("Unexpected transform_point() call for dragged overlay view!");
189 return scale_around_grab(view, point, 1.0 / scale_factor);
190 }
191
untransform_point(wf::geometry_t view,wf::pointf_t point)192 wf::pointf_t untransform_point(wf::geometry_t view, wf::pointf_t point) override
193 {
194 LOGE("Unexpected untransform_point() call for dragged overlay view!");
195 return scale_around_grab(view, point, scale_factor);
196 }
197
get_bounding_box(wf::geometry_t view,wf::geometry_t region)198 wf::geometry_t get_bounding_box(wf::geometry_t view,
199 wf::geometry_t region) override
200 {
201 int w = std::floor(view.width / scale_factor);
202 int h = std::floor(view.height / scale_factor);
203
204 auto bb = find_geometry_around({w, h}, grab_position, relative_grab);
205 return bb;
206 }
207
render_with_damage(wf::texture_t src_tex,wlr_box src_box,const wf::region_t & damage,const wf::framebuffer_t & target_fb)208 void render_with_damage(wf::texture_t src_tex, wlr_box src_box,
209 const wf::region_t& damage, const wf::framebuffer_t& target_fb) override
210 {
211 // Get target size
212 auto bbox = get_bounding_box(src_box, src_box);
213
214 OpenGL::render_begin(target_fb);
215 for (auto& rect : damage)
216 {
217 target_fb.logic_scissor(wlr_box_from_pixman_box(rect));
218 OpenGL::render_texture(src_tex, target_fb, bbox);
219 }
220
221 OpenGL::render_end();
222 }
223 };
224
225 static const std::string move_drag_transformer = "move-drag-transformer";
226
227 /**
228 * Represents a view which is being dragged.
229 * Multiple views exist only if join_views is set to true.
230 */
231 struct dragged_view_t
232 {
233 // The view being dragged
234 wayfire_view view;
235
236 // Its transformer
237 nonstd::observer_ptr<scale_around_grab_t> transformer;
238
239 // The last bounding box used for damage.
240 // This is needed in case the view resizes or something like that, in which
241 // case we don't have access to the previous bbox.
242 wf::geometry_t last_bbox;
243 };
244
get_toplevel(wayfire_view view)245 inline wayfire_view get_toplevel(wayfire_view view)
246 {
247 while (view->parent)
248 {
249 view = view->parent;
250 }
251
252 return view;
253 }
254
get_target_views(wayfire_view grabbed,bool join_views)255 inline std::vector<wayfire_view> get_target_views(wayfire_view grabbed,
256 bool join_views)
257 {
258 std::vector<wayfire_view> r = {grabbed};
259 if (join_views)
260 {
261 r = grabbed->enumerate_views();
262 }
263
264 return r;
265 }
266
267 /**
268 * An object for storing per-output data.
269 */
270 class output_data_t : public noncopyable_t, public custom_data_t
271 {
272 public:
output_data_t(wf::output_t * output,std::vector<dragged_view_t> views)273 output_data_t(wf::output_t *output, std::vector<dragged_view_t> views)
274 {
275 output->render->add_effect(&damage_overlay, OUTPUT_EFFECT_PRE);
276 output->render->add_effect(&render_overlay, OUTPUT_EFFECT_OVERLAY);
277
278 this->output = output;
279 this->views = views;
280 }
281
~output_data_t()282 ~output_data_t()
283 {
284 output->render->rem_effect(&damage_overlay);
285 output->render->rem_effect(&render_overlay);
286 }
287
apply_damage()288 void apply_damage()
289 {
290 for (auto& view : views)
291 {
292 // Note: bbox will be in output layout coordinates now, since this is
293 // how the transformer works
294 auto bbox = view.view->get_bounding_box();
295 bbox = bbox + -wf::origin(output->get_layout_geometry());
296
297 output->render->damage(bbox);
298 output->render->damage(view.last_bbox);
299
300 view.last_bbox = bbox;
301 }
302 }
303
304 private:
305 wf::output_t *output;
306
307 std::vector<dragged_view_t> views;
308
309 // An effect hook for damaging the view on the current output.
310 //
311 // This is needed on a per-output basis in order to drive the scaling animation
312 // forward, if such an animation is running.
313 //
314 // TODO: We overdo damage, for ex. in the following cases:
315 // - Expo does not need any damage (can't really be fixed, since we don't know
316 // the plugin which uses this API).
317 // - If the view has not updated, and cursor has not moved
318 effect_hook_t damage_overlay = [=] ()
__anon3cc5b27e0102() 319 {
320 apply_damage();
321 };
322
323 effect_hook_t render_overlay = [=] ()
__anon3cc5b27e0202() 324 {
325 auto fb = output->render->get_target_framebuffer();
326 fb.geometry = output->get_layout_geometry();
327
328 for (auto& view : wf::reverse(views))
329 {
330 // Convert damage from output-local coordinates (last_bbox) to
331 // output-layout coords.
332 wf::region_t damage;
333 damage |= view.last_bbox + wf::origin(fb.geometry);
334
335 // Render the full view, always
336 // Not very efficient
337 view.view->render_transformed(fb, std::move(damage));
338 }
339 };
340 };
341
342 struct drag_options_t
343 {
344 /**
345 * Whether to enable snap off, that is, hold the view in place until
346 * a certain threshold is reached.
347 */
348 bool enable_snap_off = false;
349
350 /**
351 * If snap-off is enabled, the amount of pixels to wait for motion until
352 * snap-off is triggered.
353 */
354 int snap_off_threshold = 0;
355
356 /**
357 * Join views together, i.e move main window and dialogues together.
358 */
359 bool join_views = false;
360
361 double initial_scale = 1.0;
362 };
363
364 /**
365 * An object for storing global move drag data (i.e shared between all outputs).
366 *
367 * Intended for use via wf::shared_data::ref_ptr_t.
368 */
369 class core_drag_t : public signal_provider_t
370 {
371 /**
372 * Rebuild the wobbly model after a change in the scaling, so that the wobbly
373 * model does not try to animate the scaling change itself.
374 */
rebuild_wobbly(wayfire_view view,wf::point_t grab,wf::pointf_t relative)375 void rebuild_wobbly(wayfire_view view, wf::point_t grab, wf::pointf_t relative)
376 {
377 auto dim = wf::dimensions(view->get_bounding_box("wobbly"));
378 modify_wobbly(view, find_geometry_around(dim, grab, relative));
379 }
380
381 public:
382 /**
383 * Start drag.
384 *
385 * @param grab_view The view which is being dragged.
386 * @param grab_position The position of the input, in output-layout coordinates.
387 * @param relative The position of the grab_position relative to view.
388 */
start_drag(wayfire_view grab_view,wf::point_t grab_position,wf::pointf_t relative,const drag_options_t & options)389 void start_drag(wayfire_view grab_view, wf::point_t grab_position,
390 wf::pointf_t relative,
391 const drag_options_t& options)
392 {
393 auto bbox = grab_view->get_bounding_box("wobbly");
394 wf::point_t rel_grab_pos = {
395 int(bbox.x + relative.x * bbox.width),
396 int(bbox.y + relative.y * bbox.height),
397 };
398
399 if (options.join_views)
400 {
401 grab_view = get_toplevel(grab_view);
402 }
403
404 this->view = grab_view;
405 this->params = options;
406
407 auto target_views = get_target_views(grab_view, options.join_views);
408 for (auto& v : target_views)
409 {
410 dragged_view_t dragged;
411 dragged.view = v;
412
413 // Setup view transform
414 auto tr = std::make_unique<scale_around_grab_t>();
415 dragged.transformer = {tr};
416
417 tr->relative_grab = find_relative_grab(
418 v->get_bounding_box("wobbly"), rel_grab_pos);
419 tr->grab_position = grab_position;
420 tr->scale_factor.animate(options.initial_scale, options.initial_scale);
421
422 v->add_transformer(std::move(tr), move_drag_transformer);
423
424 // Hide the view, we will render it as an overlay
425 v->set_visible(false);
426 v->damage();
427
428 // Make sure that wobbly has the correct geometry from the start!
429 rebuild_wobbly(v, grab_position, dragged.transformer->relative_grab);
430
431 // TODO: make this configurable!
432 start_wobbly_rel(v, dragged.transformer->relative_grab);
433
434 this->all_views.push_back(dragged);
435 v->connect_signal("unmapped", &on_view_unmap);
436 }
437
438 // Setup overlay hooks
439 for (auto& output : wf::get_core().output_layout->get_outputs())
440 {
441 output->store_data(
442 std::make_unique<output_data_t>(output, all_views));
443 }
444
445 wf::get_core().set_cursor("grabbing");
446
447 // Set up snap-off
448 if (params.enable_snap_off)
449 {
450 for (auto& v : all_views)
451 {
452 set_tiled_wobbly(v.view, true);
453 }
454
455 grab_origin = grab_position;
456 view_held_in_place = true;
457 }
458 }
459
start_drag(wayfire_view view,wf::point_t grab_position,const drag_options_t & options)460 void start_drag(wayfire_view view, wf::point_t grab_position,
461 const drag_options_t& options)
462 {
463 if (options.join_views)
464 {
465 view = get_toplevel(view);
466 }
467
468 auto bbox = view->get_bounding_box() +
469 wf::origin(view->get_output()->get_layout_geometry());
470 start_drag(view, grab_position,
471 find_relative_grab(bbox, grab_position), options);
472 }
473
handle_motion(wf::point_t to)474 void handle_motion(wf::point_t to)
475 {
476 if (view_held_in_place)
477 {
478 auto current_offset = to - grab_origin;
479 const int dst_sq = current_offset.x * current_offset.x +
480 current_offset.y * current_offset.y;
481 const int thresh_sq =
482 params.snap_off_threshold * params.snap_off_threshold;
483
484 if (dst_sq >= thresh_sq)
485 {
486 view_held_in_place = false;
487 for (auto& v : all_views)
488 {
489 set_tiled_wobbly(v.view, false);
490 }
491
492 snap_off_signal data;
493 data.focus_output = current_output;
494 emit_signal("snap-off", &data);
495 }
496 }
497
498 // Update wobbly independently of the grab position.
499 // This is because while held in place, wobbly is anchored to its edges
500 // so we can still move the grabbed point without moving the view.
501 for (auto& v : all_views)
502 {
503 move_wobbly(v.view, to.x, to.y);
504 if (!view_held_in_place)
505 {
506 v.transformer->grab_position = to;
507 }
508 }
509
510 update_current_output(to);
511 }
512
handle_input_released()513 void handle_input_released()
514 {
515 // Store data for the drag done signal
516 drag_done_signal data;
517 data.grab_position = all_views.front().transformer->grab_position;
518 for (auto& v : all_views)
519 {
520 data.all_views.push_back(
521 {v.view, v.transformer->relative_grab});
522 }
523
524 data.main_view = this->view;
525 data.focused_output = current_output;
526 data.join_views = params.join_views;
527
528 // Remove overlay hooks and damage outputs BEFORE popping the transformer
529 for (auto& output : wf::get_core().output_layout->get_outputs())
530 {
531 output->get_data<output_data_t>()->apply_damage();
532 output->erase_data<output_data_t>();
533 }
534
535 for (auto& v : all_views)
536 {
537 auto grab_position = v.transformer->grab_position;
538 auto rel_pos = v.transformer->relative_grab;
539
540 // Restore view to where it was before
541 v.view->set_visible(true);
542 v.view->pop_transformer(move_drag_transformer);
543
544 // Reset wobbly and leave it in output-LOCAL coordinates
545 end_wobbly(v.view);
546
547 // Important! If the view scale was not 1.0, the wobbly model needs to be
548 // updated with the new size. Since this is an artificial resize, we need
549 // to make sure that the resize happens smoothly.
550 rebuild_wobbly(v.view, grab_position, rel_pos);
551
552 // Put wobbly back in output-local space, the plugins will take it from
553 // here.
554 translate_wobbly(v.view,
555 -wf::origin(v.view->get_output()->get_layout_geometry()));
556 }
557
558 // Reset our state
559 view = nullptr;
560 all_views.clear();
561 current_output = nullptr;
562 view_held_in_place = false;
563 wf::get_core().set_cursor("default");
564
565 // Lastly, let the plugins handle what happens on drag end.
566 emit_signal("done", &data);
567 on_view_unmap.disconnect();
568 }
569
set_scale(double new_scale)570 void set_scale(double new_scale)
571 {
572 for (auto& view : all_views)
573 {
574 view.transformer->scale_factor.animate(new_scale);
575 }
576 }
577
is_view_held_in_place()578 bool is_view_held_in_place()
579 {
580 return view_held_in_place;
581 }
582
583 // View currently being moved.
584 wayfire_view view;
585
586 // Output where the action is happening.
587 wf::output_t *current_output = NULL;
588
589 private:
590 // All views being dragged, more than one in case of join_views.
591 std::vector<dragged_view_t> all_views;
592
593 // Current parameters
594 drag_options_t params;
595
596 // Grab origin, used for snap-off
597 wf::point_t grab_origin;
598
599 // View is held in place, waiting for snap-off
600 bool view_held_in_place = false;
601
update_current_output(wf::point_t grab)602 void update_current_output(wf::point_t grab)
603 {
604 wf::pointf_t origin = {1.0 * grab.x, 1.0 * grab.y};
605 auto output =
606 wf::get_core().output_layout->get_output_coords_at(origin, origin);
607
608 if (output != current_output)
609 {
610 drag_focus_output_signal data;
611 data.previous_focus_output = current_output;
612
613 current_output = output;
614 data.focus_output = output;
615 wf::get_core().focus_output(output);
616 emit_signal("focus-output", &data);
617 }
618 }
619
620 wf::signal_connection_t on_view_unmap = [=] (auto *ev)
__anon3cc5b27e0302(auto *ev) 621 {
622 handle_input_released();
623 };
624 };
625
626 /**
627 * Move the view to the target output and put it at the coordinates of the grab.
628 * Also take into account view's fullscreen and tiled state.
629 *
630 * Unmapped views are ignored.
631 */
adjust_view_on_output(drag_done_signal * ev)632 inline void adjust_view_on_output(drag_done_signal *ev)
633 {
634 // Any one of the views that are being dragged.
635 // They are all part of the same view tree.
636 auto parent = get_toplevel(ev->main_view);
637 if (!parent->is_mapped())
638 {
639 return;
640 }
641
642 if (parent->get_output() != ev->focused_output)
643 {
644 wf::get_core().move_view_to_output(parent, ev->focused_output, false);
645 }
646
647 // Calculate the position we're leaving the view on
648 auto output_delta = -wf::origin(ev->focused_output->get_layout_geometry());
649 auto grab = ev->grab_position + output_delta;
650
651 auto output_geometry = ev->focused_output->get_relative_geometry();
652 auto current_ws = ev->focused_output->workspace->get_current_workspace();
653 wf::point_t target_ws{
654 (int)std::floor(1.0 * grab.x / output_geometry.width),
655 (int)std::floor(1.0 * grab.y / output_geometry.height),
656 };
657 target_ws = target_ws + current_ws;
658
659 auto gsize = ev->focused_output->workspace->get_workspace_grid_size();
660 target_ws.x = wf::clamp(target_ws.x, 0, gsize.width - 1);
661 target_ws.y = wf::clamp(target_ws.y, 0, gsize.height - 1);
662
663 for (auto& v : ev->all_views)
664 {
665 if (!v.view->is_mapped())
666 {
667 // Maybe some dialog got unmapped
668 continue;
669 }
670
671 auto bbox = v.view->get_bounding_box("wobbly");
672 auto wm = v.view->get_wm_geometry();
673
674 wf::point_t wm_offset = wf::origin(wm) + -wf::origin(bbox);
675 bbox = wf::move_drag::find_geometry_around(
676 wf::dimensions(bbox), grab, v.relative_grab);
677
678 wf::point_t target = wf::origin(bbox) + wm_offset;
679 v.view->move(target.x, target.y);
680 if (v.view->fullscreen)
681 {
682 v.view->fullscreen_request(ev->focused_output, true, target_ws);
683 } else if (v.view->tiled_edges)
684 {
685 v.view->tile_request(v.view->tiled_edges, target_ws);
686 }
687 }
688
689 // Ensure that every view is visible on parent's main workspace
690 for (auto& v : parent->enumerate_views())
691 {
692 ev->focused_output->workspace->move_to_workspace(v, target_ws);
693 }
694
695 ev->focused_output->focus_view(parent, true);
696 }
697
698 /**
699 * Adjust the view's state after snap-off.
700 */
adjust_view_on_snap_off(wayfire_view view)701 inline void adjust_view_on_snap_off(wayfire_view view)
702 {
703 if (view->tiled_edges && !view->fullscreen)
704 {
705 view->tile_request(0);
706 }
707 }
708 }
709 }
710