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 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
147pub 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#[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 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}