1use iced::widget;
12use iced::{Element, Length};
13
14use crate::keys::Keymap;
15use crate::layout::resizable_panel::{self, Panel, panel};
16
17pub use crate::layout::resizable_panel::{Action, Axis, Event, Extent, Output, default_keymap};
18
19#[derive(Debug, Clone, PartialEq)]
21pub struct State {
22 group: resizable_panel::State,
23}
24
25impl State {
26 pub fn new(ratio: f32) -> Self {
29 let ratio = clamp_ratio(ratio);
30 Self::with_panels(
31 panel(Extent::Fraction(ratio)),
32 panel(Extent::Fraction(1.0 - ratio)),
33 )
34 }
35
36 pub fn with_panels(start: Panel, end: Panel) -> Self {
39 Self {
40 group: resizable_panel::State::new([start, end]),
41 }
42 }
43
44 pub fn min(self, start: Extent, end: Extent) -> Self {
46 self.map_panels(|[first, second]| [first.min(start), second.min(end)])
47 }
48
49 pub fn max(self, start: Extent, end: Extent) -> Self {
51 self.map_panels(|[first, second]| [first.max(start), second.max(end)])
52 }
53
54 pub fn collapsible(self, start: bool, end: bool) -> Self {
57 self.map_panels(|[first, second]| [first.collapsible(start), second.collapsible(end)])
58 }
59
60 fn map_panels(self, change: impl FnOnce([Panel; 2]) -> [Panel; 2]) -> Self {
61 let panels = self.group.panels();
62 let (Some(first), Some(second)) = (panels.first(), panels.get(1)) else {
63 return self;
64 };
65 let ratio = self.ratio();
66 let [start, end] = change([*first, *second]);
67 let mut state = Self::with_panels(start, end);
68 if !self.group.sizes().is_empty() {
69 state.group.set_sizes([ratio, 1.0 - ratio]);
70 }
71 state
72 }
73
74 pub fn with_ratio(mut self, ratio: f32) -> Self {
76 self.set_ratio(ratio);
77 self
78 }
79
80 pub fn set_ratio(&mut self, ratio: f32) {
83 let ratio = clamp_ratio(ratio);
84 self.group.set_sizes([ratio, 1.0 - ratio]);
85 }
86
87 pub fn ratio(&self) -> f32 {
89 ratio_of(&self.group.sizes())
90 }
91
92 pub fn is_start_collapsed(&self) -> bool {
93 self.group.is_collapsed(0)
94 }
95
96 pub fn is_end_collapsed(&self) -> bool {
97 self.group.is_collapsed(1)
98 }
99
100 pub fn group(&self) -> &resizable_panel::State {
102 &self.group
103 }
104
105 pub fn update(&mut self, event: Event) -> Option<Output> {
107 self.group.update(event)
108 }
109}
110
111impl Default for State {
112 fn default() -> Self {
114 Self::new(0.5)
115 }
116}
117
118fn clamp_ratio(ratio: f32) -> f32 {
119 if ratio.is_nan() {
120 return 0.5;
121 }
122 ratio.clamp(0.0, 1.0)
123}
124
125pub fn ratio_of(sizes: &[f32]) -> f32 {
128 let total: f32 = sizes.iter().sum();
129 match sizes.first() {
130 Some(first) if total > 0.0 => clamp_ratio(first / total),
131 _ => 0.5,
132 }
133}
134
135pub struct SplitPane<'a, Message> {
137 state: &'a State,
138 start: Element<'a, Message>,
139 end: Element<'a, Message>,
140 axis: Axis,
141 stack_below: Option<f32>,
142 grip: bool,
143 width: Length,
144 height: Length,
145 id: Option<widget::Id>,
146 keymap: Keymap<Action>,
147 on_event: Option<Box<dyn Fn(Event) -> Message + 'a>>,
148}
149
150impl<Message> std::fmt::Debug for SplitPane<'_, Message> {
151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 f.debug_struct("SplitPane")
153 .field("state", self.state)
154 .field("axis", &self.axis)
155 .field("stack_below", &self.stack_below)
156 .field("grip", &self.grip)
157 .finish_non_exhaustive()
158 }
159}
160
161pub fn split_pane<'a, Message>(
165 state: &'a State,
166 start: impl Into<Element<'a, Message>>,
167 end: impl Into<Element<'a, Message>>,
168) -> SplitPane<'a, Message> {
169 SplitPane {
170 state,
171 start: start.into(),
172 end: end.into(),
173 axis: Axis::default(),
174 stack_below: None,
175 grip: true,
176 width: Length::Fill,
177 height: Length::Fill,
178 id: None,
179 keymap: default_keymap(),
180 on_event: None,
181 }
182}
183
184impl<'a, Message> SplitPane<'a, Message> {
185 pub fn axis(mut self, axis: Axis) -> Self {
186 self.axis = axis;
187 self
188 }
189
190 pub fn stack_below(mut self, width: f32) -> Self {
193 self.stack_below = Some(width);
194 self
195 }
196
197 pub fn grip(mut self, grip: bool) -> Self {
200 self.grip = grip;
201 self
202 }
203
204 pub fn width(mut self, width: impl Into<Length>) -> Self {
205 self.width = width.into();
206 self
207 }
208
209 pub fn height(mut self, height: impl Into<Length>) -> Self {
210 self.height = height.into();
211 self
212 }
213
214 pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
216 self.id = Some(id.into());
217 self
218 }
219
220 pub fn keymap(mut self, keymap: Keymap<Action>) -> Self {
221 self.keymap = keymap;
222 self
223 }
224
225 pub fn on_event(mut self, on_event: impl Fn(Event) -> Message + 'a) -> Self {
226 self.on_event = Some(Box::new(on_event));
227 self
228 }
229}
230
231impl<'a, Message: 'a> From<SplitPane<'a, Message>> for Element<'a, Message> {
232 fn from(split: SplitPane<'a, Message>) -> Self {
233 let SplitPane {
234 state,
235 start,
236 end,
237 axis,
238 stack_below,
239 grip,
240 width,
241 height,
242 id,
243 keymap,
244 on_event,
245 } = split;
246
247 let mut group = resizable_panel::resizable_panel(&state.group, [start, end])
248 .axis(axis)
249 .grip(grip)
250 .width(width)
251 .height(height)
252 .keymap(keymap);
253 if let Some(breakpoint) = stack_below {
254 group = group.stack_below(breakpoint);
255 }
256 if let Some(id) = id {
257 group = group.id(id);
258 }
259 if let Some(on_event) = on_event {
260 group = group.on_event(on_event);
261 }
262 group.into()
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 fn close(a: f32, b: f32) -> bool {
271 (a - b).abs() < 1e-3
272 }
273
274 fn measured(state: State) -> State {
275 let mut state = state;
276 let _ = state.update(Event::Measure(1000.0));
277 state
278 }
279
280 #[test]
281 fn ratio_is_clamped_and_defaults_to_even() {
282 assert!(close(State::new(0.3).ratio(), 0.3));
283 assert!(close(State::new(1.5).ratio(), 1.0));
284 assert!(close(State::new(-1.0).ratio(), 0.0));
285 assert!(close(State::new(f32::NAN).ratio(), 0.5));
286 assert!(close(State::default().ratio(), 0.5));
287 }
288
289 #[test]
290 fn ratio_of_sizes() {
291 assert!(close(ratio_of(&[300.0, 700.0]), 0.3));
292 assert!(close(ratio_of(&[1.0, 3.0]), 0.25));
293 assert!(close(ratio_of(&[]), 0.5));
294 assert!(close(ratio_of(&[0.0, 0.0]), 0.5));
295 }
296
297 #[test]
298 fn dragging_changes_the_ratio() {
299 let mut state = measured(State::new(0.5));
300 let _ = state.update(Event::DragStart {
301 handle: 0,
302 position: 500.0,
303 });
304 assert_eq!(state.update(Event::Drag(650.0)), Some(Output::Resized));
305 assert!(close(state.ratio(), 0.65));
306 }
307
308 #[test]
309 fn limits_hold_the_ratio_in_range() {
310 let mut state = measured(State::new(0.5).min(Extent::Pixels(200.0), Extent::Fraction(0.4)));
311 let _ = state.update(Event::Resize {
312 handle: 0,
313 delta: 400.0,
314 });
315 assert!(close(state.ratio(), 0.6), "end keeps 40%");
316 let _ = state.update(Event::Resize {
317 handle: 0,
318 delta: -900.0,
319 });
320 assert!(close(state.ratio(), 0.2), "start keeps 200px");
321
322 let capped = measured(State::new(0.9).max(Extent::Fraction(0.7), Extent::Fraction(1.0)));
323 assert!(close(capped.ratio(), 0.7));
324 }
325
326 #[test]
327 fn set_ratio_fits_the_limits_once_measured() {
328 let mut state = measured(State::new(0.5).min(Extent::Pixels(100.0), Extent::Pixels(300.0)));
329 state.set_ratio(0.9);
330 assert!(close(state.ratio(), 0.7));
331 let restored = State::new(0.5).with_ratio(0.25);
332 assert!(close(restored.ratio(), 0.25));
333 }
334
335 #[test]
336 fn limits_keep_a_ratio_set_earlier() {
337 let state = State::new(0.5)
338 .with_ratio(0.3)
339 .min(Extent::Pixels(10.0), Extent::Pixels(10.0));
340 assert!(close(state.ratio(), 0.3));
341 }
342
343 #[test]
344 fn either_side_can_collapse() {
345 let mut state = measured(
346 State::new(0.5)
347 .min(Extent::Pixels(200.0), Extent::Pixels(200.0))
348 .collapsible(false, true),
349 );
350 assert_eq!(state.update(Event::Toggle(0)), None);
351 assert_eq!(state.update(Event::Toggle(1)), Some(Output::Collapsed(1)));
352 assert!(state.is_end_collapsed());
353 assert!(!state.is_start_collapsed());
354 assert!(close(state.ratio(), 1.0));
355 }
356
357 #[test]
358 fn builder_defaults_show_a_grip() {
359 let state = State::default();
360 let split: SplitPane<'_, ()> = split_pane(&state, "start", "end");
361 assert!(split.grip);
362 assert_eq!(split.axis, Axis::Horizontal);
363 assert_eq!(split.stack_below, None);
364 assert!(split.on_event.is_none());
365 }
366}