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
38pub 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 pub fn with_thickness(mut self, thickness: f32) -> Self {
80 self.thickness = thickness;
81 self
82 }
83
84 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
149pub 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#[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 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}