teksilo_widgets/
group_box.rs1use teksilo_canvas::{Rect, Size, SizeProposal};
39use teksilo_core::accessibility::AccessNodeBuilder;
40use teksilo_core::binding::BindingLevel;
41use teksilo_core::build_context::BuildContext;
42use teksilo_core::signal::Signal;
43use teksilo_core::widget::{LayoutContext, Widget, WidgetPlacement};
44use teksilo_core::widget_id::WidgetId;
45
46use crate::Checkbox;
47use crate::primitives::{HStack, Padding, RectWidget, TextWidget, VStack, ZStack};
48use teksilo_i18n::LocalizedString;
49use teksilo_tokens::{TextRole, TextStyleRole};
50
51pub const GROUP_BOX_CONTENT_INDENT: f32 = 24.0;
53pub const GROUP_BOX_TITLE_CONTENT_SPACING: f32 = 8.0;
55pub const GROUP_BOX_CHECKBOX_GAP: f32 = 6.0;
57
58pub struct GroupBox {
63 title: LocalizedString,
64 checked: Option<Signal<bool>>,
65 pending_content: Option<Box<dyn Widget>>,
66 content_id: Option<WidgetId>,
67 root_child_id: Option<WidgetId>,
68}
69
70impl GroupBox {
71 pub fn new(title: impl Into<LocalizedString>) -> Self {
73 let ls: LocalizedString = title.into();
74 Self {
75 title: ls,
76 checked: None,
77 pending_content: None,
78 content_id: None,
79 root_child_id: None,
80 }
81 }
82
83 pub fn checkable(mut self, checked: Signal<bool>) -> Self {
87 self.checked = Some(checked);
88 self
89 }
90
91 pub fn child(mut self, widget: impl Widget + 'static) -> Self {
93 self.pending_content = Some(Box::new(widget));
94 self
95 }
96
97 pub fn child_id(mut self, id: WidgetId) -> Self {
99 self.content_id = Some(id);
100 self
101 }
102}
103
104impl std::fmt::Debug for GroupBox {
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 f.debug_struct("GroupBox")
107 .field("title", &self.title)
108 .field("checkable", &self.checked.is_some())
109 .finish()
110 }
111}
112
113impl Widget for GroupBox {
114 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
115 if let Some(pending) = self.pending_content.take() {
116 self.content_id = Some(ctx.add_boxed(pending));
117 }
118
119 if let Some(ref checked) = self.checked {
122 let self_id = ctx.self_id();
123 checked.bind_to(
124 self_id,
125 ctx.binding_registry(),
126 BindingLevel::AccessibilityOnly,
127 );
128 }
129
130 let theme_signal = ctx.theme_signal();
131 let _ = theme_signal.get();
132
133 let title_label = TextWidget::new(self.title.clone())
134 .style(TextStyleRole::BodyBold)
135 .color(TextRole::Primary)
136 .single_line()
137 .a11y_hidden();
138
139 let title_row_id = if let Some(ref checked) = self.checked {
140 let checkbox = Checkbox::new(checked.clone()).label(self.title.clone());
143 ctx.add(
144 HStack::new()
145 .spacing(GROUP_BOX_CHECKBOX_GAP)
146 .child(checkbox)
147 .child(title_label),
148 )
149 } else {
150 ctx.add(title_label)
151 };
152
153 let padded_content_id = if let Some(content_id) = self.content_id {
154 ctx.add(Padding::new(0.0, 0.0, 0.0, GROUP_BOX_CONTENT_INDENT).child_id(content_id))
155 } else {
156 ctx.add(Padding::new(0.0, 0.0, 0.0, GROUP_BOX_CONTENT_INDENT))
157 };
158
159 let content_wrapper_id = if let Some(ref checked) = self.checked {
164 let dim_color = theme_signal.map(|t| t.colors.surface_main.with_alpha(0.6));
165 let dim_overlay_id = ctx.add(RectWidget::new().background(dim_color));
166 ctx.visible_when(dim_overlay_id, checked.map(|v| !*v));
167 ctx.enabled_when(padded_content_id, checked.clone());
168 ctx.add(
169 ZStack::new()
170 .add_child(padded_content_id)
171 .add_child(dim_overlay_id),
172 )
173 } else {
174 padded_content_id
175 };
176
177 let root = ctx.add(
178 VStack::new()
179 .spacing(GROUP_BOX_TITLE_CONTENT_SPACING)
180 .add_child(title_row_id)
181 .add_child(content_wrapper_id),
182 );
183 self.root_child_id = Some(root);
184
185 vec![root]
186 }
187
188 fn layout_response(
189 &self,
190 proposal: SizeProposal,
191 ctx: &LayoutContext,
192 ) -> teksilo_core::widget::LayoutResponse {
193 if let Some(root) = self.root_child_id
194 && let Some(size) = ctx.child_size(root, proposal)
195 {
196 return (size).into();
197 }
198 proposal.resolve(0.0, 0.0).into()
199 }
200
201 fn place_children(
202 &self,
203 bounds: Rect,
204 _proposal: SizeProposal,
205 children: &mut [WidgetPlacement],
206 _ctx: &LayoutContext,
207 ) {
208 for child in children.iter_mut() {
209 child.origin = teksilo_canvas::Point::new(bounds.x, bounds.y);
210 child.size = Size::new(bounds.width, bounds.height);
211 }
212 }
213
214 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
215 builder.set_role(teksilo_core::accesskit::Role::Group);
216 builder.set_name(self.title.resolve_now());
217 if let Some(ref checked) = self.checked
218 && !checked.get()
219 {
220 builder.set_disabled();
221 }
222 }
223
224 fn children(&self) -> Vec<WidgetId> {
225 self.root_child_id.into_iter().collect()
226 }
227}