rosace_widgets/tree/
tabs.rs1use std::sync::Arc;
11use rosace_core::types::{Point, Rect, Size};
12use rosace_render::Color;
13use super::{BoxedWidget, Children, LayoutCtx, PaintCtx, Widget};
14use super::tab::{Tab, TabBar};
15use super::scroll_view::{ScrollAxis, ScrollView};
16
17type OnChange = Arc<dyn Fn(usize) + Send + Sync>;
18
19pub struct TabView {
23 children: Vec<BoxedWidget>,
24 selected: usize,
25}
26
27impl TabView {
28 pub fn new(children: Vec<BoxedWidget>, selected: usize) -> Self {
29 Self { children, selected }
30 }
31}
32
33impl Widget for TabView {
34 fn children(&self) -> Children<'_> {
35 match self.children.get(self.selected) {
36 Some(c) => Children::One(&**c),
37 None => Children::None,
38 }
39 }
40}
41
42pub struct Tabs {
55 labels: Vec<String>,
56 contents: Vec<BoxedWidget>,
57 selected: usize,
58 bar_height: f32,
59 on_change: Option<OnChange>,
60 background: Option<Color>,
62 active: Option<Color>,
63 inactive: Option<Color>,
64 indicator: Option<Color>,
65 border: Option<Color>,
66 font_size: Option<f32>,
67 animated: bool,
68 scrollable: bool,
71}
72
73impl Tabs {
74 pub fn new(selected: usize, on_change: impl Fn(usize) + Send + Sync + 'static) -> Self {
75 Self {
76 labels: Vec::new(),
77 contents: Vec::new(),
78 selected,
79 bar_height: 40.0,
80 on_change: Some(Arc::new(on_change)),
81 background: None, active: None, inactive: None, indicator: None, border: None, font_size: None, animated: true,
82 scrollable: false,
83 }
84 }
85 pub fn readonly(selected: usize) -> Self {
87 Self {
88 labels: Vec::new(), contents: Vec::new(), selected, bar_height: 40.0, on_change: None,
89 background: None, active: None, inactive: None, indicator: None, border: None, font_size: None, animated: true,
90 scrollable: false,
91 }
92 }
93 pub fn tab(mut self, label: impl Into<String>, content: impl Widget + 'static) -> Self {
94 self.labels.push(label.into());
95 self.contents.push(Box::new(content));
96 self
97 }
98 pub fn bar_height(mut self, h: f32) -> Self { self.bar_height = h; self }
99 pub fn background(mut self, c: Color) -> Self { self.background = Some(c); self }
101 pub fn active_color(mut self, c: Color) -> Self { self.active = Some(c); self }
103 pub fn inactive_color(mut self, c: Color) -> Self { self.inactive = Some(c); self }
105 pub fn indicator_color(mut self, c: Color) -> Self { self.indicator = Some(c); self }
107 pub fn border_color(mut self, c: Color) -> Self { self.border = Some(c); self }
109 pub fn font_size(mut self, s: f32) -> Self { self.font_size = Some(s); self }
110 pub fn animated(mut self, on: bool) -> Self { self.animated = on; self }
112 pub fn scrollable(mut self, on: bool) -> Self { self.scrollable = on; self }
116}
117
118impl Widget for Tabs {
119 fn layout(&self, ctx: &LayoutCtx) -> Size {
120 let w = ctx.constraints.max_width_f32();
121 let h = ctx.constraints.max_height_f32();
122 let w = if w.is_finite() { w } else { 0.0 };
123 let h = if h.is_finite() { h } else { self.bar_height };
124 ctx.constraints.constrain(Size { width: w, height: h })
125 }
126
127 fn paint(&self, ctx: &mut PaintCtx) {
128 let r = ctx.rect;
129 let bar_rect = Rect { origin: r.origin, size: Size { width: r.size.width, height: self.bar_height } };
130 let content_rect = Rect {
131 origin: Point { x: r.origin.x, y: r.origin.y + self.bar_height },
132 size: Size { width: r.size.width, height: (r.size.height - self.bar_height).max(0.0) },
133 };
134
135 let mut bar = TabBar::new().selected(self.selected).height(self.bar_height).animated(self.animated);
137 for label in &self.labels {
138 bar = bar.tab(Tab::new(label));
139 }
140 if let Some(c) = self.background { bar = bar.background(c); }
141 if let Some(c) = self.active { bar = bar.active_color(c); }
142 if let Some(c) = self.inactive { bar = bar.inactive_color(c); }
143 if let Some(c) = self.indicator { bar = bar.indicator_color(c); }
144 if let Some(c) = self.border { bar = bar.border_color(c); }
145 if let Some(s) = self.font_size { bar = bar.font_size(s); }
146 if let Some(cb) = &self.on_change {
147 let cb = cb.clone();
148 bar = bar.on_change(move |i| cb(i));
149 }
150 if self.scrollable {
151 bar = bar.scrollable(true);
152 ScrollView::new(bar).axis(ScrollAxis::Horizontal).paint(&mut ctx.child(bar_rect));
153 } else {
154 bar.paint(&mut ctx.child(bar_rect));
155 }
156
157 if let Some(content) = self.contents.get(self.selected) {
159 content.paint(&mut ctx.child(content_rect));
160 }
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use crate::tree::Text;
168
169 #[test]
170 fn tabview_declares_the_selected_child() {
171 let tv = TabView::new(vec![Box::new(Text::new("a")), Box::new(Text::new("b"))], 1);
172 assert!(matches!(tv.children(), Children::One(_)));
173 }
174
175 #[test]
176 fn tabview_out_of_range_is_empty() {
177 let tv = TabView::new(vec![Box::new(Text::new("a"))], 9);
178 assert!(matches!(tv.children(), Children::None));
179 }
180
181 #[test]
182 fn tabs_builder_collects_labels_and_content() {
183 let t = Tabs::readonly(0)
184 .tab("One", Text::new("1"))
185 .tab("Two", Text::new("2"));
186 assert_eq!(t.labels, vec!["One", "Two"]);
187 assert_eq!(t.contents.len(), 2);
188 }
189}