Skip to main content

freya_borderless_plugin/
lib.rs

1use freya_core::prelude::*;
2use freya_engine::prelude::{
3    ClipOp,
4    Color,
5    RRect,
6    SkRect,
7};
8use freya_winit::{
9    extensions::WinitPlatformExt,
10    plugins::{
11        FreyaPlugin,
12        PluginEvent,
13        PluginHandle,
14    },
15    winit::window::ResizeDirection,
16};
17use torin::{
18    position::Position,
19    prelude::{
20        Area,
21        Point2D,
22        Size2D,
23    },
24    size::Size,
25};
26
27const DIRECTIONS: [ResizeDirection; 8] = [
28    ResizeDirection::North,
29    ResizeDirection::South,
30    ResizeDirection::West,
31    ResizeDirection::East,
32    ResizeDirection::NorthWest,
33    ResizeDirection::NorthEast,
34    ResizeDirection::SouthWest,
35    ResizeDirection::SouthEast,
36];
37
38/// Plugin for windows without native decorations.
39///
40/// # Platforms
41///
42/// - **Linux** and **Windows**: overlays [ResizeBands] on top of the app, so dragging
43///   the window borders resizes it and hovering them shows the resize cursors.
44/// - **macOS**: no bands, resizing is left to the system.
45/// - **All**: an optional corner radius clips the whole canvas, overlay layers included.
46///
47/// # Example
48///
49/// ```rust,no_run
50/// # use freya_winit::{config::{LaunchConfig, WindowConfig}, launch};
51/// # use freya_borderless_plugin::BorderlessPlugin;
52/// # fn app() -> impl freya_core::prelude::IntoElement { freya_core::prelude::rect() }
53/// launch(
54///     LaunchConfig::new()
55///         .with_plugin(BorderlessPlugin::new().with_corner_radius(12.))
56///         .with_window(WindowConfig::new(app).with_decorations(false)),
57/// )
58/// ```
59pub struct BorderlessPlugin {
60    thickness: f32,
61    corner_radius: f32,
62}
63
64impl Default for BorderlessPlugin {
65    fn default() -> Self {
66        Self {
67            thickness: 6.,
68            corner_radius: 0.,
69        }
70    }
71}
72
73impl BorderlessPlugin {
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    /// How far into the window each resize band reaches.
79    pub fn with_thickness(mut self, thickness: f32) -> Self {
80        self.thickness = thickness;
81        self
82    }
83
84    /// Clip the whole canvas with this corner radius, in logical pixels.
85    pub fn with_corner_radius(mut self, corner_radius: f32) -> Self {
86        self.corner_radius = corner_radius;
87        self
88    }
89}
90
91impl FreyaPlugin for BorderlessPlugin {
92    fn plugin_id(&self) -> &'static str {
93        "borderless"
94    }
95
96    fn on_event(&mut self, event: &mut PluginEvent, _handle: PluginHandle) {
97        if self.corner_radius <= 0. {
98            return;
99        }
100        match event {
101            PluginEvent::BeforeRender { canvas, window, .. } => {
102                canvas.clear(Color::TRANSPARENT);
103                canvas.save();
104                let size = window.inner_size();
105                let radius = self.corner_radius * window.scale_factor() as f32;
106                let rounded_window = RRect::new_rect_xy(
107                    SkRect::from_wh(size.width as f32, size.height as f32),
108                    radius,
109                    radius,
110                );
111                canvas.clip_rrect(rounded_window, ClipOp::Intersect, true);
112            }
113            PluginEvent::AfterRender { canvas, .. } => {
114                canvas.restore();
115            }
116            _ => {}
117        }
118    }
119
120    fn root_component(&self, root: Element) -> Element {
121        if cfg!(target_os = "macos") {
122            return root;
123        }
124        BorderlessRoot {
125            thickness: self.thickness,
126            inner: root,
127        }
128        .into_element()
129    }
130}
131
132#[derive(Clone, PartialEq)]
133struct BorderlessRoot {
134    thickness: f32,
135    inner: Element,
136}
137
138impl Component for BorderlessRoot {
139    fn render(&self) -> impl IntoElement {
140        rect()
141            .expanded()
142            .child(self.inner.clone())
143            .child(ResizeBands::new(self.thickness))
144    }
145}
146
147/// Whether the window is fullscreen or maximized.
148pub fn use_maximized() -> State<bool> {
149    let mut maximized = use_state(|| false);
150
151    use_side_effect(move || {
152        let _ = Platform::get().root_size.read();
153        Platform::get().with_window(None, move |window| {
154            if let Some(mut maximized) = maximized.try_write() {
155                *maximized = window.fullscreen().is_some() || window.is_maximized();
156            }
157        });
158    });
159
160    maximized
161}
162
163/// Invisible bands along the window borders that drive a native resize.
164#[derive(PartialEq)]
165pub struct ResizeBands {
166    thickness: f32,
167    key: DiffKey,
168}
169
170impl KeyExt for ResizeBands {
171    fn write_key(&mut self) -> &mut DiffKey {
172        &mut self.key
173    }
174}
175
176impl ResizeBands {
177    /// Create the bands, with `thickness` as how far into the window each one reaches.
178    pub fn new(thickness: f32) -> Self {
179        Self {
180            thickness,
181            key: DiffKey::None,
182        }
183    }
184}
185
186impl Component for ResizeBands {
187    fn render(&self) -> impl IntoElement {
188        let maximized = use_maximized();
189        let size = *Platform::get().root_size.read();
190        let thickness = self.thickness;
191
192        rect()
193            .layer(Layer::Overlay)
194            .width(Size::px(0.))
195            .height(Size::px(0.))
196            .maybe(!maximized(), |el| {
197                el.children(
198                    DIRECTIONS
199                        .iter()
200                        .map(|direction| band(*direction, size, thickness)),
201                )
202            })
203    }
204}
205
206fn band(direction: ResizeDirection, size: Size2D, thickness: f32) -> Element {
207    let corner = thickness * 2.;
208    let span_x = (size.width - corner).max(0.);
209    let span_y = (size.height - corner).max(0.);
210    let far_x = (size.width - thickness).max(0.);
211    let far_y = (size.height - thickness).max(0.);
212
213    let (left, top, width, height) = match direction {
214        ResizeDirection::North => (thickness, 0., span_x, thickness),
215        ResizeDirection::South => (thickness, far_y, span_x, thickness),
216        ResizeDirection::West => (0., thickness, thickness, span_y),
217        ResizeDirection::East => (far_x, thickness, thickness, span_y),
218        ResizeDirection::NorthWest => (0., 0., corner, corner),
219        ResizeDirection::NorthEast => (span_x, 0., corner, corner),
220        ResizeDirection::SouthWest => (0., span_y, corner, corner),
221        ResizeDirection::SouthEast => (span_x, span_y, corner, corner),
222    };
223    let area = Area::new(Point2D::new(left, top), Size2D::new(width, height));
224
225    rect()
226        .position(
227            Position::new_global()
228                .top(area.origin.y)
229                .left(area.origin.x),
230        )
231        .width(Size::px(area.width()))
232        .height(Size::px(area.height()))
233        .cursor(cursor(direction))
234        .on_pointer_down(move |_| {
235            Platform::get().with_window(None, move |window| {
236                let _ = window.drag_resize_window(direction);
237            });
238        })
239        .into_element()
240}
241
242fn cursor(direction: ResizeDirection) -> CursorIcon {
243    match direction {
244        ResizeDirection::North | ResizeDirection::South => CursorIcon::NsResize,
245        ResizeDirection::West | ResizeDirection::East => CursorIcon::EwResize,
246        ResizeDirection::NorthWest | ResizeDirection::SouthEast => CursorIcon::NwseResize,
247        ResizeDirection::NorthEast | ResizeDirection::SouthWest => CursorIcon::NeswResize,
248    }
249}