densa_widgets/
splitter.rs1use densa_core::Rect;
2use densa_draw::Canvas;
3use densa_ui::{
4 AccessNode, Cursor, Event, EventCx, Handled, Length, PaintCx, PointerButton, PointerKind, Role,
5 Style, Widget,
6};
7
8const HANDLE: f32 = 6.0;
10const MIN_PANE: f32 = 48.0;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15enum Split {
16 Row,
17 Column,
18}
19
20#[derive(Debug)]
26pub struct Splitter {
27 direction: Split,
28 grab: Option<f32>,
30}
31
32impl Splitter {
33 pub fn row() -> Self {
35 Self {
36 direction: Split::Row,
37 grab: None,
38 }
39 }
40
41 pub fn column() -> Self {
43 Self {
44 direction: Split::Column,
45 ..Self::row()
46 }
47 }
48
49 pub fn style(&self) -> Style {
52 let base = match self.direction {
53 Split::Row => Style::row(),
54 Split::Column => Style::column(),
55 };
56 base.gap(HANDLE)
57 .grow(1.0)
58 .min_size(Length::Px(0.0), Length::Px(0.0))
59 }
60
61 fn cursor(&self) -> Cursor {
62 match self.direction {
63 Split::Row => Cursor::ResizeColumn,
64 Split::Column => Cursor::ResizeRow,
65 }
66 }
67
68 fn span(&self, rect: Rect) -> (f32, f32) {
70 match self.direction {
71 Split::Row => (rect.left(), rect.right()),
72 Split::Column => (rect.top(), rect.bottom()),
73 }
74 }
75}
76
77impl Widget for Splitter {
78 fn accessibility(&self, _: &PaintCx<'_>, node: &mut AccessNode) {
79 node.set_role(Role::Splitter);
80 }
81
82 fn paint(&mut self, cx: &PaintCx<'_>, canvas: &mut Canvas<'_>) {
83 let Some(pane) = cx.children().first().and_then(|&first| cx.rect_of(first)) else {
84 return;
85 };
86 let content = cx.content_rect();
88 let line = cx.px(1.0).round().max(1.0);
89 let middle = (self.span(pane).1 + cx.px(HANDLE) / 2.0 - line / 2.0).round();
90 let divider = match self.direction {
91 Split::Row => Rect::new(middle, content.top(), line, content.height()),
92 Split::Column => Rect::new(content.left(), middle, content.width(), line),
93 };
94 let theme = cx.theme();
95 let color = if cx.is_pressed() || cx.is_hover_target() {
96 theme.accent
97 } else {
98 theme.border
99 };
100 canvas.fill_rect(divider, color);
101 }
102
103 fn event(&mut self, cx: &mut EventCx<'_>, event: &Event<'_>) -> Handled {
104 let Event::Pointer(pointer) = event else {
105 return Handled::No;
106 };
107 let Some(&first) = cx.children().first() else {
108 return Handled::No;
109 };
110 let Some(pane) = cx.rect_of(first) else {
111 return Handled::No;
112 };
113 let at = match self.direction {
114 Split::Row => pointer.position.x,
115 Split::Column => pointer.position.y,
116 };
117 match pointer.kind {
118 PointerKind::Enter => {
120 cx.set_cursor(Some(self.cursor()));
121 cx.request_paint();
122 Handled::Yes
123 }
124 PointerKind::Leave => {
125 cx.set_cursor(None);
126 cx.request_paint();
127 Handled::Yes
128 }
129 PointerKind::Down(PointerButton::Primary) => {
130 let position = pointer.position;
133 let on_pane = cx
134 .children()
135 .iter()
136 .any(|&child| cx.rect_of(child).is_some_and(|r| r.contains(position)));
137 if on_pane {
138 return Handled::No;
139 }
140 self.grab = Some(at - self.span(pane).1);
141 cx.request_paint();
142 Handled::Yes
143 }
144 PointerKind::Cancel => {
145 self.grab = None;
146 cx.request_paint();
147 Handled::Yes
148 }
149 PointerKind::Move => {
150 let Some(grab) = self.grab else {
151 return Handled::No;
152 };
153 let (start, end) = self.span(cx.content_rect());
154 let (pane_start, _) = self.span(pane);
155 let most = (end - start - cx.px(HANDLE) - cx.px(MIN_PANE)).max(cx.px(MIN_PANE));
156 let size = (at - grab - pane_start).clamp(cx.px(MIN_PANE), most);
157 let logical = Length::Px(size / cx.scale());
158 let style = cx.style_of(first).cloned().unwrap_or_default().shrink(0.0);
159 let style = match self.direction {
160 Split::Row => style.width(logical),
161 Split::Column => style.height(logical),
162 };
163 cx.set_style(first, style);
164 Handled::Yes
165 }
166 PointerKind::Up(PointerButton::Primary) => {
167 self.grab = None;
168 cx.request_paint();
169 Handled::Yes
170 }
171 _ => Handled::No,
172 }
173 }
174}