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 rounds the app.
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                if window.fullscreen().is_some() || window.is_maximized() {
105                    return;
106                }
107                let size = window.inner_size();
108                let radius = self.corner_radius * window.scale_factor() as f32;
109                let rounded_window = RRect::new_rect_xy(
110                    SkRect::from_wh(size.width as f32, size.height as f32),
111                    radius,
112                    radius,
113                );
114                canvas.clip_rrect(rounded_window, ClipOp::Intersect, true);
115            }
116            PluginEvent::AfterRender { canvas, .. } => {
117                canvas.restore();
118            }
119            _ => {}
120        }
121    }
122
123    fn root_component(&self, root: Element) -> Element {
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            .maybe(!cfg!(target_os = "macos"), |el| {
144                el.child(ResizeBands::new(self.thickness))
145            })
146    }
147}
148
149/// Whether the window is fullscreen or maximized.
150pub fn use_maximized() -> State<bool> {
151    let mut maximized = use_state(|| false);
152
153    use_side_effect(move || {
154        let _ = Platform::get().root_size.read();
155        Platform::get().with_window(Platform::window_id(), move |window| {
156            if let Some(mut maximized) = maximized.try_write() {
157                *maximized = window.fullscreen().is_some() || window.is_maximized();
158            }
159        });
160    });
161
162    maximized
163}
164
165/// Invisible bands along the window borders that drive a native resize.
166#[derive(PartialEq)]
167pub struct ResizeBands {
168    thickness: f32,
169    key: DiffKey,
170}
171
172impl KeyExt for ResizeBands {
173    fn write_key(&mut self) -> &mut DiffKey {
174        &mut self.key
175    }
176}
177
178impl ResizeBands {
179    /// Create the bands, with `thickness` as how far into the window each one reaches.
180    pub fn new(thickness: f32) -> Self {
181        Self {
182            thickness,
183            key: DiffKey::None,
184        }
185    }
186}
187
188impl Component for ResizeBands {
189    fn render(&self) -> impl IntoElement {
190        let maximized = use_maximized();
191        let size = *Platform::get().root_size.read();
192
193        rect()
194            .layer(Layer::Overlay)
195            .width(Size::px(0.))
196            .height(Size::px(0.))
197            .maybe(!maximized(), |el| {
198                el.children(
199                    DIRECTIONS
200                        .iter()
201                        .map(|direction| self.band(*direction, size)),
202                )
203            })
204    }
205}
206
207impl ResizeBands {
208    fn band(&self, direction: ResizeDirection, size: Size2D) -> Element {
209        let band = self.thickness;
210        let corner = band * 2.;
211        let span_x = (size.width - corner * 2.).max(0.);
212        let span_y = (size.height - corner * 2.).max(0.);
213        let far_x = (size.width - band).max(0.);
214        let far_y = (size.height - band).max(0.);
215        let corner_far_x = (size.width - corner).max(0.);
216        let corner_far_y = (size.height - corner).max(0.);
217
218        let (left, top, width, height) = match direction {
219            ResizeDirection::North => (corner, 0., span_x, band),
220            ResizeDirection::South => (corner, far_y, span_x, band),
221            ResizeDirection::West => (0., corner, band, span_y),
222            ResizeDirection::East => (far_x, corner, band, span_y),
223            ResizeDirection::NorthWest => (0., 0., corner, corner),
224            ResizeDirection::NorthEast => (corner_far_x, 0., corner, corner),
225            ResizeDirection::SouthWest => (0., corner_far_y, corner, corner),
226            ResizeDirection::SouthEast => (corner_far_x, corner_far_y, corner, corner),
227        };
228        let area = Area::new(Point2D::new(left, top), Size2D::new(width, height));
229
230        rect()
231            .position(
232                Position::new_global()
233                    .top(area.origin.y)
234                    .left(area.origin.x),
235            )
236            .width(Size::px(area.width()))
237            .height(Size::px(area.height()))
238            .cursor(cursor(direction))
239            .on_pointer_down(move |_| {
240                Platform::get().with_window(Platform::window_id(), move |window| {
241                    let _ = window.drag_resize_window(direction);
242                });
243            })
244            .into_element()
245    }
246}
247
248fn cursor(direction: ResizeDirection) -> CursorIcon {
249    match direction {
250        ResizeDirection::North | ResizeDirection::South => CursorIcon::NsResize,
251        ResizeDirection::West | ResizeDirection::East => CursorIcon::EwResize,
252        ResizeDirection::NorthWest | ResizeDirection::SouthEast => CursorIcon::NwseResize,
253        ResizeDirection::NorthEast | ResizeDirection::SouthWest => CursorIcon::NeswResize,
254    }
255}