Skip to main content

freya_borderless_plugin/
lib.rs

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