1use frus_core::{Rect, Scene, SizeClass};
6use frus_layout::{FlexDirection, Style};
7
8use crate::flex::Flex;
9use crate::interaction::Status;
10use crate::theme::Theme;
11use crate::widget::Widget;
12
13pub struct TwoPane<Msg = crate::callback::Callback> {
15 class: SizeClass,
16 ratio: f32,
17 show_detail: bool,
18 list: Option<Box<dyn Widget<Msg>>>,
19 row: bool,
20 children: Vec<Box<dyn Widget<Msg>>>,
21}
22
23impl<Msg: Clone + 'static> TwoPane<Msg> {
24 pub fn new(class: SizeClass) -> Self {
26 Self {
27 class,
28 ratio: 0.38,
29 show_detail: false,
30 list: None,
31 row: false,
32 children: Vec::new(),
33 }
34 }
35
36 pub fn ratio(mut self, ratio: f32) -> Self {
39 self.ratio = ratio.clamp(0.1, 0.9);
40 self
41 }
42
43 pub fn show_detail(mut self, show: bool) -> Self {
46 self.show_detail = show;
47 self
48 }
49
50 pub fn list(mut self, list: impl Widget<Msg> + 'static) -> Self {
52 self.list = Some(Box::new(list));
53 self
54 }
55
56 pub fn detail(mut self, detail: impl Widget<Msg> + 'static) -> Self {
58 let detail: Box<dyn Widget<Msg>> = Box::new(detail);
59 if self.class == SizeClass::Expanded {
60 let list = self
62 .list
63 .take()
64 .expect("list() before detail() in side-by-side mode");
65 self.children = vec![
66 Box::new(Flex::column().flex(self.ratio).child(list)),
67 Box::new(Flex::column().flex(1.0 - self.ratio).child(detail)),
68 ];
69 self.row = true;
70 } else {
71 let single = if self.show_detail {
73 detail
74 } else {
75 self.list
76 .take()
77 .expect("single-pane mode needs list() or show_detail")
78 };
79 self.children = vec![Box::new(Flex::column().flex(1.0).child(single))];
80 self.row = false;
81 }
82 self
83 }
84}
85
86impl<Msg: Clone> Widget<Msg> for TwoPane<Msg> {
87 fn style(&self) -> Style {
92 Style {
93 flex_direction: if self.row {
94 FlexDirection::Row
95 } else {
96 FlexDirection::Column
97 },
98 ..Default::default()
99 }
100 }
101
102 fn fill_axes(&self, _theme: &Theme) -> crate::widget::FillAxes {
104 crate::widget::FillAxes::BOTH
105 }
106
107 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
108 &self.children
109 }
110
111 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
112
113 fn on_click(&self) -> Option<Msg> {
114 None
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use crate::Container;
122
123 #[test]
124 fn expanded_shows_both_panes_side_by_side() {
125 let tp = TwoPane::<()>::new(SizeClass::Expanded)
126 .ratio(0.4)
127 .list(Container::new())
128 .detail(Container::new());
129 assert_eq!(Widget::<()>::style(&tp).flex_direction, FlexDirection::Row);
130 assert_eq!(Widget::<()>::children(&tp).len(), 2);
131 let panes = Widget::<()>::children(&tp);
133 assert_eq!(Widget::<()>::style(&*panes[0]).flex_grow, 0.4);
134 assert!((Widget::<()>::style(&*panes[1]).flex_grow - 0.6).abs() < 1e-6);
135 }
136
137 #[test]
138 fn compact_shows_a_single_pane() {
139 let list_only = TwoPane::<()>::new(SizeClass::Compact)
140 .list(Container::new())
141 .detail(Container::new());
142 assert_eq!(
143 Widget::<()>::style(&list_only).flex_direction,
144 FlexDirection::Column
145 );
146 assert_eq!(Widget::<()>::children(&list_only).len(), 1);
147
148 let detail_shown = TwoPane::<()>::new(SizeClass::Compact)
150 .show_detail(true)
151 .list(Container::new())
152 .detail(Container::new());
153 assert_eq!(Widget::<()>::children(&detail_shown).len(), 1);
154 }
155}