1use crate::event::Event;
4use crate::geometry::{Rect, Size};
5use crate::widget::{Axis as FlexAxis, EventCx, Flex, Length, MeasureCx, Node, NodeMut, PaintCx, View, Widget};
6
7use super::boundary::{self, Axis, Change};
8
9type Pane<'a, Msg> = Box<dyn FnOnce(&mut View<'_, Msg>) + 'a>;
10
11type SizeMessage<Msg> = Box<dyn Fn(u16) -> Msg>;
13
14pub struct Splitter<'a, Msg> {
25 axis: Axis,
26 size: u16,
27 min: u16,
28 max: Option<u16>,
29 on_resize: Option<SizeMessage<Msg>>,
30 first: Option<Pane<'a, Msg>>,
31 second: Option<Pane<'a, Msg>>,
32}
33
34impl<'a, Msg: 'static> Splitter<'a, Msg> {
35 fn new(axis: Axis, size: u16) -> Self {
36 Self { axis, size, min: 1, max: None, on_resize: None, first: None, second: None }
37 }
38
39 #[must_use]
41 pub fn columns(width: u16) -> Self {
42 Self::new(Axis::Columns, width)
43 }
44
45 #[must_use]
47 pub fn rows(height: u16) -> Self {
48 Self::new(Axis::Rows, height)
49 }
50
51 #[must_use]
55 pub fn limits(mut self, min: u16, max: impl Into<Option<u16>>) -> Self {
56 self.min = min;
57 self.max = max.into();
58 self
59 }
60
61 #[must_use]
64 pub fn on_resize(mut self, message: impl Fn(u16) -> Msg + 'static) -> Self {
65 self.on_resize = Some(Box::new(message));
66 self
67 }
68
69 #[must_use]
71 pub fn first(mut self, build: impl FnOnce(&mut View<'_, Msg>) + 'a) -> Self {
72 self.first = Some(Box::new(build));
73 self
74 }
75
76 #[must_use]
78 pub fn second(mut self, build: impl FnOnce(&mut View<'_, Msg>) + 'a) -> Self {
79 self.second = Some(Box::new(build));
80 self
81 }
82
83 pub fn show<'v>(self, ui: &'v mut View<'_, Msg>) -> NodeMut<'v, Msg> {
85 let build = |pane: Option<Pane<'a, Msg>>, place: usize| {
87 let mut children = Vec::new();
88 if let Some(pane) = pane {
89 pane(&mut ui.nested(&mut children));
90 }
91 let mut node = Node::new(Flex::new(FlexAxis::Column, children), place);
92 node.layout.width = Length::Fill(1);
93 node.layout.height = Length::Fill(1);
94 node
95 };
96 let split = Split {
97 axis: self.axis,
98 size: self.size,
99 min: self.min,
100 max: self.max,
101 on_resize: self.on_resize,
102 panes: vec![build(self.first, 0), build(self.second, 1)],
103 };
104 ui.add(split).fill()
105 }
106}
107
108struct Split<Msg> {
109 axis: Axis,
110 size: u16,
111 min: u16,
112 max: Option<u16>,
114 on_resize: Option<SizeMessage<Msg>>,
115 panes: Vec<Node<Msg>>,
116}
117
118impl<Msg: 'static> Split<Msg> {
119 fn extent(&self, area: Rect) -> u16 {
120 match self.axis {
121 Axis::Columns => area.width,
122 Axis::Rows => area.height,
123 }
124 }
125
126 fn max_size(&self, area: Rect) -> u16 {
129 let handle = u16::from(self.on_resize.is_some());
130 let room = self.extent(area).saturating_sub(handle + 1);
131 self.max.map_or(room, |max| max.min(room))
132 }
133
134 fn first_size(&self, area: Rect) -> u16 {
135 boundary::clamp_size(i32::from(self.size), self.min, self.max_size(area)).min(self.extent(area))
136 }
137
138 fn resize(&self, cx: &mut EventCx<'_, Msg>, target: i32) {
139 let area = cx.area();
140 let size = boundary::clamp_size(target, self.min, self.max_size(area));
141 if let Some(message) = &self.on_resize
142 && size != self.first_size(area)
143 {
144 cx.emit(message(size));
145 }
146 }
147}
148
149impl<Msg: 'static> Widget<Msg> for Split<Msg> {
150 fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
151 available
152 }
153
154 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
155 let first = self.first_size(area);
156 let handle = u16::from(self.on_resize.is_some());
157 let rest = self.extent(area).saturating_sub(first + handle);
158 let offset = |cells: u16| i32::from(cells);
159 let (first_rect, handle_rect, second_rect) = match self.axis {
160 Axis::Columns => (
161 Rect::new(area.x, area.y, first, area.height),
162 Rect::new(area.x + offset(first), area.y, handle, area.height),
163 Rect::new(area.x + offset(first + handle), area.y, rest, area.height),
164 ),
165 Axis::Rows => (
166 Rect::new(area.x, area.y, area.width, first),
167 Rect::new(area.x, area.y + offset(first), area.width, handle),
168 Rect::new(area.x, area.y + offset(first + handle), area.width, rest),
169 ),
170 };
171 cx.paint_child(&self.panes[0], first_rect);
172 cx.paint_child(&self.panes[1], second_rect);
173 if self.on_resize.is_some() {
174 boundary::paint(cx, handle_rect, None);
175 }
176 }
177
178 fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
179 if self.on_resize.is_none() {
180 return false;
181 }
182 let area = cx.area();
183 let current = i32::from(self.first_size(area));
184 match boundary::event(cx, event, self.axis) {
185 Change::Ignored => false,
186 Change::Used | Change::Activate => true,
187 Change::DragTo(at) => {
188 let start = match self.axis {
189 Axis::Columns => area.x,
190 Axis::Rows => area.y,
191 };
192 self.resize(cx, at - start);
193 true
194 }
195 Change::Nudge(cells) => {
196 self.resize(cx, current + cells);
197 true
198 }
199 Change::Jump(end) => {
200 self.resize(cx, if end { i32::MAX } else { 0 });
201 true
202 }
203 }
204 }
205
206 fn focusable(&self) -> bool {
207 self.on_resize.is_some()
208 }
209
210 fn children(&self) -> &[Node<Msg>] {
211 &self.panes
212 }
213
214 fn children_mut(&mut self) -> &mut [Node<Msg>] {
215 &mut self.panes
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use crate::event::{MouseButton, MouseKind};
223 use crate::runtime::{App, Command, Harness};
224 use crate::theme::State;
225 use crate::widgets::Text;
226
227 struct Demo {
228 size: u16,
229 rows: bool,
230 resizable: bool,
231 max: Option<u16>,
232 }
233
234 impl App for Demo {
235 type Msg = u16;
236 fn update(&mut self, size: u16) -> Command<u16> {
237 self.size = size;
238 Command::none()
239 }
240 fn view(&self, ui: &mut View<'_, u16>) {
241 let split = if self.rows { Splitter::rows(self.size) } else { Splitter::columns(self.size) };
242 let split = if self.resizable { split.on_resize(|size| size) } else { split };
243 split
244 .limits(3, self.max)
245 .first(|ui| {
246 ui.add(Text::new("left"));
247 })
248 .second(|ui| {
249 ui.add(Text::new("right"));
250 })
251 .show(ui)
252 .id("split");
253 }
254 }
255
256 #[test]
257 fn plain_panes_sit_side_by_side_without_a_boundary() {
258 let h = Harness::new(Demo { size: 6, rows: false, resizable: false, max: Some(12) }, 20, 2);
259 assert_eq!(h.screen(), "left right\n\n");
260 }
261
262 #[test]
263 fn boundary_is_invisible_until_hovered_and_takes_the_accent_while_dragged() {
264 let mut h = Harness::new(Demo { size: 6, rows: false, resizable: true, max: Some(12) }, 20, 3);
265 assert_eq!(h.screen(), "left right\n\n\n");
266 let theme = h.env().theme();
267 let canvas = theme.color("canvas");
268 let hover = theme.style("split-handle", None, &[State::Hover]).paint("bg").map(|p| p.at(0.0));
269 let active = theme.style("split-handle", None, &[State::Active]).paint("bg").map(|p| p.at(0.0));
270 assert_eq!(h.bg(6, 1), canvas);
271 h.hover(6, 1);
272 assert_eq!(h.bg(6, 1), hover);
273 assert_ne!(hover, canvas);
274 h.mouse(MouseKind::Down(MouseButton::Left), 6, 1);
275 h.mouse(MouseKind::Drag(MouseButton::Left), 9, 1);
276 assert_eq!(h.app().size, 9);
277 assert_eq!(h.bg(9, 0), active);
278 h.mouse(MouseKind::Drag(MouseButton::Left), 30, 1);
279 assert_eq!(h.app().size, 12, "limited by max");
280 h.mouse(MouseKind::Up(MouseButton::Left), 30, 1);
281 assert_eq!(h.screen(), "left right\n\n\n");
282 }
283
284 #[test]
285 fn keyboard_moves_the_focused_boundary() {
286 let mut h = Harness::new(Demo { size: 6, rows: true, resizable: true, max: Some(12) }, 20, 20);
287 h.press("tab");
288 assert!(h.is_focused("split"));
289 h.press("down");
290 assert_eq!(h.app().size, 7);
291 h.press("shift+up");
292 assert_eq!(h.app().size, 3, "limited by min");
293 h.press("end");
294 assert_eq!(h.app().size, 12);
295 assert_eq!(h.screen().lines().nth(13), Some("right"));
296 }
297
298 #[test]
299 fn without_an_upper_limit_only_the_second_pane_stops_the_boundary() {
300 let mut h = Harness::new(Demo { size: 6, rows: false, resizable: true, max: None }, 20, 3);
301 h.mouse(MouseKind::Down(MouseButton::Left), 6, 1);
302 h.mouse(MouseKind::Drag(MouseButton::Left), 30, 1);
303 assert_eq!(h.app().size, 18, "20 columns: the handle and one cell of the second pane stay");
304 h.mouse(MouseKind::Drag(MouseButton::Left), 0, 1);
305 assert_eq!(h.app().size, 3, "the lower limit still holds");
306 h.mouse(MouseKind::Up(MouseButton::Left), 0, 1);
307 h.press("end");
308 assert_eq!(h.app().size, 18);
309 assert_eq!(h.screen(), "left r\n i\n g\n");
310 h.press("home");
311 assert_eq!(h.app().size, 3);
312 }
313
314 #[test]
315 fn a_max_below_min_counts_as_min() {
316 let mut h = Harness::new(Demo { size: 6, rows: false, resizable: true, max: Some(2) }, 20, 1);
317 assert_eq!(h.screen(), "lef right\n", "the first pane is held at the minimum of 3");
318 h.press("tab").press("right");
319 assert_eq!(h.app().size, 6, "already at the only allowed size: nothing to send");
320 }
321
322 #[test]
323 fn narrow_area_keeps_the_second_pane() {
324 let h = Harness::new(Demo { size: 12, rows: false, resizable: true, max: Some(12) }, 8, 1);
325 assert_eq!(h.screen(), "left r\n");
326 }
327
328 #[derive(Default)]
330 struct TwoPanes {
331 copied: u32,
332 }
333
334 impl App for TwoPanes {
335 type Msg = bool;
336 fn update(&mut self, copied: bool) -> Command<bool> {
337 self.copied += u32::from(copied);
338 Command::none()
339 }
340 fn view(&self, ui: &mut View<'_, bool>) {
341 Splitter::columns(20)
342 .first(|ui| {
343 ui.row(|ui| {
344 ui.add(Text::new("places"));
345 });
346 })
347 .second(|ui| {
348 ui.row(|ui| {
349 ui.add(crate::widgets::Button::new("notes.txt").on_press(false));
350 })
351 .on_clipboard(crate::widget::ClipboardKey::Copy, true);
352 })
353 .show(ui);
354 }
355 }
356
357 #[test]
358 fn a_key_the_second_pane_claims_reaches_it_and_not_the_first() {
359 let mut h = Harness::new(TwoPanes::default(), 60, 6);
360 h.click_text("notes.txt");
361 h.press("ctrl+c");
362 assert_eq!(h.app().copied, 1, "the second pane's claim answered");
363 }
364}