1use freya_core::prelude::*;
2use thiserror::Error;
3use torin::{
4 content::Content,
5 prelude::{
6 Area,
7 Direction,
8 },
9 size::Size,
10};
11
12use crate::{
13 get_theme,
14 theming::component_themes::{
15 ResizableHandleTheme,
16 ResizableHandleThemePartial,
17 },
18};
19
20#[derive(Error, Debug)]
21pub enum ResizableError {
22 #[error("Panel does not exist")]
23 PanelNotFound,
24}
25
26#[derive(Clone, Copy, Debug)]
27pub struct Panel {
28 pub size: f32,
29 pub initial_size: f32,
30 pub min_size: f32,
31 pub id: usize,
32}
33
34#[derive(Default)]
35pub struct ResizableContext {
36 pub panels: Vec<Panel>,
37 pub direction: Direction,
38}
39
40impl ResizableContext {
41 pub fn direction(&self) -> Direction {
42 self.direction
43 }
44
45 pub fn panels(&mut self) -> &mut Vec<Panel> {
46 &mut self.panels
47 }
48
49 pub fn push_panel(&mut self, panel: Panel, order: Option<usize>) {
50 let mut buffer = panel.size;
51
52 for panel in &mut self.panels.iter_mut() {
53 let resized_sized = (panel.initial_size - panel.size).min(buffer);
54
55 if resized_sized >= 0. {
56 panel.size = (panel.size - resized_sized).max(panel.min_size);
57 let new_resized_sized = panel.initial_size - panel.size;
58 buffer -= new_resized_sized;
59 }
60 }
61
62 if let Some(order) = order {
63 if self.panels.len() <= order {
64 self.panels.push(panel);
65 } else {
66 self.panels.insert(order, panel);
67 }
68 } else {
69 self.panels.push(panel);
70 }
71 }
72
73 pub fn remove_panel(&mut self, id: usize) -> Result<(), ResizableError> {
74 let removed_panel = self
75 .panels
76 .iter()
77 .find(|p| p.id == id)
78 .cloned()
79 .ok_or(ResizableError::PanelNotFound)?;
80 self.panels.retain(|e| e.id != id);
81
82 let mut buffer = removed_panel.size;
83
84 for panel in &mut self.panels.iter_mut() {
85 let resized_sized = (panel.initial_size - panel.size).min(buffer);
86
87 panel.size = (panel.size + resized_sized).max(panel.min_size);
88 let new_resized_sized = panel.initial_size - panel.size;
89 buffer -= new_resized_sized;
90 }
91
92 Ok(())
93 }
94
95 pub fn apply_resize(&mut self, panel_index: usize, distance: f32) -> bool {
96 let mut changed_panels = false;
97
98 let (corrected_distance, behind_range, forward_range) = if distance >= 0. {
99 (distance, 0..panel_index, panel_index..self.panels.len())
100 } else {
101 (-distance, panel_index..self.panels.len(), 0..panel_index)
102 };
103
104 let mut acc_per = 0.0;
105
106 for panel in &mut self.panels[forward_range].iter_mut() {
108 let old_size = panel.size;
109 let new_size = (panel.size - corrected_distance).clamp(panel.min_size, 100.);
110
111 if panel.size != new_size {
112 changed_panels = true
113 }
114
115 panel.size = new_size;
116 acc_per -= new_size - old_size;
117
118 if old_size > panel.min_size {
119 break;
120 }
121 }
122
123 if let Some(panel) = &mut self.panels[behind_range].iter_mut().next_back() {
125 let new_size = (panel.size + acc_per).clamp(panel.min_size, 100.);
126
127 if panel.size != new_size {
128 changed_panels = true
129 }
130
131 panel.size = new_size;
132 }
133
134 changed_panels
135 }
136
137 pub fn reset(&mut self) {
138 for panel in &mut self.panels {
139 panel.size = panel.initial_size;
140 }
141 }
142}
143
144#[cfg_attr(feature = "docs",
168 doc = embed_doc_image::embed_image!("resizable_container", "images/gallery_resizable_container.png"),
169)]
170#[derive(PartialEq, Clone)]
171pub struct ResizableContainer {
172 direction: Direction,
173 panels: Vec<ResizablePanel>,
174 controller: Option<Writable<ResizableContext>>,
175}
176
177impl Default for ResizableContainer {
178 fn default() -> Self {
179 Self::new()
180 }
181}
182
183impl ResizableContainer {
184 pub fn new() -> Self {
185 Self {
186 direction: Direction::Vertical,
187 panels: vec![],
188 controller: None,
189 }
190 }
191
192 pub fn direction(mut self, direction: Direction) -> Self {
193 self.direction = direction;
194 self
195 }
196
197 pub fn panel(mut self, panel: impl Into<Option<ResizablePanel>>) -> Self {
198 if let Some(panel) = panel.into() {
199 self.panels.push(panel);
200 }
201 self
202 }
203
204 pub fn panels_iter(mut self, panels: impl Iterator<Item = ResizablePanel>) -> Self {
205 self.panels.extend(panels);
206 self
207 }
208
209 pub fn controller(mut self, controller: impl Into<Writable<ResizableContext>>) -> Self {
210 self.controller = Some(controller.into());
211 self
212 }
213}
214
215impl Component for ResizableContainer {
216 fn render(&self) -> impl IntoElement {
217 let mut size = use_state(Area::default);
218 use_provide_context(|| size);
219
220 let direction = use_reactive(&self.direction);
221 use_provide_context(|| {
222 self.controller.clone().unwrap_or_else(|| {
223 let mut state = State::create(ResizableContext {
224 direction: self.direction,
225 ..Default::default()
226 });
227
228 Effect::create_sync_with_gen(move |current_gen| {
229 let direction = direction();
230 if current_gen > 0 {
231 state.write().direction = direction;
232 }
233 });
234
235 state.into_writable()
236 })
237 });
238
239 rect()
240 .direction(self.direction)
241 .on_sized(move |e: Event<SizedEventData>| size.set(e.area))
242 .expanded()
243 .content(Content::flex())
244 .children(self.panels.iter().enumerate().flat_map(|(i, e)| {
245 if i > 0 {
246 vec![ResizableHandle::new(i).into(), e.clone().into()]
247 } else {
248 vec![e.clone().into()]
249 }
250 }))
251 }
252}
253
254#[derive(PartialEq, Clone)]
255pub struct ResizablePanel {
256 key: DiffKey,
257 initial_size: f32,
258 min_size: Option<f32>,
259 children: Vec<Element>,
260 order: Option<usize>,
261}
262
263impl KeyExt for ResizablePanel {
264 fn write_key(&mut self) -> &mut DiffKey {
265 &mut self.key
266 }
267}
268
269impl ChildrenExt for ResizablePanel {
270 fn get_children(&mut self) -> &mut Vec<Element> {
271 &mut self.children
272 }
273}
274
275impl ResizablePanel {
276 pub fn new(initial_size: f32) -> Self {
277 Self {
278 key: DiffKey::None,
279 initial_size,
280 min_size: None,
281 children: vec![],
282 order: None,
283 }
284 }
285
286 pub fn key(mut self, key: impl Into<DiffKey>) -> Self {
287 self.key = key.into();
288 self
289 }
290
291 pub fn initial_size(mut self, initial_size: impl Into<f32>) -> Self {
292 self.initial_size = initial_size.into();
293 self
294 }
295
296 pub fn min_size(mut self, min_size: impl Into<f32>) -> Self {
297 self.min_size = Some(min_size.into());
298 self
299 }
300
301 pub fn order(mut self, order: impl Into<usize>) -> Self {
302 self.order = Some(order.into());
303 self
304 }
305}
306
307impl Component for ResizablePanel {
308 fn render(&self) -> impl IntoElement {
309 let registry = use_consume::<Writable<ResizableContext>>();
310
311 let id = use_hook({
312 let mut registry = registry.clone();
313 move || {
314 let id = UseId::<ResizableContext>::get_in_hook();
315 let panel = Panel {
316 initial_size: self.initial_size,
317 size: self.initial_size,
318 min_size: self.min_size.unwrap_or(self.initial_size * 0.25),
319 id,
320 };
321 registry.write().push_panel(panel, self.order);
322 id
323 }
324 });
325
326 use_drop({
327 let mut registry = registry.clone();
328 move || {
329 let _ = registry.write().remove_panel(id);
330 }
331 });
332
333 let registry = registry.read();
334 let index = registry
335 .panels
336 .iter()
337 .position(|e| e.id == id)
338 .unwrap_or_default();
339
340 let Panel { size, .. } = registry.panels[index];
341
342 let (width, height) = match registry.direction {
343 Direction::Horizontal => (Size::flex(size), Size::fill()),
344 Direction::Vertical => (Size::fill(), Size::flex(size)),
345 };
346
347 rect()
348 .a11y_role(AccessibilityRole::Pane)
349 .width(width)
350 .height(height)
351 .overflow(Overflow::Clip)
352 .children(self.children.clone())
353 }
354
355 fn render_key(&self) -> DiffKey {
356 self.key.clone().or(DiffKey::None)
357 }
358}
359
360#[derive(Debug, Default, PartialEq, Clone, Copy)]
362pub enum HandleStatus {
363 #[default]
365 Idle,
366 Hovering,
368}
369
370#[derive(PartialEq)]
371pub struct ResizableHandle {
372 panel_index: usize,
373 pub(crate) theme: Option<ResizableHandleThemePartial>,
375}
376
377impl ResizableHandle {
378 pub fn new(panel_index: usize) -> Self {
379 Self {
380 panel_index,
381 theme: None,
382 }
383 }
384}
385
386impl Component for ResizableHandle {
387 fn render(&self) -> impl IntoElement {
388 let ResizableHandleTheme {
389 background,
390 hover_background,
391 corner_radius,
392 } = get_theme!(&self.theme, resizable_handle);
393 let mut size = use_state(Area::default);
394 let mut clicking = use_state(|| false);
395 let mut status = use_state(HandleStatus::default);
396 let registry = use_consume::<Writable<ResizableContext>>();
397 let container_size = use_consume::<State<Area>>();
398 let mut allow_resizing = use_state(|| false);
399
400 let panel_index = self.panel_index;
401
402 use_drop(move || {
403 if *status.peek() == HandleStatus::Hovering {
404 Cursor::set(CursorIcon::default());
405 }
406 });
407
408 let cursor = match registry.read().direction {
409 Direction::Horizontal => CursorIcon::ColResize,
410 _ => CursorIcon::RowResize,
411 };
412
413 let on_pointer_leave = move |_| {
414 *status.write() = HandleStatus::Idle;
415 if !clicking() {
416 Cursor::set(CursorIcon::default());
417 }
418 };
419
420 let on_pointer_enter = move |_| {
421 *status.write() = HandleStatus::Hovering;
422 Cursor::set(cursor);
423 };
424
425 let on_capture_global_pointer_move = {
426 let mut registry = registry.clone();
427 move |e: Event<PointerEventData>| {
428 if *clicking.read() {
429 e.prevent_default();
430
431 if !*allow_resizing.read() {
432 return;
433 }
434
435 let coordinates = e.global_location();
436 let mut registry = registry.write();
437
438 let total_size = registry.panels.iter().fold(0., |acc, p| acc + p.size);
439
440 let distance = match registry.direction {
441 Direction::Horizontal => {
442 let container_width = container_size.read().width();
443 let displacement = coordinates.x as f32 - size.read().min_x();
444 total_size / container_width * displacement
445 }
446 Direction::Vertical => {
447 let container_height = container_size.read().height();
448 let displacement = coordinates.y as f32 - size.read().min_y();
449 total_size / container_height * displacement
450 }
451 };
452
453 let changed_panels = registry.apply_resize(panel_index, distance);
454
455 if changed_panels {
456 allow_resizing.set(false);
457 }
458 }
459 }
460 };
461
462 let on_pointer_down = move |e: Event<PointerEventData>| {
463 e.stop_propagation();
464 e.prevent_default();
465 clicking.set(true);
466 };
467
468 let on_global_pointer_press = move |_: Event<PointerEventData>| {
469 if *clicking.read() {
470 if *status.peek() != HandleStatus::Hovering {
471 Cursor::set(CursorIcon::default());
472 }
473 clicking.set(false);
474 }
475 };
476
477 let (width, height) = match registry.read().direction {
478 Direction::Horizontal => (Size::px(4.), Size::fill()),
479 Direction::Vertical => (Size::fill(), Size::px(4.)),
480 };
481
482 let background = match *status.read() {
483 _ if *clicking.read() => hover_background,
484 HandleStatus::Hovering => hover_background,
485 HandleStatus::Idle => background,
486 };
487
488 rect()
489 .width(width)
490 .height(height)
491 .background(background)
492 .corner_radius(corner_radius)
493 .on_sized(move |e: Event<SizedEventData>| {
494 size.set(e.area);
495 allow_resizing.set(true);
496 })
497 .on_pointer_down(on_pointer_down)
498 .on_global_pointer_press(on_global_pointer_press)
499 .on_pointer_enter(on_pointer_enter)
500 .on_capture_global_pointer_move(on_capture_global_pointer_move)
501 .on_pointer_leave(on_pointer_leave)
502 }
503}