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