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 pub ResizableClass
35);
36style_class!(
37 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#[deprecated(note = "use ResizableStack::new")]
82pub fn resizable<VT: ViewTuple + 'static>(children: VT) -> Resizable {
83 create_resizable(children.into_views())
84}
85
86prop!(
87 pub HandleColor: Brush {} = Brush::Solid(css::TRANSPARENT)
89);
90prop!(
91 pub HandleThickness: Pt {} = Pt(6.)
93);
94prop!(
95 pub HandleHitTestThickness: Pt {} = Pt(10.)
97);
98prop!(
99 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 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 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 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 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 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 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 let mouse_offset = point.get_coord(axis) - affected_rect.origin().get_coord(axis);
281
282 let affected_size = mouse_offset - (gap_size / 2.0);
284
285 let affected_fraction = affected_size / pair_total;
287
288 let min_fraction = 0.1; let max_fraction = 0.9; let clamped_fraction = affected_fraction.clamp(min_fraction, max_fraction);
292
293 let new_affected_size = clamped_fraction * pair_total;
295 let new_next_size = (1.0 - clamped_fraction) * pair_total;
296
297 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 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
369pub 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 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_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_layout();
422
423 let axis = self.re_style.direction().axis();
424
425 for (idx, pixel_size) in sizes {
426 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 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 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)>), 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 fn pixels_to_percent_for_pair(&self, _idx: usize, pixel_size: f64, axis: Axis) -> (f64, f64) {
497 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) }
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 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 pub fn handle_color(mut self, color: impl Into<Brush>) -> Self {
573 self = ResizableCustomStyle(self.0.set(HandleColor, color));
574 self
575 }
576
577 pub fn handle_thickness(mut self, width: impl Into<Pt>) -> Self {
582 self = ResizableCustomStyle(self.0.set(HandleThickness, width));
583 self
584 }
585
586 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
661trait 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}