Skip to main content

floem/views/
resizable.rs

1use std::{any::Any, cell::RefCell, rc::Rc, time::Duration};
2
3use crate::{
4    BoxTree, ElementId, ViewId,
5    context::{EventCx, PaintCx, UpdateCx},
6    easing::Linear,
7    event::{
8        DragEvent, DragSourceEvent, Event, EventPropagation, InteractionEvent, Phase,
9        listener::UpdatePhaseLayout,
10    },
11    prelude::*,
12    prop, prop_extractor,
13    style::{
14        ContextValue, CursorStyle, CustomStylable, CustomStyle, ExprStyle, FlexDirectionProp,
15        Style, StyleClass,
16        recalc::{StyleReason, StyleReasonFlags},
17    },
18    style_class,
19    unit::{Pct, Pt},
20};
21use floem_reactive::Effect;
22use peniko::{
23    Brush,
24    color::palette::css,
25    kurbo::{Axis, Rect},
26};
27use rustc_hash::FxHashMap;
28use taffy::{FlexDirection, Overflow};
29use ui_events::pointer::PointerEvent;
30use understory_box_tree::NodeFlags;
31
32style_class!(
33    /// The style class that is applied to all [`ResizableStack`] views.
34    pub ResizableClass
35);
36style_class!(
37    /// The style class that is applied to all ResizableHandles.
38    pub ResizableHandleClass
39);
40
41pub(crate) fn create_resizable(children: Vec<Box<dyn View>>) -> Resizable {
42    let id = ViewId::new();
43    id.register_listener(listener::UpdatePhaseLayout::listener_key());
44
45    let mut view_children = Vec::new();
46    let mut child_ids = Vec::new();
47
48    let mut children_iter = children.into_iter().peekable();
49
50    while let Some(c) = children_iter.next() {
51        let child_id = ViewId::new();
52        child_id.add_child(c);
53        let resize_child = ResizeChild {
54            id: child_id,
55            set_basis_percent: None,
56            is_last: children_iter.peek().is_none(),
57        }
58        .into_any();
59        child_ids.push(child_id);
60        view_children.push(resize_child);
61    }
62
63    id.set_children_vec(view_children);
64
65    let mut handles = FxHashMap::default();
66    for i in 0..child_ids.len() - 1 {
67        let child_id = child_ids[i];
68        let next_child_id = child_ids[i + 1];
69        let handle = Handle::new(id, child_id, next_child_id);
70        handles.insert(handle.element_id, handle);
71    }
72
73    Resizable {
74        id,
75        re_style: ReStyle::default(),
76        handles,
77    }
78}
79
80/// Creates a [ResizableStack] from a group of `Views`.
81#[deprecated(note = "use ResizableStack::new")]
82pub fn resizable<VT: ViewTuple + 'static>(children: VT) -> Resizable {
83    create_resizable(children.into_views())
84}
85
86prop!(
87    /// The color of the handle
88    pub HandleColor: Brush {} = Brush::Solid(css::TRANSPARENT)
89);
90prop!(
91    /// The width of the handle
92    pub HandleThickness: Pt {} = Pt(6.)
93);
94prop!(
95    /// The width of the handle that is used for hit testing.
96    pub HandleHitTestThickness: Pt {} = Pt(10.)
97);
98prop!(
99    /// The cursor style over the handle.
100    /// Defaults to automatically handling the style for you.
101    pub HandleCursorStyle: Option<CursorStyle> {} = None
102);
103
104prop_extractor! {
105    ReStyle {
106        direction: FlexDirectionProp,
107    }
108}
109prop_extractor! {
110    HandleStyle {
111        color: HandleColor,
112        thickness: HandleThickness,
113        hit_test_thickness: HandleHitTestThickness,
114        cursor: HandleCursorStyle,
115    }
116}
117
118pub enum ResizeChildMessage {
119    SetBasisPercent(Pct),
120    ClearBasis,
121}
122
123pub struct ResizeChild {
124    id: ViewId,
125    set_basis_percent: Option<Pct>,
126    is_last: bool,
127}
128impl View for ResizeChild {
129    fn id(&self) -> ViewId {
130        self.id
131    }
132
133    fn view_style(&self) -> Option<Style> {
134        Some(
135            Style::new()
136                .apply_opt(self.set_basis_percent, |s, percent| s.flex_basis(percent))
137                .apply_if(self.is_last, |s| s.flex_grow(1.))
138                .min_size(0., 0.)
139                .overflow_x(Overflow::Clip)
140                .overflow_y(Overflow::Clip),
141        )
142    }
143
144    fn update(&mut self, _cx: &mut UpdateCx, state: Box<dyn Any>) {
145        if let Ok(msg) = state.downcast::<ResizeChildMessage>() {
146            self.id.request_style(StyleReason::view_style());
147            self.id.request_layout();
148            match *msg {
149                ResizeChildMessage::SetBasisPercent(percent) => {
150                    self.set_basis_percent = Some(percent)
151                }
152                ResizeChildMessage::ClearBasis => self.set_basis_percent = None,
153            }
154        }
155    }
156}
157
158#[derive(Debug, Clone)]
159struct Handle {
160    /// Access to relevent view ids for message passing
161    parent_id: ViewId,
162    affected_child_id: ViewId,
163    next_child_id: ViewId,
164    element_id: ElementId,
165    box_tree: Rc<RefCell<BoxTree>>,
166    handle_style: HandleStyle,
167}
168impl Handle {
169    fn new(parent_id: ViewId, affected_child_id: ViewId, next_child_id: ViewId) -> Self {
170        let box_tree = parent_id.box_tree();
171        let element_id = parent_id.create_child_element_id(1);
172
173        Self {
174            parent_id,
175            affected_child_id,
176            next_child_id,
177            element_id,
178            box_tree,
179            handle_style: Default::default(),
180        }
181    }
182
183    fn set_position(&mut self, axis: Axis) {
184        let parent_content = self.parent_id.get_content_rect_local();
185        let affected_rect = self.affected_child_id.get_layout_rect();
186        let next_rect = self.next_child_id.get_layout_rect();
187        let hit_test_thickness = self.handle_style.hit_test_thickness().0;
188
189        let new_rect = match axis {
190            Axis::Horizontal => {
191                // Center handle in the gap between children
192                let center_x = (affected_rect.x1 + next_rect.x0) / 2.0;
193                let half_width = hit_test_thickness / 2.0;
194                Rect::new(
195                    center_x - half_width,
196                    parent_content.y0,
197                    center_x + half_width,
198                    parent_content.y1,
199                )
200            }
201            Axis::Vertical => {
202                // Center handle in the gap between children
203                let center_y = (affected_rect.y1 + next_rect.y0) / 2.0;
204                let half_height = hit_test_thickness / 2.0;
205                Rect::new(
206                    parent_content.x0,
207                    center_y - half_height,
208                    parent_content.x1,
209                    center_y + half_height,
210                )
211            }
212        };
213
214        self.box_tree
215            .borrow_mut()
216            .set_local_bounds(self.element_id.0, new_rect);
217        self.box_tree
218            .borrow_mut()
219            .set_flags(self.element_id.0, NodeFlags::VISIBLE | NodeFlags::PICKABLE);
220    }
221
222    fn event(&mut self, cx: &mut EventCx, axis: Axis) {
223        match &cx.event {
224            Event::Interaction(InteractionEvent::DoubleClick) => {
225                // Reset to equal sizes
226                self.affected_child_id
227                    .update_state(ResizeChildMessage::ClearBasis);
228                self.next_child_id
229                    .update_state(ResizeChildMessage::ClearBasis);
230            }
231            Event::Pointer(PointerEvent::Down(e)) => {
232                if let Some(pointer_id) = e.pointer.pointer_id {
233                    cx.window_state
234                        .set_pointer_capture(pointer_id, self.element_id);
235                }
236            }
237            Event::PointerCapture(crate::event::PointerCaptureEvent::Gained(drag)) => {
238                cx.start_drag(
239                    *drag,
240                    crate::event::DragConfig {
241                        threshold: 1.,
242                        animation_duration: Duration::ZERO,
243                        easing: Rc::new(Linear),
244                        custom_data: None,
245                        track_targets: false,
246                    },
247                    false,
248                );
249            }
250            Event::Pointer(PointerEvent::Leave(_)) => {
251                cx.window_state.clear_cursor(self.element_id);
252            }
253            Event::Pointer(PointerEvent::Move(_)) => {
254                let cursor = match axis {
255                    Axis::Horizontal => CursorStyle::ColResize,
256                    Axis::Vertical => CursorStyle::RowResize,
257                };
258                let cursor = self.handle_style.cursor().unwrap_or(cursor);
259                cx.window_state.set_cursor(self.element_id, cursor);
260            }
261            Event::Drag(DragEvent::Source(DragSourceEvent::Move(dme))) => {
262                let point = dme.current_state.logical_point();
263                let affected_rect = self.affected_child_id.get_layout_rect();
264                let next_rect = self.next_child_id.get_layout_rect();
265
266                // Calculate the gap between children
267                let (_, affected_x1) = affected_rect.get_coords(axis);
268                let (next_x0, _) = next_rect.get_coords(axis);
269                let gap_size = next_x0 - affected_x1;
270
271                // Use the CURRENT rendered sizes of just these two children
272                let pair_total =
273                    affected_rect.size().get_coord(axis) + next_rect.size().get_coord(axis);
274
275                if pair_total <= 0.0 {
276                    return;
277                }
278
279                // The mouse position relative to where the affected child starts
280                let mouse_offset = point.get_coord(axis) - affected_rect.origin().get_coord(axis);
281
282                // Subtract half the gap since the handle is centered in it
283                let affected_size = mouse_offset - (gap_size / 2.0);
284
285                // What fraction of the pair does the affected child want?
286                let affected_fraction = affected_size / pair_total;
287
288                // Apply min/max as fractions
289                let min_fraction = 0.1; // 10%
290                let max_fraction = 0.9; // 90%
291                let clamped_fraction = affected_fraction.clamp(min_fraction, max_fraction);
292
293                // Calculate the new sizes
294                let new_affected_size = clamped_fraction * pair_total;
295                let new_next_size = (1.0 - clamped_fraction) * pair_total;
296
297                // Convert these pixel sizes to percentages of parent
298                let parent_content = self.parent_id.get_content_rect_local();
299                let parent_size = parent_content.size().get_coord(axis);
300
301                if parent_size > 0.0 {
302                    let affected_percent = (new_affected_size / parent_size) * 100.0;
303                    let next_percent = (new_next_size / parent_size) * 100.0;
304
305                    self.affected_child_id
306                        .update_state(ResizeChildMessage::SetBasisPercent(Pct(affected_percent)));
307                    self.next_child_id
308                        .update_state(ResizeChildMessage::SetBasisPercent(Pct(next_percent)));
309                }
310            }
311
312            _ => {}
313        }
314    }
315
316    fn style(&mut self, cx: &mut crate::context::StyleCx<'_>, axis: Axis) {
317        let resolved = cx.resolve_nested_maps(
318            Style::new(),
319            &[ResizableHandleClass::class_ref()],
320            self.element_id,
321        );
322        if self
323            .handle_style
324            .read_style_for(cx, &resolved, self.element_id)
325        {
326            let cursor = match axis {
327                Axis::Horizontal => CursorStyle::ColResize,
328                Axis::Vertical => CursorStyle::RowResize,
329            };
330            let cursor = self.handle_style.cursor().unwrap_or(cursor);
331            cx.window_state.set_cursor(self.element_id, cursor);
332            cx.window_state.request_paint(self.element_id);
333        }
334    }
335
336    fn paint(&self, cx: &mut PaintCx<'_>, axis: Axis) {
337        let box_tree = self.box_tree.borrow();
338        let rect = box_tree.local_bounds(self.element_id.0).unwrap_or_default();
339        let thickness = self.handle_style.thickness().0;
340
341        // Center the actual thickness within the hit-testable rect
342        let paint_rect = match axis {
343            Axis::Horizontal => {
344                let center_x = (rect.x0 + rect.x1) / 2.0;
345                let half_thickness = thickness / 2.0;
346                Rect::new(
347                    center_x - half_thickness,
348                    rect.y0,
349                    center_x + half_thickness,
350                    rect.y1,
351                )
352            }
353            Axis::Vertical => {
354                let center_y = (rect.y0 + rect.y1) / 2.0;
355                let half_thickness = thickness / 2.0;
356                Rect::new(
357                    rect.x0,
358                    center_y - half_thickness,
359                    rect.x1,
360                    center_y + half_thickness,
361                )
362            }
363        };
364
365        cx.fill(&paint_rect, &self.handle_style.color(), 0.);
366    }
367}
368
369/// A container View around other Views that allows for resizing with a handle.
370pub struct Resizable {
371    id: ViewId,
372    re_style: ReStyle,
373    handles: FxHashMap<ElementId, Handle>,
374}
375
376impl View for Resizable {
377    fn id(&self) -> ViewId {
378        self.id
379    }
380
381    fn view_class(&self) -> Option<crate::style::StyleClassRef> {
382        Some(ResizableClass::class_ref())
383    }
384
385    fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
386        if cx.reason.flags != StyleReasonFlags::TARGET {
387            self.re_style.read(cx);
388        }
389
390        // If the reason implies nested style maps must be resolved, restyle everything.
391        if cx.reason.needs_resolve_nested_maps() {
392            for handle in self.handles.values_mut() {
393                handle.style(cx, self.re_style.direction().axis());
394            }
395            return;
396        }
397
398        for (element_id, _reason) in cx.targeted_elements.clone() {
399            if let Some(handle) = self.handles.get_mut(&element_id) {
400                handle.style(cx, self.re_style.direction().axis());
401            }
402        }
403    }
404
405    fn update(&mut self, _cx: &mut UpdateCx, state: Box<dyn std::any::Any>) {
406        if let Ok(state) = state.downcast::<ResizableMessage>() {
407            match *state {
408                ResizableMessage::SetSizesPercent(sizes) => {
409                    // self.id.request_style();
410                    self.id.request_layout();
411
412                    for (idx, percent) in sizes {
413                        let child = self.id.with_children(|children| children.get(idx).copied());
414                        if let Some(child) = child {
415                            child.update_state(ResizeChildMessage::SetBasisPercent(percent));
416                        }
417                    }
418                }
419                ResizableMessage::SetSizesPixels(sizes) => {
420                    // self.id.request_style();
421                    self.id.request_layout();
422
423                    let axis = self.re_style.direction().axis();
424
425                    for (idx, pixel_size) in sizes {
426                        // Convert pixels to percentages for this child and the next
427                        let (affected_percent, next_percent) =
428                            self.pixels_to_percent_for_pair(idx, pixel_size, axis);
429
430                        let (affected, next) = self.id.with_children(|children| {
431                            (children.get(idx).copied(), children.get(idx + 1).copied())
432                        });
433                        if let Some(child) = affected {
434                            child.update_state(ResizeChildMessage::SetBasisPercent(Pct(
435                                affected_percent,
436                            )));
437                        }
438                        if let Some(next_child) = next {
439                            next_child.update_state(ResizeChildMessage::SetBasisPercent(Pct(
440                                next_percent,
441                            )));
442                        }
443                    }
444                }
445                ResizableMessage::ClearSize(idx) => {
446                    let child = self.id.with_children(|c| c.get(idx).copied());
447                    if let Some(child) = child {
448                        child.update_state(ResizeChildMessage::ClearBasis);
449                    }
450                }
451                ResizableMessage::ClearAll => {
452                    for child in self.id.children() {
453                        child.update_state(ResizeChildMessage::ClearBasis);
454                    }
455                }
456            }
457        }
458    }
459
460    fn event(&mut self, cx: &mut EventCx) -> EventPropagation {
461        // for this to work we had to set `id.has_layout_listener`.
462        if UpdatePhaseLayout::extract(&cx.event).is_some() {
463            self.post_layout();
464        }
465        if cx.phase == Phase::Target
466            && let Some(handle) = self.handles.get_mut(&cx.target)
467        {
468            handle.event(cx, self.re_style.direction().axis());
469            return EventPropagation::Stop;
470        }
471
472        EventPropagation::Continue
473    }
474
475    fn paint(&mut self, cx: &mut PaintCx) {
476        // Children are now painted automatically by traversal system
477        if let Some(handle) = self.handles.get(&cx.target_id) {
478            handle.paint(cx, self.re_style.direction().axis())
479        }
480    }
481}
482
483pub enum ResizableMessage {
484    SetSizesPercent(Vec<(usize, Pct)>), // (index, percentage)
485    SetSizesPixels(Vec<(usize, f64)>),
486    ClearSize(usize),
487    ClearAll,
488}
489
490impl Resizable {
491    pub fn new<VT: ViewTuple + 'static>(children: VT) -> Self {
492        create_resizable(children.into_views())
493    }
494
495    /// Convert pixel sizes to percentages for adjacent children
496    fn pixels_to_percent_for_pair(&self, _idx: usize, pixel_size: f64, axis: Axis) -> (f64, f64) {
497        // Get parent size instead of pair size
498        let parent_content = self.id.get_content_rect_local();
499        let parent_size = parent_content.size().get_coord(axis);
500
501        if parent_size > 0.0 {
502            let affected_percent = (pixel_size / parent_size) * 100.0;
503            let next_percent = 100.0 - affected_percent;
504            return (affected_percent, next_percent);
505        }
506
507        (50.0, 50.0) // Default to equal split
508    }
509
510    pub fn custom_sizes(self, sizes: impl Fn() -> Vec<(usize, f64)> + 'static) -> Self {
511        let id = self.id;
512        Effect::new(move |_| {
513            let sizes = sizes();
514            id.update_state(ResizableMessage::SetSizesPixels(sizes));
515        });
516        self
517    }
518
519    pub fn custom_sizes_pct(self, sizes: impl Fn() -> Vec<(usize, Pct)> + 'static) -> Self {
520        let id = self.id;
521        Effect::new(move |_| {
522            let sizes = sizes();
523            id.update_state(ResizableMessage::SetSizesPercent(sizes));
524        });
525        self
526    }
527
528    /// Sets the custom style properties of the `ResizableStack`.
529    pub fn resizable_style(
530        self,
531        style: impl Fn(ResizableCustomStyle) -> ResizableCustomStyle + 'static,
532    ) -> Self {
533        self.custom_style(style)
534    }
535
536    fn post_layout(&mut self) {
537        for handle in self.handles.values_mut() {
538            handle.set_position(self.re_style.direction().axis());
539        }
540    }
541}
542
543#[derive(Debug, Default, Clone)]
544pub struct ResizableCustomStyle(Style);
545impl From<ResizableCustomStyle> for Style {
546    fn from(val: ResizableCustomStyle) -> Self {
547        val.0
548    }
549}
550impl From<Style> for ResizableCustomStyle {
551    fn from(val: Style) -> Self {
552        Self(val)
553    }
554}
555impl CustomStyle for ResizableCustomStyle {
556    type StyleClass = ResizableHandleClass;
557}
558
559impl CustomStylable<ResizableCustomStyle> for Resizable {
560    type DV = Self;
561}
562
563impl ResizableCustomStyle {
564    pub fn new() -> Self {
565        Self::default()
566    }
567
568    /// Sets the color of the handle handle.
569    ///
570    /// # Arguments
571    /// * `color` - A `Brush` that sets the handle's color.
572    pub fn handle_color(mut self, color: impl Into<Brush>) -> Self {
573        self = ResizableCustomStyle(self.0.set(HandleColor, color));
574        self
575    }
576
577    /// Sets the thickness of the handle.
578    ///
579    /// # Arguments
580    /// * `Thickness` - A `Px` value that sets the handle's thickness.
581    pub fn handle_thickness(mut self, width: impl Into<Pt>) -> Self {
582        self = ResizableCustomStyle(self.0.set(HandleThickness, width));
583        self
584    }
585
586    /// Sets the cursor style over the handle.
587    ///
588    /// # Arguments
589    /// * `cursor_style` - An optional `CursorStyle` that sets the handle's cursor style.
590    ///   If `None` is provided, default automatic cursor style is used.
591    pub fn handle_cursor_style(mut self, cursor_style: impl Into<Option<CursorStyle>>) -> Self {
592        self = ResizableCustomStyle(self.0.set(HandleCursorStyle, cursor_style));
593        self
594    }
595}
596
597#[derive(Debug, Default, Clone)]
598pub struct ResizableCustomExprStyle(Style);
599impl From<ResizableCustomExprStyle> for Style {
600    fn from(val: ResizableCustomExprStyle) -> Self {
601        val.0
602    }
603}
604impl From<Style> for ResizableCustomExprStyle {
605    fn from(val: Style) -> Self {
606        Self(val)
607    }
608}
609impl ResizableCustomExprStyle {
610    pub fn new() -> Self {
611        Self::default()
612    }
613
614    pub fn style(self, style: impl FnOnce(ExprStyle) -> ExprStyle) -> Self {
615        let new: Style = style(self.0.into()).into();
616        new.into()
617    }
618
619    pub fn hover(self, style: impl FnOnce(Self) -> Self) -> Self {
620        let new = self.0.hover(|_| style(Self::default()).into());
621        new.into()
622    }
623
624    pub fn handle_color<T>(mut self, color: ContextValue<T>) -> Self
625    where
626        T: Into<Brush> + 'static,
627    {
628        self = ResizableCustomExprStyle(
629            ExprStyle::from(self.0)
630                .set_context(HandleColor, color.map(Into::into))
631                .into(),
632        );
633        self
634    }
635
636    pub fn handle_thickness<T>(mut self, width: ContextValue<T>) -> Self
637    where
638        T: Into<Pt> + 'static,
639    {
640        self = ResizableCustomExprStyle(
641            ExprStyle::from(self.0)
642                .set_context(HandleThickness, width.map(Into::into))
643                .into(),
644        );
645        self
646    }
647
648    pub fn handle_cursor_style<T>(mut self, cursor_style: ContextValue<T>) -> Self
649    where
650        T: Into<Option<CursorStyle>> + 'static,
651    {
652        self = ResizableCustomExprStyle(
653            ExprStyle::from(self.0)
654                .set_context_opt(HandleCursorStyle, cursor_style.map(Into::into))
655                .into(),
656        );
657        self
658    }
659}
660
661// trait HitExt {
662//     fn hit(&self, point: Point, threshhold: f64) -> bool;
663// }
664
665// impl<T> HitExt for T
666// where
667//     T: kurbo::ParamCurveNearest,
668// {
669//     fn hit(&self, point: Point, threshhold: f64) -> bool {
670//         const ACCURACY: f64 = 0.1;
671//         let nearest = self.nearest(point, ACCURACY);
672//         nearest.distance_sq < threshhold * threshhold
673//     }
674// }
675
676trait AxisExt {
677    fn axis(&self) -> Axis;
678}
679impl AxisExt for FlexDirection {
680    fn axis(&self) -> Axis {
681        match self {
682            Self::Row | Self::RowReverse => Axis::Horizontal,
683            Self::Column | Self::ColumnReverse => Axis::Vertical,
684        }
685    }
686}