1use iced::Event as Input;
26use iced::advanced::layout::{self, Layout};
27use iced::advanced::widget::operation::Focusable;
28use iced::advanced::widget::{Operation, Tree, Widget, tree};
29use iced::advanced::{Clipboard, Shell, overlay, renderer};
30use iced::keyboard::{self, key::Named};
31use iced::widget;
32use iced::{
33 Background, Border, Color, Element, Length, Point, Rectangle, Renderer, Shadow, Size, Theme,
34 Vector, mouse, touch,
35};
36
37use crate::keys::{self, Chord, Keymap};
38use crate::theme::{Tokens, radius};
39
40pub const HANDLE: f32 = 1.0;
42
43pub const HIT_AREA: f32 = 9.0;
45
46const HIGHLIGHT: f32 = 3.0;
48
49const GRIP: Size = Size::new(12.0, 18.0);
51
52const EPSILON: f32 = 0.01;
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
57pub enum Axis {
58 #[default]
60 Horizontal,
61 Vertical,
63}
64
65impl Axis {
66 pub const ALL: [Axis; 2] = [Axis::Horizontal, Axis::Vertical];
67
68 pub fn along(self, point: Point) -> f32 {
70 match self {
71 Axis::Horizontal => point.x,
72 Axis::Vertical => point.y,
73 }
74 }
75
76 pub fn cursor(self) -> mouse::Interaction {
78 match self {
79 Axis::Horizontal => mouse::Interaction::ResizingColumn,
80 Axis::Vertical => mouse::Interaction::ResizingRow,
81 }
82 }
83
84 fn main(self, size: Size) -> f32 {
85 match self {
86 Axis::Horizontal => size.width,
87 Axis::Vertical => size.height,
88 }
89 }
90
91 fn cross(self, size: Size) -> f32 {
92 match self {
93 Axis::Horizontal => size.height,
94 Axis::Vertical => size.width,
95 }
96 }
97
98 fn size(self, main: f32, cross: f32) -> Size {
99 match self {
100 Axis::Horizontal => Size::new(main, cross),
101 Axis::Vertical => Size::new(cross, main),
102 }
103 }
104
105 fn point(self, main: f32) -> Point {
106 match self {
107 Axis::Horizontal => Point::new(main, 0.0),
108 Axis::Vertical => Point::new(0.0, main),
109 }
110 }
111}
112
113#[derive(Debug, Clone, Copy, PartialEq)]
116pub enum Extent {
117 Fraction(f32),
119 Pixels(f32),
120}
121
122impl Extent {
123 pub fn resolve(self, total: f32) -> f32 {
125 let pixels = match self {
126 Extent::Fraction(share) => share * total,
127 Extent::Pixels(pixels) => pixels,
128 };
129 pixels.max(0.0)
130 }
131}
132
133#[derive(Debug, Clone, Copy, PartialEq)]
135pub struct Panel {
136 pub default: Extent,
139 pub min: Extent,
140 pub max: Extent,
141 pub collapsible: bool,
143 pub collapsed_size: Extent,
145 pub collapse_below: Option<Extent>,
148 pub collapsed: bool,
150}
151
152pub fn panel(default: Extent) -> Panel {
154 Panel {
155 default,
156 min: Extent::Pixels(0.0),
157 max: Extent::Fraction(1.0),
158 collapsible: false,
159 collapsed_size: Extent::Pixels(0.0),
160 collapse_below: None,
161 collapsed: false,
162 }
163}
164
165impl Panel {
166 pub fn min(mut self, min: Extent) -> Self {
167 self.min = min;
168 self
169 }
170
171 pub fn max(mut self, max: Extent) -> Self {
172 self.max = max;
173 self
174 }
175
176 pub fn collapsible(mut self, collapsible: bool) -> Self {
177 self.collapsible = collapsible;
178 self
179 }
180
181 pub fn collapsed_size(mut self, size: Extent) -> Self {
183 self.collapsed_size = size;
184 self
185 }
186
187 pub fn collapse_below(mut self, threshold: Extent) -> Self {
188 self.collapse_below = Some(threshold);
189 self
190 }
191
192 pub fn collapsed(mut self, collapsed: bool) -> Self {
194 self.collapsed = collapsed;
195 self
196 }
197
198 pub fn limits(&self, total: f32) -> Limits {
201 let min = self.min.resolve(total);
202 let collapsed = self.collapsed_size.resolve(total).min(min);
203 Limits {
204 min,
205 max: self.max.resolve(total).max(min),
206 collapsed,
207 threshold: self
208 .collapse_below
209 .map_or((collapsed + min) / 2.0, |threshold| {
210 threshold.resolve(total).clamp(collapsed, min)
211 }),
212 }
213 }
214
215 pub fn settle(&self, size: f32, total: f32) -> f32 {
218 let limits = self.limits(total);
219 if self.collapsible && size < limits.threshold {
220 return limits.collapsed;
221 }
222 size.clamp(limits.min, limits.max)
223 }
224
225 pub fn is_collapsed_at(&self, size: f32, total: f32) -> bool {
227 let limits = self.limits(total);
228 self.collapsible && size <= limits.collapsed + EPSILON && limits.collapsed < limits.min
229 }
230}
231
232#[derive(Debug, Clone, Copy, PartialEq)]
234pub struct Limits {
235 pub min: f32,
236 pub max: f32,
237 pub collapsed: f32,
238 pub threshold: f32,
239}
240
241pub fn defaults(panels: &[Panel], total: f32) -> Vec<f32> {
245 let fixed: f32 = panels
246 .iter()
247 .filter_map(|panel| match panel.default {
248 Extent::Pixels(pixels) => Some(pixels.max(0.0)),
249 Extent::Fraction(_) => None,
250 })
251 .sum();
252 let shares: f32 = panels
253 .iter()
254 .filter_map(|panel| match panel.default {
255 Extent::Fraction(share) => Some(share.max(0.0)),
256 Extent::Pixels(_) => None,
257 })
258 .sum();
259 let left = (total - fixed).max(0.0);
260 let sizes = panels
261 .iter()
262 .map(|panel| match panel.default {
263 Extent::Pixels(pixels) => pixels.max(0.0),
264 Extent::Fraction(share) if shares > 0.0 => left * share.max(0.0) / shares,
265 Extent::Fraction(_) => 0.0,
266 })
267 .collect();
268 let collapsed: Vec<bool> = panels
269 .iter()
270 .map(|panel| panel.collapsible && panel.collapsed)
271 .collect();
272 fit(panels, &collapsed, sizes, total)
273}
274
275pub fn fit(panels: &[Panel], collapsed: &[bool], sizes: Vec<f32>, total: f32) -> Vec<f32> {
282 let mut shut: Vec<bool> = (0..panels.len())
283 .map(|index| collapsed.get(index).copied().unwrap_or(false))
284 .collect();
285 let mut fitted = constrain(panels, &shut, &sizes, total);
286 distribute(panels, &shut, &mut fitted, total);
287
288 while fitted.iter().sum::<f32>() > total + EPSILON {
289 let Some(index) = (0..panels.len())
290 .rev()
291 .find(|&index| panels[index].collapsible && !shut[index])
292 else {
293 break;
294 };
295 shut[index] = true;
296 fitted = constrain(panels, &shut, &sizes, total);
297 distribute(panels, &shut, &mut fitted, total);
298 }
299
300 let sum: f32 = fitted.iter().sum();
301 if sum > EPSILON && (sum - total).abs() > EPSILON {
302 for size in &mut fitted {
303 *size *= total / sum;
304 }
305 }
306 fitted
307}
308
309fn constrain(panels: &[Panel], shut: &[bool], sizes: &[f32], total: f32) -> Vec<f32> {
310 panels
311 .iter()
312 .enumerate()
313 .map(|(index, panel)| {
314 let limits = panel.limits(total);
315 if shut[index] && panel.collapsible {
316 return limits.collapsed;
317 }
318 sizes
319 .get(index)
320 .copied()
321 .unwrap_or(0.0)
322 .clamp(limits.min, limits.max)
323 })
324 .collect()
325}
326
327fn distribute(panels: &[Panel], shut: &[bool], sizes: &mut [f32], total: f32) {
330 for _ in 0..=panels.len() {
331 let gap = total - sizes.iter().sum::<f32>();
332 if gap.abs() <= EPSILON {
333 return;
334 }
335 let room = |index: usize| {
336 let limits = panels[index].limits(total);
337 let open = !(shut[index] && panels[index].collapsible);
338 open && if gap > 0.0 {
339 sizes[index] < limits.max - EPSILON
340 } else {
341 sizes[index] > limits.min + EPSILON
342 }
343 };
344 let flexible: Vec<usize> = (0..panels.len()).filter(|&index| room(index)).collect();
345 if flexible.is_empty() {
346 return;
347 }
348 let weight: f32 = flexible.iter().map(|&index| sizes[index]).sum();
349 for &index in &flexible {
350 let share = if weight > EPSILON {
351 sizes[index] / weight
352 } else {
353 1.0 / flexible.len() as f32
354 };
355 let limits = panels[index].limits(total);
356 sizes[index] = (sizes[index] + gap * share).clamp(limits.min, limits.max);
357 }
358 }
359}
360
361pub fn resize(panels: &[Panel], origin: &[f32], handle: usize, delta: f32, total: f32) -> Vec<f32> {
369 let count = panels.len();
370 if handle + 1 >= count || origin.len() != count || delta.abs() < EPSILON {
371 return origin.to_vec();
372 }
373 let before: Vec<usize> = (0..=handle).rev().collect();
374 let after: Vec<usize> = (handle + 1..count).collect();
375 let (grow, shrink) = if delta > 0.0 {
376 (before, after)
377 } else {
378 (after, before)
379 };
380
381 let mut request = delta.abs();
382 for _ in 0..3 {
383 let (shrunk, given) = shrink_by(panels, origin, &shrink, request, total);
384 if given < EPSILON {
385 return origin.to_vec();
386 }
387 let (grown, taken) = grow_by(panels, shrunk, &grow, given, total);
388 if (taken - given).abs() < EPSILON {
389 return grown;
390 }
391 request = taken;
394 }
395 origin.to_vec()
396}
397
398fn shrink_by(
399 panels: &[Panel],
400 origin: &[f32],
401 order: &[usize],
402 request: f32,
403 total: f32,
404) -> (Vec<f32>, f32) {
405 let mut sizes = origin.to_vec();
406 let mut given = 0.0;
407 for &index in order {
408 let remaining = request - given;
409 if remaining < EPSILON {
410 break;
411 }
412 let current = sizes[index];
413 let settled = panels[index].settle(current - remaining, total);
414 if settled < current {
415 given += current - settled;
416 sizes[index] = settled;
417 }
418 }
419 (sizes, given)
420}
421
422fn grow_by(
423 panels: &[Panel],
424 mut sizes: Vec<f32>,
425 order: &[usize],
426 amount: f32,
427 total: f32,
428) -> (Vec<f32>, f32) {
429 let mut taken = 0.0;
430 for &index in order {
431 let remaining = amount - taken;
432 if remaining < EPSILON {
433 break;
434 }
435 let current = sizes[index];
436 let settled = panels[index].settle(current + remaining, total);
437 if settled > current {
438 taken += settled - current;
439 sizes[index] = settled;
440 } else if panels[index].is_collapsed_at(current, total) {
441 break;
443 }
444 }
445 (sizes, taken)
446}
447
448#[derive(Debug, Clone, Copy, PartialEq)]
450pub enum Event {
451 Measure(f32),
454 DragStart {
456 handle: usize,
457 position: f32,
458 },
459 Drag(f32),
461 DragEnd,
462 Resize {
464 handle: usize,
465 delta: f32,
466 },
467 Collapse(usize),
468 Expand(usize),
469 Toggle(usize),
471 Reset,
473}
474
475#[derive(Debug, Clone, Copy, PartialEq, Eq)]
477pub enum Output {
478 Resized,
480 Collapsed(usize),
482 Expanded(usize),
484}
485
486#[derive(Debug, Clone, PartialEq)]
489struct Drag {
490 handle: usize,
491 start: f32,
492 origin: Vec<f32>,
493}
494
495#[derive(Debug, Clone, PartialEq)]
497pub struct State {
498 panels: Vec<Panel>,
499 sizes: Vec<f32>,
502 collapsed: Vec<bool>,
503 restore: Vec<Option<f32>>,
505 total: f32,
506 restored: bool,
509 drag: Option<Drag>,
510 step: Extent,
511 large_step: Extent,
512}
513
514impl State {
515 pub fn new(panels: impl IntoIterator<Item = Panel>) -> Self {
517 let panels: Vec<Panel> = panels.into_iter().collect();
518 let collapsed = panels
519 .iter()
520 .map(|panel| panel.collapsible && panel.collapsed)
521 .collect();
522 Self {
523 restore: vec![None; panels.len()],
524 panels,
525 sizes: Vec::new(),
526 collapsed,
527 total: 0.0,
528 restored: false,
529 drag: None,
530 step: Extent::Fraction(0.05),
531 large_step: Extent::Fraction(0.2),
532 }
533 }
534
535 pub fn with_sizes(mut self, sizes: impl IntoIterator<Item = f32>) -> Self {
539 self.set_sizes(sizes);
540 self
541 }
542
543 pub fn set_sizes(&mut self, sizes: impl IntoIterator<Item = f32>) {
546 let sizes: Vec<f32> = sizes.into_iter().map(|size| size.max(0.0)).collect();
547 let sum: f32 = sizes.iter().sum();
548 if sizes.len() != self.panels.len() || sum <= 0.0 {
549 return;
550 }
551 self.sizes = sizes.iter().map(|size| size / sum).collect();
552 self.collapsed = self
553 .panels
554 .iter()
555 .zip(&self.sizes)
556 .map(|(panel, size)| panel.collapsible && *size <= EPSILON / 100.0)
557 .collect();
558 self.restored = true;
559 if self.total > 0.0 {
560 self.settle_restored();
561 }
562 }
563
564 pub fn step(mut self, step: Extent) -> Self {
566 self.step = step;
567 self
568 }
569
570 pub fn large_step(mut self, step: Extent) -> Self {
572 self.large_step = step;
573 self
574 }
575
576 pub fn panels(&self) -> &[Panel] {
577 &self.panels
578 }
579
580 pub fn sizes(&self) -> Vec<f32> {
585 if !self.sizes.is_empty() {
586 return self.sizes.clone();
587 }
588 let fractions: Option<Vec<f32>> = self
589 .panels
590 .iter()
591 .map(|panel| match panel.default {
592 Extent::Fraction(share) => Some(share),
593 Extent::Pixels(_) => None,
594 })
595 .collect();
596 fractions.unwrap_or_default()
597 }
598
599 pub fn total(&self) -> f32 {
602 self.total
603 }
604
605 pub fn is_collapsed(&self, panel: usize) -> bool {
606 self.collapsed.get(panel).copied().unwrap_or(false)
607 }
608
609 pub fn dragging(&self) -> Option<usize> {
611 self.drag.as_ref().map(|drag| drag.handle)
612 }
613
614 pub fn resolve(&self, total: f32) -> Vec<f32> {
616 if self.sizes.is_empty() {
617 let mut sizes = defaults(&self.panels, total);
618 self.collapse_marked(&mut sizes, total);
619 return sizes;
620 }
621 let sizes = self.sizes.iter().map(|size| size * total).collect();
622 fit(&self.panels, &self.collapsed, sizes, total)
623 }
624
625 fn collapse_marked(&self, sizes: &mut [f32], total: f32) {
626 let marked = self.collapsed.iter().any(|collapsed| *collapsed);
627 if marked {
628 let fitted = fit(&self.panels, &self.collapsed, sizes.to_vec(), total);
629 sizes.copy_from_slice(&fitted);
630 }
631 }
632
633 fn current(&self) -> Vec<f32> {
635 self.resolve(self.total)
636 }
637
638 pub fn update(&mut self, event: Event) -> Option<Output> {
640 if let Event::Measure(total) = event {
641 self.measure(total);
642 return None;
643 }
644 if self.total <= 0.0 {
645 return None;
646 }
647 match event {
648 Event::Measure(_) => None,
649 Event::DragStart { handle, position } => {
650 if handle + 1 >= self.panels.len() {
651 return None;
652 }
653 self.drag = Some(Drag {
654 handle,
655 start: position,
656 origin: self.current(),
657 });
658 None
659 }
660 Event::Drag(position) => {
661 let drag = self.drag.clone()?;
662 let sizes = resize(
663 &self.panels,
664 &drag.origin,
665 drag.handle,
666 position - drag.start,
667 self.total,
668 );
669 self.apply(sizes, &drag.origin)
670 }
671 Event::DragEnd => {
672 self.drag = None;
673 None
674 }
675 Event::Resize { handle, delta } => {
676 let origin = self.current();
677 let sizes = resize(&self.panels, &origin, handle, delta, self.total);
678 self.apply(sizes, &origin)
679 }
680 Event::Collapse(panel) => self.collapse(panel),
681 Event::Expand(panel) => self.expand(panel),
682 Event::Toggle(panel) if self.is_collapsed(panel) => self.expand(panel),
683 Event::Toggle(panel) => self.collapse(panel),
684 Event::Reset => {
685 let origin = self.current();
686 self.collapsed = self
687 .panels
688 .iter()
689 .map(|panel| panel.collapsible && panel.collapsed)
690 .collect();
691 self.restore = vec![None; self.panels.len()];
692 self.sizes.clear();
693 let sizes = self.current();
694 self.store(&sizes);
695 (!same(&sizes, &origin)).then_some(Output::Resized)
696 }
697 }
698 }
699
700 fn measure(&mut self, total: f32) {
701 if total <= 0.0 || !total.is_finite() {
702 return;
703 }
704 if self.sizes.is_empty() {
705 let sizes = self.resolve(total);
706 self.total = total;
707 self.store(&sizes);
708 return;
709 }
710 self.total = total;
711 if self.restored {
712 self.settle_restored();
713 }
714 }
715
716 fn settle_restored(&mut self) {
719 self.restored = false;
720 let total = self.total;
721 self.collapsed = self
722 .panels
723 .iter()
724 .zip(&self.sizes)
725 .map(|(panel, size)| panel.collapsible && size * total < panel.limits(total).threshold)
726 .collect();
727 let sizes = self.current();
728 self.store(&sizes);
729 }
730
731 fn handle_for(&self, panel: usize) -> Option<(usize, bool)> {
734 if panel + 1 < self.panels.len() {
735 return Some((panel, true));
736 }
737 (panel > 0 && panel < self.panels.len()).then(|| (panel - 1, false))
738 }
739
740 fn collapse(&mut self, panel: usize) -> Option<Output> {
741 let settings = self.panels.get(panel)?;
742 if !settings.collapsible || self.is_collapsed(panel) {
743 return None;
744 }
745 let (handle, forward) = self.handle_for(panel)?;
746 let origin = self.current();
747 let shrink = origin[panel] - settings.limits(self.total).collapsed;
748 let delta = if forward { -shrink } else { shrink };
749 let sizes = resize(&self.panels, &origin, handle, delta, self.total);
750 self.apply(sizes, &origin)
751 }
752
753 fn expand(&mut self, panel: usize) -> Option<Output> {
754 let settings = self.panels.get(panel)?;
755 if !self.is_collapsed(panel) {
756 return None;
757 }
758 let (handle, forward) = self.handle_for(panel)?;
759 let origin = self.current();
760 let limits = settings.limits(self.total);
761 let wanted = self
762 .restore
763 .get(panel)
764 .copied()
765 .flatten()
766 .map_or_else(
767 || settings.default.resolve(self.total),
768 |size| size * self.total,
769 )
770 .clamp(limits.min, limits.max);
771 let grow = wanted - origin[panel];
772 let delta = if forward { grow } else { -grow };
773 let sizes = resize(&self.panels, &origin, handle, delta, self.total);
774 self.apply(sizes, &origin)
775 }
776
777 fn apply(&mut self, sizes: Vec<f32>, origin: &[f32]) -> Option<Output> {
779 if same(&sizes, &self.current()) {
780 return None;
781 }
782 let was = self.collapsed.clone();
783 self.store(&sizes);
784 let changed = (0..self.panels.len()).find(|&index| was[index] != self.collapsed[index]);
785 match changed {
786 Some(index) if self.collapsed[index] => {
787 if let (Some(slot), Some(size)) = (self.restore.get_mut(index), origin.get(index)) {
788 *slot = Some(size / self.total);
789 }
790 Some(Output::Collapsed(index))
791 }
792 Some(index) => Some(Output::Expanded(index)),
793 None => Some(Output::Resized),
794 }
795 }
796
797 fn store(&mut self, sizes: &[f32]) {
798 let sum: f32 = sizes.iter().sum();
799 if sum <= 0.0 {
800 return;
801 }
802 self.sizes = sizes.iter().map(|size| size / sum).collect();
803 self.collapsed = self
804 .panels
805 .iter()
806 .zip(sizes)
807 .map(|(panel, size)| panel.is_collapsed_at(*size, self.total.max(sum)))
808 .collect();
809 }
810}
811
812fn same(a: &[f32], b: &[f32]) -> bool {
813 a.len() == b.len() && a.iter().zip(b).all(|(a, b)| (a - b).abs() < EPSILON)
814}
815
816#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
818pub enum Action {
819 Decrease,
821 Increase,
823 DecreaseMore,
824 IncreaseMore,
825 Min,
827 Max,
829 Toggle,
831 Release,
833}
834
835impl Action {
836 pub fn event(self, state: &State, handle: usize) -> Option<Event> {
840 if handle + 1 >= state.panels.len() {
841 return None;
842 }
843 let total = state.total;
844 let size = || state.current().get(handle).copied().unwrap_or(0.0);
845 let limits = || state.panels[handle].limits(total);
846 let delta = match self {
847 Action::Decrease => -state.step.resolve(total),
848 Action::Increase => state.step.resolve(total),
849 Action::DecreaseMore => -state.large_step.resolve(total),
850 Action::IncreaseMore => state.large_step.resolve(total),
851 Action::Min => limits().min - size(),
852 Action::Max => limits().max - size(),
853 Action::Toggle => {
854 return [handle, handle + 1]
855 .into_iter()
856 .find(|&panel| state.panels[panel].collapsible)
857 .map(Event::Toggle);
858 }
859 Action::Release => return None,
860 };
861 Some(Event::Resize { handle, delta })
862 }
863}
864
865impl keys::Action for Action {
866 const ALL: &'static [Self] = &[
867 Action::Decrease,
868 Action::Increase,
869 Action::DecreaseMore,
870 Action::IncreaseMore,
871 Action::Min,
872 Action::Max,
873 Action::Toggle,
874 Action::Release,
875 ];
876
877 fn defaults() -> Keymap<Self> {
878 default_keymap()
879 }
880
881 fn name(self) -> &'static str {
882 match self {
883 Action::Decrease => "Decrease",
884 Action::Increase => "Increase",
885 Action::DecreaseMore => "DecreaseMore",
886 Action::IncreaseMore => "IncreaseMore",
887 Action::Min => "Min",
888 Action::Max => "Max",
889 Action::Toggle => "Toggle",
890 Action::Release => "Release",
891 }
892 }
893
894 fn description(self) -> &'static str {
895 match self {
896 Action::Decrease => "Moves the focused handle one step towards the start.",
897 Action::Increase => "Moves the focused handle one step towards the end.",
898 Action::DecreaseMore => "Moves the focused handle a large step towards the start.",
899 Action::IncreaseMore => "Moves the focused handle a large step towards the end.",
900 Action::Min => "Shrinks the panel before the handle to its minimum.",
901 Action::Max => "Grows the panel before the handle to its maximum.",
902 Action::Toggle => "Collapses or expands the collapsible panel next to the handle.",
903 Action::Release => "Takes the focus away from the handle.",
904 }
905 }
906}
907
908pub fn default_keymap() -> Keymap<Action> {
924 Keymap::new()
925 .bind(Chord::named(Named::ArrowLeft), Action::Decrease)
926 .bind(Chord::named(Named::ArrowUp), Action::Decrease)
927 .bind(Chord::named(Named::ArrowRight), Action::Increase)
928 .bind(Chord::named(Named::ArrowDown), Action::Increase)
929 .bind(Chord::named(Named::ArrowLeft).shift(), Action::DecreaseMore)
930 .bind(Chord::named(Named::ArrowUp).shift(), Action::DecreaseMore)
931 .bind(
932 Chord::named(Named::ArrowRight).shift(),
933 Action::IncreaseMore,
934 )
935 .bind(Chord::named(Named::ArrowDown).shift(), Action::IncreaseMore)
936 .bind(Chord::named(Named::Home), Action::Min)
937 .bind(Chord::named(Named::End), Action::Max)
938 .bind(Chord::named(Named::Enter), Action::Toggle)
939 .bind(Chord::named(Named::Escape), Action::Release)
940}
941
942#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
944pub enum HandleStatus {
945 #[default]
946 Idle,
947 Hovered,
949 Dragged,
950 Focused,
952 Disabled,
954}
955
956impl HandleStatus {
957 pub const ALL: [HandleStatus; 5] = [
958 HandleStatus::Idle,
959 HandleStatus::Hovered,
960 HandleStatus::Dragged,
961 HandleStatus::Focused,
962 HandleStatus::Disabled,
963 ];
964}
965
966#[derive(Debug, Clone, Copy, PartialEq)]
968pub struct HandleStyle {
969 pub line: Color,
971 pub highlight: Option<Color>,
973 pub grip: Color,
974 pub grip_border: Color,
975 pub grip_dots: Color,
976}
977
978pub fn handle_style(tokens: &Tokens, status: HandleStatus) -> HandleStyle {
980 let highlight = match status {
981 HandleStatus::Idle | HandleStatus::Disabled => None,
982 HandleStatus::Hovered => Some(tokens.border),
983 HandleStatus::Dragged | HandleStatus::Focused => Some(tokens.ring),
984 };
985 HandleStyle {
986 line: tokens.border,
987 highlight,
988 grip: tokens.background,
989 grip_border: match status {
990 HandleStatus::Focused | HandleStatus::Dragged => tokens.ring,
991 _ => tokens.border,
992 },
993 grip_dots: tokens.muted_foreground,
994 }
995}
996
997pub struct ResizablePanel<'a, Message> {
999 state: &'a State,
1000 panes: Vec<Element<'a, Message>>,
1001 axis: Axis,
1002 stack_below: Option<f32>,
1003 grip: bool,
1004 width: Length,
1005 height: Length,
1006 id: Option<widget::Id>,
1007 keymap: Keymap<Action>,
1008 on_event: Option<Box<dyn Fn(Event) -> Message + 'a>>,
1009}
1010
1011impl<Message> std::fmt::Debug for ResizablePanel<'_, Message> {
1012 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1013 f.debug_struct("ResizablePanel")
1014 .field("state", self.state)
1015 .field("panes", &self.panes.len())
1016 .field("axis", &self.axis)
1017 .field("stack_below", &self.stack_below)
1018 .field("grip", &self.grip)
1019 .field("keymap", &self.keymap)
1020 .finish_non_exhaustive()
1021 }
1022}
1023
1024pub fn resizable_panel<'a, Message>(
1027 state: &'a State,
1028 panes: impl IntoIterator<Item = Element<'a, Message>>,
1029) -> ResizablePanel<'a, Message> {
1030 ResizablePanel {
1031 state,
1032 panes: panes.into_iter().collect(),
1033 axis: Axis::default(),
1034 stack_below: None,
1035 grip: false,
1036 width: Length::Fill,
1037 height: Length::Fill,
1038 id: None,
1039 keymap: default_keymap(),
1040 on_event: None,
1041 }
1042}
1043
1044impl<'a, Message> ResizablePanel<'a, Message> {
1045 pub fn axis(mut self, axis: Axis) -> Self {
1046 self.axis = axis;
1047 self
1048 }
1049
1050 pub fn stack_below(mut self, width: f32) -> Self {
1053 self.stack_below = Some(width);
1054 self
1055 }
1056
1057 pub fn grip(mut self, grip: bool) -> Self {
1059 self.grip = grip;
1060 self
1061 }
1062
1063 pub fn width(mut self, width: impl Into<Length>) -> Self {
1065 self.width = width.into();
1066 self
1067 }
1068
1069 pub fn height(mut self, height: impl Into<Length>) -> Self {
1071 self.height = height.into();
1072 self
1073 }
1074
1075 pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
1078 self.id = Some(id.into());
1079 self
1080 }
1081
1082 pub fn keymap(mut self, keymap: Keymap<Action>) -> Self {
1084 self.keymap = keymap;
1085 self
1086 }
1087
1088 pub fn on_event(mut self, on_event: impl Fn(Event) -> Message + 'a) -> Self {
1089 self.on_event = Some(Box::new(on_event));
1090 self
1091 }
1092
1093 pub fn axis_at(&self, width: f32) -> Axis {
1095 match self.stack_below {
1096 Some(breakpoint) if self.axis == Axis::Horizontal && width < breakpoint => {
1097 Axis::Vertical
1098 }
1099 _ => self.axis,
1100 }
1101 }
1102}
1103
1104impl<'a, Message: 'a> From<ResizablePanel<'a, Message>> for Element<'a, Message> {
1105 fn from(group: ResizablePanel<'a, Message>) -> Self {
1106 Element::new(group)
1107 }
1108}
1109
1110#[derive(Debug, Clone, Copy, Default)]
1114struct HandleFocus {
1115 focused: bool,
1116 visible: bool,
1117}
1118
1119impl Focusable for HandleFocus {
1120 fn is_focused(&self) -> bool {
1121 self.focused
1122 }
1123
1124 fn focus(&mut self) {
1125 self.focused = true;
1126 self.visible = true;
1127 }
1128
1129 fn unfocus(&mut self) {
1130 self.focused = false;
1131 self.visible = false;
1132 }
1133}
1134
1135#[derive(Debug, Default)]
1137struct Memory {
1138 axis: Axis,
1139 total: f32,
1140 slots: Vec<(f32, f32)>,
1143 hovered: Option<usize>,
1144 handles: Vec<HandleFocus>,
1145}
1146
1147impl Memory {
1148 fn focused(&self) -> Option<usize> {
1149 self.handles.iter().position(|handle| handle.focused)
1150 }
1151
1152 fn visible(&self) -> Option<usize> {
1154 self.handles
1155 .iter()
1156 .position(|handle| handle.focused && handle.visible)
1157 }
1158
1159 fn focus(&mut self, index: Option<usize>) {
1161 for (i, handle) in self.handles.iter_mut().enumerate() {
1162 handle.focused = Some(i) == index;
1163 handle.visible = false;
1164 }
1165 }
1166
1167 fn show_focus(&mut self) {
1168 for handle in &mut self.handles {
1169 handle.visible = handle.focused;
1170 }
1171 }
1172
1173 fn slot(&self, bounds: Rectangle, index: usize) -> Option<Rectangle> {
1175 let (start, length) = *self.slots.get(index)?;
1176 Some(match self.axis {
1177 Axis::Horizontal => Rectangle {
1178 x: bounds.x + start,
1179 width: length,
1180 ..bounds
1181 },
1182 Axis::Vertical => Rectangle {
1183 y: bounds.y + start,
1184 height: length,
1185 ..bounds
1186 },
1187 })
1188 }
1189
1190 fn lines(&self, bounds: Rectangle) -> Vec<Rectangle> {
1192 let count = self.slots.len().saturating_sub(1);
1193 self.slots
1194 .iter()
1195 .take(count)
1196 .map(|(start, length)| match self.axis {
1197 Axis::Horizontal => Rectangle {
1198 x: bounds.x + start + length,
1199 width: HANDLE,
1200 ..bounds
1201 },
1202 Axis::Vertical => Rectangle {
1203 y: bounds.y + start + length,
1204 height: HANDLE,
1205 ..bounds
1206 },
1207 })
1208 .collect()
1209 }
1210
1211 fn handle_at(&self, bounds: Rectangle, point: Point) -> Option<usize> {
1212 self.lines(bounds)
1213 .into_iter()
1214 .position(|line| hit_area(line, self.axis).contains(point))
1215 }
1216}
1217
1218fn hit_area(line: Rectangle, axis: Axis) -> Rectangle {
1220 let grow = (HIT_AREA - HANDLE) / 2.0;
1221 match axis {
1222 Axis::Horizontal => Rectangle {
1223 x: line.x - grow,
1224 width: HIT_AREA,
1225 ..line
1226 },
1227 Axis::Vertical => Rectangle {
1228 y: line.y - grow,
1229 height: HIT_AREA,
1230 ..line
1231 },
1232 }
1233}
1234
1235fn pointer(event: &Input, cursor: mouse::Cursor) -> Option<Point> {
1236 match event {
1237 Input::Touch(
1238 touch::Event::FingerPressed { position, .. }
1239 | touch::Event::FingerMoved { position, .. },
1240 ) => Some(*position),
1241 _ => cursor.position(),
1242 }
1243}
1244
1245impl<Message> Widget<Message, Theme, Renderer> for ResizablePanel<'_, Message> {
1246 fn tag(&self) -> tree::Tag {
1247 tree::Tag::of::<Memory>()
1248 }
1249
1250 fn state(&self) -> tree::State {
1251 tree::State::new(Memory::default())
1252 }
1253
1254 fn size(&self) -> Size<Length> {
1255 Size::new(self.width, self.height)
1256 }
1257
1258 fn children(&self) -> Vec<Tree> {
1259 self.panes.iter().map(Tree::new).collect()
1260 }
1261
1262 fn diff(&self, tree: &mut Tree) {
1263 tree.diff_children(&self.panes);
1264 }
1265
1266 fn layout(
1267 &mut self,
1268 tree: &mut Tree,
1269 renderer: &Renderer,
1270 limits: &layout::Limits,
1271 ) -> layout::Node {
1272 let limits = limits.width(self.width).height(self.height);
1273 let max = limits.max();
1274 let axis = self.axis_at(max.width);
1275 let count = self.panes.len();
1276 let handles = count.saturating_sub(1) as f32 * HANDLE;
1277 let main = axis.main(max);
1278 let total = if main.is_finite() {
1279 (main - handles).max(0.0)
1280 } else {
1281 0.0
1282 };
1283 let cross = axis.cross(max);
1284 let sizes = self.state.resolve(total);
1285
1286 let mut start = 0.0_f32;
1287 let mut used = 0.0_f32;
1288 let mut widest = 0.0_f32;
1289 let mut nodes = Vec::with_capacity(count);
1290 let mut slots = Vec::with_capacity(count);
1291 for (index, (pane, tree)) in self.panes.iter_mut().zip(&mut tree.children).enumerate() {
1292 used += sizes.get(index).copied().unwrap_or(0.0);
1293 let end = used.round();
1294 let length = (end - start).max(0.0);
1295 let offset = start + index as f32 * HANDLE;
1296 let bounds = axis.size(length, cross);
1297 let node = pane
1298 .as_widget_mut()
1299 .layout(tree, renderer, &layout::Limits::new(Size::ZERO, bounds))
1300 .move_to(axis.point(offset));
1301 widest = widest.max(axis.cross(node.size()));
1302 nodes.push(node);
1303 slots.push((offset, length));
1304 start = end;
1305 }
1306
1307 let cross = if cross.is_finite() { cross } else { widest };
1308 let memory = tree.state.downcast_mut::<Memory>();
1309 memory.axis = axis;
1310 memory.total = total;
1311 memory.slots = slots;
1312 memory
1313 .handles
1314 .resize(count.saturating_sub(1), HandleFocus::default());
1315
1316 let size = limits.resolve(self.width, self.height, axis.size(total + handles, cross));
1317 layout::Node::with_children(size, nodes)
1318 }
1319
1320 fn operate(
1321 &mut self,
1322 tree: &mut Tree,
1323 layout: Layout<'_>,
1324 renderer: &Renderer,
1325 operation: &mut dyn Operation,
1326 ) {
1327 operation.container(None, layout.bounds());
1328 let Tree {
1329 state, children, ..
1330 } = tree;
1331 let memory = state.downcast_mut::<Memory>();
1332 let lines = memory.lines(layout.bounds());
1333 let id = self.id.as_ref();
1334 operation.traverse(&mut |operation| {
1335 for (index, (handle, line)) in memory.handles.iter_mut().zip(&lines).enumerate() {
1336 let id = id.filter(|_| index == 0);
1337 operation.focusable(id, *line, handle);
1338 }
1339 for ((pane, tree), layout) in self
1340 .panes
1341 .iter_mut()
1342 .zip(children.iter_mut())
1343 .zip(layout.children())
1344 {
1345 pane.as_widget_mut()
1346 .operate(tree, layout, renderer, operation);
1347 }
1348 });
1349 }
1350
1351 fn update(
1352 &mut self,
1353 tree: &mut Tree,
1354 event: &Input,
1355 layout: Layout<'_>,
1356 cursor: mouse::Cursor,
1357 renderer: &Renderer,
1358 clipboard: &mut dyn Clipboard,
1359 shell: &mut Shell<'_, Message>,
1360 viewport: &Rectangle,
1361 ) {
1362 if let Some(on_event) = &self.on_event {
1363 let memory = tree.state.downcast_mut::<Memory>();
1364 if handle_event(
1365 self.state,
1366 &self.keymap,
1367 on_event,
1368 memory,
1369 event,
1370 layout,
1371 cursor,
1372 shell,
1373 ) {
1374 return;
1375 }
1376 }
1377
1378 for ((pane, tree), layout) in self
1379 .panes
1380 .iter_mut()
1381 .zip(&mut tree.children)
1382 .zip(layout.children())
1383 {
1384 pane.as_widget_mut().update(
1385 tree, event, layout, cursor, renderer, clipboard, shell, viewport,
1386 );
1387 }
1388 }
1389
1390 fn mouse_interaction(
1391 &self,
1392 tree: &Tree,
1393 layout: Layout<'_>,
1394 cursor: mouse::Cursor,
1395 viewport: &Rectangle,
1396 renderer: &Renderer,
1397 ) -> mouse::Interaction {
1398 let memory = tree.state.downcast_ref::<Memory>();
1399 if self.on_event.is_some() {
1400 let over = cursor
1401 .position()
1402 .and_then(|point| memory.handle_at(layout.bounds(), point));
1403 if self.state.dragging().is_some() || over.is_some() {
1404 return memory.axis.cursor();
1405 }
1406 }
1407 self.panes
1408 .iter()
1409 .zip(&tree.children)
1410 .zip(layout.children())
1411 .map(|((pane, tree), layout)| {
1412 pane.as_widget()
1413 .mouse_interaction(tree, layout, cursor, viewport, renderer)
1414 })
1415 .max()
1416 .unwrap_or_default()
1417 }
1418
1419 fn draw(
1420 &self,
1421 tree: &Tree,
1422 renderer: &mut Renderer,
1423 theme: &Theme,
1424 style: &renderer::Style,
1425 layout: Layout<'_>,
1426 cursor: mouse::Cursor,
1427 viewport: &Rectangle,
1428 ) {
1429 use iced::advanced::Renderer as _;
1430
1431 let memory = tree.state.downcast_ref::<Memory>();
1432 let bounds = layout.bounds();
1433 for (index, ((pane, tree), layout)) in self
1434 .panes
1435 .iter()
1436 .zip(&tree.children)
1437 .zip(layout.children())
1438 .enumerate()
1439 {
1440 let Some(slot) = memory.slot(bounds, index) else {
1441 continue;
1442 };
1443 if slot.width < 1.0 || slot.height < 1.0 {
1444 continue;
1445 }
1446 let Some(clip) = slot.intersection(viewport) else {
1447 continue;
1448 };
1449 renderer.with_layer(clip, |renderer| {
1450 pane.as_widget()
1451 .draw(tree, renderer, theme, style, layout, cursor, &clip);
1452 });
1453 }
1454
1455 let tokens = Tokens::of(theme);
1456 let focused = memory.visible();
1457 let over = cursor
1458 .position()
1459 .and_then(|point| memory.handle_at(bounds, point));
1460 for (index, line) in memory.lines(bounds).into_iter().enumerate() {
1461 let status = if self.on_event.is_none() {
1462 HandleStatus::Disabled
1463 } else if self.state.dragging() == Some(index) {
1464 HandleStatus::Dragged
1465 } else if focused == Some(index) {
1466 HandleStatus::Focused
1467 } else if over == Some(index) {
1468 HandleStatus::Hovered
1469 } else {
1470 HandleStatus::Idle
1471 };
1472 draw_handle(
1473 renderer,
1474 line,
1475 memory.axis,
1476 handle_style(&tokens, status),
1477 self.grip,
1478 );
1479 }
1480 }
1481
1482 fn overlay<'b>(
1483 &'b mut self,
1484 tree: &'b mut Tree,
1485 layout: Layout<'b>,
1486 renderer: &Renderer,
1487 viewport: &Rectangle,
1488 translation: Vector,
1489 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
1490 overlay::from_children(
1491 &mut self.panes,
1492 tree,
1493 layout,
1494 renderer,
1495 viewport,
1496 translation,
1497 )
1498 }
1499}
1500
1501#[allow(clippy::too_many_arguments)]
1504fn handle_event<Message>(
1505 state: &State,
1506 keymap: &Keymap<Action>,
1507 on_event: &dyn Fn(Event) -> Message,
1508 memory: &mut Memory,
1509 event: &Input,
1510 layout: Layout<'_>,
1511 cursor: mouse::Cursor,
1512 shell: &mut Shell<'_, Message>,
1513) -> bool {
1514 if memory.total > 0.0 && (state.total() - memory.total).abs() > 0.5 {
1515 shell.publish(on_event(Event::Measure(memory.total)));
1516 }
1517 let axis = memory.axis;
1518
1519 match event {
1520 Input::Mouse(mouse::Event::CursorMoved { .. })
1521 | Input::Touch(touch::Event::FingerMoved { .. }) => {
1522 let Some(point) = pointer(event, cursor) else {
1523 return false;
1524 };
1525 if state.dragging().is_some() {
1526 shell.publish(on_event(Event::Drag(axis.along(point))));
1527 shell.capture_event();
1528 return true;
1529 }
1530 let hovered = memory.handle_at(layout.bounds(), point);
1531 if hovered != memory.hovered {
1532 memory.hovered = hovered;
1533 shell.request_redraw();
1534 }
1535 false
1536 }
1537 Input::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
1538 | Input::Touch(touch::Event::FingerPressed { .. }) => {
1539 let hit = pointer(event, cursor).and_then(|point| {
1540 memory
1541 .handle_at(layout.bounds(), point)
1542 .map(|handle| (handle, point))
1543 });
1544 let Some((handle, point)) = hit else {
1545 if memory.focused().is_some() {
1546 memory.focus(None);
1547 shell.request_redraw();
1548 }
1549 return false;
1550 };
1551 memory.focus(Some(handle));
1552 shell.publish(on_event(Event::DragStart {
1553 handle,
1554 position: axis.along(point),
1555 }));
1556 shell.capture_event();
1557 true
1558 }
1559 Input::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
1560 | Input::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => {
1561 if state.dragging().is_none() {
1562 return false;
1563 }
1564 shell.publish(on_event(Event::DragEnd));
1565 shell.capture_event();
1566 true
1567 }
1568 Input::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) => {
1569 let Some(handle) = memory.focused() else {
1570 return false;
1571 };
1572 let Some(action) = keymap.resolve(key, *modifiers) else {
1573 return false;
1574 };
1575 if action == Action::Release {
1576 memory.focus(None);
1577 } else {
1578 memory.show_focus();
1579 if let Some(event) = action.event(state, handle) {
1580 shell.publish(on_event(event));
1581 }
1582 }
1583 shell.request_redraw();
1584 shell.capture_event();
1585 true
1586 }
1587 _ => false,
1588 }
1589}
1590
1591fn draw_handle(
1592 renderer: &mut Renderer,
1593 line: Rectangle,
1594 axis: Axis,
1595 style: HandleStyle,
1596 grip: bool,
1597) {
1598 use iced::advanced::Renderer as _;
1599
1600 let quad = |bounds: Rectangle, radius: f32, border: Option<Color>| renderer::Quad {
1601 bounds,
1602 border: Border {
1603 color: border.unwrap_or(Color::TRANSPARENT),
1604 width: if border.is_some() { 1.0 } else { 0.0 },
1605 radius: radius.into(),
1606 },
1607 shadow: Shadow::default(),
1608 snap: true,
1609 };
1610
1611 renderer.fill_quad(quad(line, 0.0, None), Background::Color(style.line));
1612
1613 if let Some(colour) = style.highlight {
1614 let grow = (HIGHLIGHT - HANDLE) / 2.0;
1615 let bar = match axis {
1616 Axis::Horizontal => Rectangle {
1617 x: line.x - grow,
1618 width: HIGHLIGHT,
1619 ..line
1620 },
1621 Axis::Vertical => Rectangle {
1622 y: line.y - grow,
1623 height: HIGHLIGHT,
1624 ..line
1625 },
1626 };
1627 renderer.fill_quad(quad(bar, 0.0, None), Background::Color(colour));
1628 }
1629
1630 if !grip {
1631 return;
1632 }
1633 let size = match axis {
1634 Axis::Horizontal => GRIP,
1635 Axis::Vertical => Size::new(GRIP.height, GRIP.width),
1636 };
1637 let centre = line.center();
1638 let bounds = Rectangle::new(
1639 Point::new(
1640 (centre.x - size.width / 2.0).round(),
1641 (centre.y - size.height / 2.0).round(),
1642 ),
1643 size,
1644 );
1645 renderer.fill_quad(
1646 quad(bounds, radius::SM, Some(style.grip_border)),
1647 Background::Color(style.grip),
1648 );
1649
1650 let dot = 2.0;
1652 for column in [-1.0_f32, 1.0] {
1653 for row in [-1.0_f32, 0.0, 1.0] {
1654 let (dx, dy) = match axis {
1655 Axis::Horizontal => (column * 2.0, row * 4.0),
1656 Axis::Vertical => (row * 4.0, column * 2.0),
1657 };
1658 let position = Point::new(
1659 (centre.x + dx - dot / 2.0).round(),
1660 (centre.y + dy - dot / 2.0).round(),
1661 );
1662 renderer.fill_quad(
1663 quad(Rectangle::new(position, Size::new(dot, dot)), 1.0, None),
1664 Background::Color(style.grip_dots),
1665 );
1666 }
1667 }
1668}
1669
1670#[cfg(test)]
1671mod tests {
1672 use super::*;
1673 use crate::theme::{dark, light};
1674
1675 const TOTAL: f32 = 1000.0;
1676
1677 fn close(a: f32, b: f32) -> bool {
1678 (a - b).abs() < 0.05
1679 }
1680
1681 fn assert_sizes(actual: &[f32], expected: &[f32]) {
1682 assert_eq!(actual.len(), expected.len(), "{actual:?}");
1683 for (a, b) in actual.iter().zip(expected) {
1684 assert!(close(*a, *b), "{actual:?} != {expected:?}");
1685 }
1686 }
1687
1688 fn measured(panels: impl IntoIterator<Item = Panel>) -> State {
1689 let mut state = State::new(panels);
1690 let _ = state.update(Event::Measure(TOTAL));
1691 state
1692 }
1693
1694 fn pixels(state: &State) -> Vec<f32> {
1695 state.resolve(state.total())
1696 }
1697
1698 fn sidebar_and_content() -> [Panel; 2] {
1699 [
1700 panel(Extent::Fraction(0.3))
1701 .min(Extent::Pixels(200.0))
1702 .max(Extent::Fraction(0.5))
1703 .collapsible(true),
1704 panel(Extent::Fraction(0.7)).min(Extent::Pixels(300.0)),
1705 ]
1706 }
1707
1708 #[test]
1709 fn panel_defaults() {
1710 let panel = panel(Extent::Fraction(0.5));
1711 assert_eq!(panel.min, Extent::Pixels(0.0));
1712 assert_eq!(panel.max, Extent::Fraction(1.0));
1713 assert!(!panel.collapsible);
1714 assert_eq!(panel.collapsed_size, Extent::Pixels(0.0));
1715 assert_eq!(panel.collapse_below, None);
1716 }
1717
1718 #[test]
1719 fn extents_resolve_to_pixels() {
1720 assert_eq!(Extent::Fraction(0.25).resolve(800.0), 200.0);
1721 assert_eq!(Extent::Pixels(120.0).resolve(800.0), 120.0);
1722 assert_eq!(Extent::Pixels(-5.0).resolve(800.0), 0.0);
1723 }
1724
1725 #[test]
1726 fn limits_keep_max_above_min_and_threshold_between() {
1727 let limits = panel(Extent::Fraction(0.5))
1728 .min(Extent::Pixels(300.0))
1729 .max(Extent::Pixels(100.0))
1730 .collapsible(true)
1731 .collapsed_size(Extent::Pixels(40.0))
1732 .limits(TOTAL);
1733 assert_eq!(limits.min, 300.0);
1734 assert_eq!(limits.max, 300.0);
1735 assert_eq!(limits.collapsed, 40.0);
1736 assert_eq!(limits.threshold, 170.0);
1737
1738 let custom = panel(Extent::Fraction(0.5))
1739 .min(Extent::Pixels(200.0))
1740 .collapsible(true)
1741 .collapse_below(Extent::Pixels(500.0))
1742 .limits(TOTAL);
1743 assert_eq!(custom.threshold, 200.0, "never above the minimum");
1744 }
1745
1746 #[test]
1747 fn settle_clamps_and_snaps_at_the_threshold() {
1748 let plain = panel(Extent::Fraction(0.5))
1749 .min(Extent::Pixels(100.0))
1750 .max(Extent::Pixels(400.0));
1751 assert_eq!(plain.settle(50.0, TOTAL), 100.0);
1752 assert_eq!(plain.settle(250.0, TOTAL), 250.0);
1753 assert_eq!(plain.settle(900.0, TOTAL), 400.0);
1754
1755 let collapsible = plain.collapsible(true);
1756 assert_eq!(
1757 collapsible.settle(60.0, TOTAL),
1758 100.0,
1759 "above the threshold"
1760 );
1761 assert_eq!(collapsible.settle(49.0, TOTAL), 0.0, "below the threshold");
1762 assert!(collapsible.is_collapsed_at(0.0, TOTAL));
1763 assert!(!plain.is_collapsed_at(0.0, TOTAL));
1764 }
1765
1766 #[test]
1767 fn defaults_share_what_pixel_panels_leave() {
1768 let panels = [
1769 panel(Extent::Pixels(250.0)),
1770 panel(Extent::Fraction(1.0)),
1771 panel(Extent::Fraction(1.0)),
1772 ];
1773 assert_sizes(&defaults(&panels, TOTAL), &[250.0, 375.0, 375.0]);
1774 }
1775
1776 #[test]
1777 fn defaults_respect_limits_and_initial_collapse() {
1778 let panels = [
1779 panel(Extent::Fraction(0.1)).min(Extent::Pixels(200.0)),
1780 panel(Extent::Fraction(0.9)),
1781 ];
1782 assert_sizes(&defaults(&panels, TOTAL), &[200.0, 800.0]);
1783
1784 let collapsed = [
1785 panel(Extent::Fraction(0.3))
1786 .collapsible(true)
1787 .collapsed_size(Extent::Pixels(48.0))
1788 .min(Extent::Pixels(200.0))
1789 .collapsed(true),
1790 panel(Extent::Fraction(0.7)),
1791 ];
1792 assert_sizes(&defaults(&collapsed, TOTAL), &[48.0, 952.0]);
1793 }
1794
1795 #[test]
1796 fn fit_always_adds_up() {
1797 let panels = sidebar_and_content();
1798 for total in [1200.0, 600.0, 450.0, 320.0, 100.0] {
1799 let sizes = fit(
1800 &panels,
1801 &[false, false],
1802 vec![0.3 * total, 0.7 * total],
1803 total,
1804 );
1805 let sum: f32 = sizes.iter().sum();
1806 assert!(close(sum, total), "{total}: {sizes:?}");
1807 }
1808 }
1809
1810 #[test]
1811 fn fit_collapses_side_panels_when_minimums_do_not_fit() {
1812 let panels = sidebar_and_content();
1813 let sizes = fit(&panels, &[false, false], vec![108.0, 252.0], 360.0);
1815 assert_sizes(&sizes, &[0.0, 360.0]);
1816 }
1817
1818 #[test]
1819 fn fit_scales_as_a_last_resort() {
1820 let panels = [
1821 panel(Extent::Fraction(0.5)).min(Extent::Pixels(300.0)),
1822 panel(Extent::Fraction(0.5)).min(Extent::Pixels(300.0)),
1823 ];
1824 let sizes = fit(&panels, &[false, false], vec![200.0, 200.0], 400.0);
1825 assert_sizes(&sizes, &[200.0, 200.0]);
1826 }
1827
1828 #[test]
1829 fn state_starts_from_defaults_once_measured() {
1830 let state = State::new(sidebar_and_content());
1831 assert_eq!(state.total(), 0.0);
1832 assert_sizes(&state.sizes(), &[0.3, 0.7]);
1833 assert_sizes(&state.resolve(TOTAL), &[300.0, 700.0]);
1834
1835 let state = measured(sidebar_and_content());
1836 assert_eq!(state.total(), TOTAL);
1837 assert_sizes(&state.sizes(), &[0.3, 0.7]);
1838 }
1839
1840 #[test]
1841 fn unmeasured_state_ignores_changes() {
1842 let mut state = State::new(sidebar_and_content());
1843 assert_eq!(
1844 state.update(Event::Resize {
1845 handle: 0,
1846 delta: 50.0
1847 }),
1848 None
1849 );
1850 assert_eq!(state.update(Event::Collapse(0)), None);
1851 let _ = state.update(Event::Measure(-1.0));
1852 let _ = state.update(Event::Measure(f32::INFINITY));
1853 assert_eq!(state.total(), 0.0);
1854 }
1855
1856 #[test]
1857 fn saved_sizes_round_trip_and_bad_ones_are_ignored() {
1858 let state = State::new(sidebar_and_content()).with_sizes([2.0, 6.0]);
1859 assert_sizes(&state.sizes(), &[0.25, 0.75]);
1860
1861 let wrong_length = State::new(sidebar_and_content()).with_sizes([1.0]);
1862 assert_sizes(&wrong_length.sizes(), &[0.3, 0.7]);
1863 let empty = State::new(sidebar_and_content()).with_sizes([0.0, 0.0]);
1864 assert_sizes(&empty.sizes(), &[0.3, 0.7]);
1865
1866 let collapsed = State::new(sidebar_and_content()).with_sizes([0.0, 1.0]);
1867 assert!(collapsed.is_collapsed(0));
1868 }
1869
1870 #[test]
1871 fn dragging_moves_the_handle_from_where_it_started() {
1872 let mut state = measured(sidebar_and_content());
1873 let _ = state.update(Event::DragStart {
1874 handle: 0,
1875 position: 300.0,
1876 });
1877 assert_eq!(state.dragging(), Some(0));
1878 assert_eq!(state.update(Event::Drag(340.0)), Some(Output::Resized));
1879 assert_sizes(&pixels(&state), &[340.0, 660.0]);
1880 assert_eq!(state.update(Event::Drag(320.0)), Some(Output::Resized));
1881 assert_sizes(&pixels(&state), &[320.0, 680.0]);
1882 let _ = state.update(Event::DragEnd);
1883 assert_eq!(state.dragging(), None);
1884 assert_eq!(state.update(Event::Drag(500.0)), None);
1885 }
1886
1887 #[test]
1888 fn drags_clamp_to_min_and_max() {
1889 let mut state = measured(sidebar_and_content());
1890 let _ = state.update(Event::DragStart {
1891 handle: 0,
1892 position: 300.0,
1893 });
1894 let _ = state.update(Event::Drag(900.0));
1895 assert_sizes(&pixels(&state), &[500.0, 500.0]);
1896 let _ = state.update(Event::Drag(160.0));
1897 assert_sizes(&pixels(&state), &[200.0, 800.0]);
1898 }
1899
1900 #[test]
1901 fn collapsible_panels_snap_shut_below_the_threshold_and_open_past_it() {
1902 let mut state = measured(sidebar_and_content());
1903 let _ = state.update(Event::DragStart {
1904 handle: 0,
1905 position: 300.0,
1906 });
1907 let _ = state.update(Event::Drag(110.0));
1909 assert_sizes(&pixels(&state), &[200.0, 800.0]);
1910 assert_eq!(state.update(Event::Drag(90.0)), Some(Output::Collapsed(0)));
1911 assert!(state.is_collapsed(0));
1912 assert_sizes(&pixels(&state), &[0.0, 1000.0]);
1913 assert_eq!(state.update(Event::Drag(120.0)), Some(Output::Expanded(0)));
1914 assert_sizes(&pixels(&state), &[200.0, 800.0]);
1915 let _ = state.update(Event::DragEnd);
1916
1917 let _ = state.update(Event::Collapse(0));
1918 let _ = state.update(Event::DragStart {
1919 handle: 0,
1920 position: 0.0,
1921 });
1922 assert_eq!(
1923 state.update(Event::Drag(60.0)),
1924 None,
1925 "still below the threshold"
1926 );
1927 assert!(state.is_collapsed(0));
1928 assert_eq!(state.update(Event::Drag(101.0)), Some(Output::Expanded(0)));
1929 assert_sizes(&pixels(&state), &[200.0, 800.0]);
1930 }
1931
1932 #[test]
1933 fn shrinking_carries_on_past_a_panel_at_its_minimum() {
1934 let mut state = measured([
1935 panel(Extent::Fraction(0.4)),
1936 panel(Extent::Fraction(0.3)).min(Extent::Pixels(200.0)),
1937 panel(Extent::Fraction(0.3)).min(Extent::Pixels(100.0)),
1938 ]);
1939 let _ = state.update(Event::Resize {
1940 handle: 0,
1941 delta: 250.0,
1942 });
1943 assert_sizes(&pixels(&state), &[650.0, 200.0, 150.0]);
1944 let _ = state.update(Event::Resize {
1945 handle: 0,
1946 delta: 500.0,
1947 });
1948 assert_sizes(&pixels(&state), &[700.0, 200.0, 100.0]);
1949 }
1950
1951 #[test]
1952 fn growth_past_a_maximum_passes_outwards() {
1953 let origin = [200.0, 300.0, 500.0];
1954 let panels = [
1955 panel(Extent::Fraction(0.2)),
1956 panel(Extent::Fraction(0.3)).max(Extent::Pixels(350.0)),
1957 panel(Extent::Fraction(0.5)),
1958 ];
1959 let sizes = resize(&panels, &origin, 1, 100.0, TOTAL);
1960 assert_sizes(&sizes, &[250.0, 350.0, 400.0]);
1961 }
1962
1963 #[test]
1964 fn resize_ignores_bad_handles_and_zero_moves() {
1965 let panels = sidebar_and_content();
1966 let origin = [300.0, 700.0];
1967 assert_eq!(resize(&panels, &origin, 1, 50.0, TOTAL), origin);
1968 assert_eq!(resize(&panels, &origin, 0, 0.0, TOTAL), origin);
1969 assert_eq!(resize(&panels, &[300.0], 0, 50.0, TOTAL), [300.0]);
1970 }
1971
1972 #[test]
1973 fn toggle_collapses_and_restores_the_previous_size() {
1974 let mut state = measured(sidebar_and_content());
1975 let _ = state.update(Event::Resize {
1976 handle: 0,
1977 delta: 100.0,
1978 });
1979 assert_eq!(state.update(Event::Toggle(0)), Some(Output::Collapsed(0)));
1980 assert_sizes(&pixels(&state), &[0.0, 1000.0]);
1981 assert_eq!(state.update(Event::Collapse(0)), None);
1982 assert_eq!(state.update(Event::Toggle(0)), Some(Output::Expanded(0)));
1983 assert_sizes(&pixels(&state), &[400.0, 600.0]);
1984 assert_eq!(state.update(Event::Expand(0)), None);
1985 }
1986
1987 #[test]
1988 fn the_last_panel_collapses_through_the_handle_before_it() {
1989 let mut state = measured([
1990 panel(Extent::Fraction(0.6)),
1991 panel(Extent::Fraction(0.4))
1992 .collapsible(true)
1993 .collapsed_size(Extent::Pixels(40.0))
1994 .min(Extent::Pixels(200.0)),
1995 ]);
1996 assert_eq!(state.update(Event::Collapse(1)), Some(Output::Collapsed(1)));
1997 assert_sizes(&pixels(&state), &[960.0, 40.0]);
1998 assert_eq!(state.update(Event::Expand(1)), Some(Output::Expanded(1)));
1999 assert_sizes(&pixels(&state), &[600.0, 400.0]);
2000 }
2001
2002 #[test]
2003 fn panels_that_cannot_collapse_ignore_it() {
2004 let mut state = measured(sidebar_and_content());
2005 assert_eq!(state.update(Event::Collapse(1)), None);
2006 assert_eq!(state.update(Event::Toggle(7)), None);
2007 }
2008
2009 #[test]
2010 fn reset_puts_the_defaults_back() {
2011 let mut state = measured(sidebar_and_content());
2012 let _ = state.update(Event::Collapse(0));
2013 assert_eq!(state.update(Event::Reset), Some(Output::Resized));
2014 assert!(!state.is_collapsed(0));
2015 assert_sizes(&pixels(&state), &[300.0, 700.0]);
2016 assert_eq!(state.update(Event::Reset), None);
2017 }
2018
2019 #[test]
2020 fn keyboard_steps_follow_the_step_sizes() {
2021 let state = measured(sidebar_and_content());
2022 let resize = |action: Action| action.event(&state, 0);
2023 assert_eq!(
2024 resize(Action::Increase),
2025 Some(Event::Resize {
2026 handle: 0,
2027 delta: 50.0
2028 })
2029 );
2030 assert_eq!(
2031 resize(Action::Decrease),
2032 Some(Event::Resize {
2033 handle: 0,
2034 delta: -50.0
2035 })
2036 );
2037 assert_eq!(
2038 resize(Action::IncreaseMore),
2039 Some(Event::Resize {
2040 handle: 0,
2041 delta: 200.0
2042 })
2043 );
2044 assert_eq!(
2045 resize(Action::DecreaseMore),
2046 Some(Event::Resize {
2047 handle: 0,
2048 delta: -200.0
2049 })
2050 );
2051
2052 let pixels = measured(sidebar_and_content())
2053 .step(Extent::Pixels(10.0))
2054 .large_step(Extent::Pixels(40.0));
2055 assert_eq!(
2056 Action::Increase.event(&pixels, 0),
2057 Some(Event::Resize {
2058 handle: 0,
2059 delta: 10.0
2060 })
2061 );
2062 assert_eq!(
2063 Action::DecreaseMore.event(&pixels, 0),
2064 Some(Event::Resize {
2065 handle: 0,
2066 delta: -40.0
2067 })
2068 );
2069 }
2070
2071 #[test]
2072 fn home_and_end_go_to_min_and_max() {
2073 let mut state = measured(sidebar_and_content());
2074 let home = Action::Min.event(&state, 0);
2075 assert_eq!(
2076 home,
2077 Some(Event::Resize {
2078 handle: 0,
2079 delta: -100.0
2080 })
2081 );
2082 if let Some(event) = home {
2083 let _ = state.update(event);
2084 }
2085 assert_sizes(&pixels(&state), &[200.0, 800.0]);
2086 if let Some(event) = Action::Max.event(&state, 0) {
2087 let _ = state.update(event);
2088 }
2089 assert_sizes(&pixels(&state), &[500.0, 500.0]);
2090 }
2091
2092 #[test]
2093 fn toggle_and_release_actions() {
2094 let state = measured(sidebar_and_content());
2095 assert_eq!(Action::Toggle.event(&state, 0), Some(Event::Toggle(0)));
2096 assert_eq!(Action::Release.event(&state, 0), None);
2097 assert_eq!(Action::Increase.event(&state, 1), None, "no such handle");
2098
2099 let neither = measured([panel(Extent::Fraction(0.5)), panel(Extent::Fraction(0.5))]);
2100 assert_eq!(Action::Toggle.event(&neither, 0), None);
2101 let after = measured([
2102 panel(Extent::Fraction(0.5)),
2103 panel(Extent::Fraction(0.5)).collapsible(true),
2104 ]);
2105 assert_eq!(Action::Toggle.event(&after, 0), Some(Event::Toggle(1)));
2106 }
2107
2108 fn press(keymap: &Keymap<Action>, chord: &str) -> Option<Action> {
2109 let chord: Chord = chord.parse().unwrap();
2110 keymap.resolve(chord.key(), chord.modifiers())
2111 }
2112
2113 #[test]
2114 fn default_keymap_covers_every_action() {
2115 let keymap = default_keymap();
2116 assert_eq!(press(&keymap, "ArrowLeft"), Some(Action::Decrease));
2117 assert_eq!(press(&keymap, "ArrowUp"), Some(Action::Decrease));
2118 assert_eq!(press(&keymap, "ArrowRight"), Some(Action::Increase));
2119 assert_eq!(press(&keymap, "ArrowDown"), Some(Action::Increase));
2120 assert_eq!(
2121 press(&keymap, "Shift+ArrowLeft"),
2122 Some(Action::DecreaseMore)
2123 );
2124 assert_eq!(
2125 press(&keymap, "Shift+ArrowDown"),
2126 Some(Action::IncreaseMore)
2127 );
2128 assert_eq!(press(&keymap, "Home"), Some(Action::Min));
2129 assert_eq!(press(&keymap, "End"), Some(Action::Max));
2130 assert_eq!(press(&keymap, "Enter"), Some(Action::Toggle));
2131 assert_eq!(press(&keymap, "Escape"), Some(Action::Release));
2132 for action in <Action as keys::Action>::ALL {
2133 assert!(!keymap.chords(action).is_empty(), "{action:?}");
2134 }
2135 assert_eq!(Keymap::<Action>::defaults(), keymap);
2136 }
2137
2138 #[test]
2139 fn keymap_overrides_and_unbinding() {
2140 let keymap = default_keymap()
2141 .unbind(&"Enter".parse().unwrap())
2142 .bind("Space".parse().unwrap(), Action::Toggle);
2143 assert_eq!(press(&keymap, "Enter"), None);
2144 assert_eq!(press(&keymap, "Space"), Some(Action::Toggle));
2145 assert!(keymap.clear().is_empty());
2146 }
2147
2148 #[test]
2149 fn stacking_only_turns_narrow_horizontal_groups() {
2150 let state = State::new(sidebar_and_content());
2151 let group: ResizablePanel<'_, ()> = resizable_panel(&state, []).stack_below(480.0);
2152 assert_eq!(group.axis_at(360.0), Axis::Vertical);
2153 assert_eq!(group.axis_at(720.0), Axis::Horizontal);
2154 let plain: ResizablePanel<'_, ()> = resizable_panel(&state, []);
2155 assert_eq!(plain.axis_at(360.0), Axis::Horizontal);
2156 let vertical: ResizablePanel<'_, ()> = resizable_panel(&state, [])
2157 .axis(Axis::Vertical)
2158 .stack_below(480.0);
2159 assert_eq!(vertical.axis_at(360.0), Axis::Vertical);
2160 }
2161
2162 #[test]
2163 fn builder_defaults() {
2164 let state = State::new(sidebar_and_content());
2165 let group: ResizablePanel<'_, ()> = resizable_panel(&state, []);
2166 assert_eq!(group.axis, Axis::Horizontal);
2167 assert!(!group.grip);
2168 assert_eq!(group.width, Length::Fill);
2169 assert_eq!(group.height, Length::Fill);
2170 assert!(group.on_event.is_none());
2171 assert_eq!(group.keymap, default_keymap());
2172 }
2173
2174 #[test]
2175 fn axes_pick_their_coordinate_and_cursor() {
2176 let point = Point::new(3.0, 7.0);
2177 assert_eq!(Axis::Horizontal.along(point), 3.0);
2178 assert_eq!(Axis::Vertical.along(point), 7.0);
2179 assert_eq!(
2180 Axis::Horizontal.cursor(),
2181 mouse::Interaction::ResizingColumn
2182 );
2183 assert_eq!(Axis::Vertical.cursor(), mouse::Interaction::ResizingRow);
2184 }
2185
2186 #[test]
2187 fn a_press_focuses_quietly_and_keys_or_operations_show_it() {
2188 let mut memory = Memory {
2189 handles: vec![HandleFocus::default(); 2],
2190 ..Memory::default()
2191 };
2192 memory.focus(Some(1));
2193 assert_eq!(memory.focused(), Some(1));
2194 assert_eq!(memory.visible(), None);
2195 memory.show_focus();
2196 assert_eq!(memory.visible(), Some(1));
2197 memory.focus(None);
2198 assert_eq!(memory.focused(), None);
2199
2200 memory.handles[0].focus();
2201 assert_eq!(memory.visible(), Some(0), "focus operations show it");
2202 memory.handles[0].unfocus();
2203 assert_eq!(memory.focused(), None);
2204 }
2205
2206 #[test]
2207 fn saved_sizes_below_the_threshold_restore_collapsed() {
2208 let panels = [
2209 panel(Extent::Pixels(240.0))
2210 .min(Extent::Pixels(180.0))
2211 .collapsible(true)
2212 .collapsed_size(Extent::Pixels(48.0)),
2213 panel(Extent::Fraction(1.0)),
2214 ];
2215 let mut state = State::new(panels).with_sizes([0.048, 0.952]);
2216 let _ = state.update(Event::Measure(TOTAL));
2217 assert!(state.is_collapsed(0));
2218 assert_sizes(&pixels(&state), &[48.0, 952.0]);
2219
2220 let _ = state.update(Event::Measure(2000.0));
2222 assert_sizes(&state.resolve(2000.0), &[48.0, 1952.0]);
2223 assert_eq!(state.update(Event::Expand(0)), Some(Output::Expanded(0)));
2224 assert_sizes(&state.resolve(2000.0), &[240.0, 1760.0]);
2225 }
2226
2227 #[test]
2228 fn handles_highlight_on_hover_drag_and_focus() {
2229 for theme in [light(), dark()] {
2230 let tokens = Tokens::of(&theme);
2231 let idle = handle_style(&tokens, HandleStatus::Idle);
2232 assert_eq!(idle.line, tokens.border);
2233 assert_eq!(idle.highlight, None);
2234 assert_eq!(
2235 handle_style(&tokens, HandleStatus::Disabled).highlight,
2236 None
2237 );
2238 assert_eq!(
2239 handle_style(&tokens, HandleStatus::Hovered).highlight,
2240 Some(tokens.border)
2241 );
2242 for status in [HandleStatus::Dragged, HandleStatus::Focused] {
2243 let style = handle_style(&tokens, status);
2244 assert_eq!(style.highlight, Some(tokens.ring));
2245 assert_eq!(style.grip_border, tokens.ring);
2246 }
2247 }
2248 }
2249}