teksilo_core/
dim_when_inactive.rs1use teksilo_canvas::{Point, Rect, SizeProposal};
38
39use crate::accessibility::AccessNodeBuilder;
40use crate::build_context::BuildContext;
41use crate::widget::{LayoutContext, PendingChild, Widget, WidgetPlacement};
42use crate::widget_id::WidgetId;
43
44pub const DEFAULT_DIM_FACTOR: f32 = 0.7;
47
48pub struct DimWhenInactive {
51 pending_child: Option<PendingChild>,
52 child_id: Option<WidgetId>,
53 factor: f32,
54}
55
56impl DimWhenInactive {
57 pub fn new() -> Self {
60 Self {
61 pending_child: None,
62 child_id: None,
63 factor: DEFAULT_DIM_FACTOR,
64 }
65 }
66
67 pub fn child(mut self, widget: impl Widget + 'static) -> Self {
69 self.pending_child = Some(PendingChild::Deferred(Box::new(widget)));
70 self
71 }
72
73 pub fn child_id(mut self, id: WidgetId) -> Self {
75 self.pending_child = Some(PendingChild::Id(id));
76 self
77 }
78
79 pub fn factor(mut self, factor: f32) -> Self {
83 self.factor = factor.clamp(0.0, 1.0);
84 self
85 }
86}
87
88impl Default for DimWhenInactive {
89 fn default() -> Self {
90 Self::new()
91 }
92}
93
94impl std::fmt::Debug for DimWhenInactive {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 f.debug_struct("DimWhenInactive")
97 .field("factor", &self.factor)
98 .finish()
99 }
100}
101
102impl Widget for DimWhenInactive {
103 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
104 if let Some(pending) = self.pending_child.take() {
105 self.child_id = Some(match pending {
106 PendingChild::Id(id) => id,
107 PendingChild::Deferred(w) => ctx.add_boxed(w),
108 });
109 }
110 let Some(child_id) = self.child_id else {
111 return vec![];
112 };
113
114 let factor = self.factor;
119 let opacity = ctx
120 .window_active_signal()
121 .map(move |&active| if active { 1.0 } else { factor });
122
123 let id = ctx.self_id();
124 ctx.set_opacity(id, opacity);
125
126 vec![child_id]
127 }
128
129 fn layout_response(
130 &self,
131 proposal: SizeProposal,
132 ctx: &LayoutContext,
133 ) -> crate::widget::LayoutResponse {
134 self.child_id
136 .and_then(|id| ctx.child_size(id, proposal))
137 .unwrap_or_else(|| proposal.resolve(0.0, 0.0))
138 .into()
139 }
140
141 fn place_children(
142 &self,
143 bounds: Rect,
144 _proposal: SizeProposal,
145 children: &mut [WidgetPlacement],
146 _ctx: &LayoutContext,
147 ) {
148 for child in children.iter_mut() {
149 child.origin = Point::new(bounds.x, bounds.y);
150 child.size = bounds.size();
151 }
152 }
153
154 fn accessibility(&self, _builder: &mut AccessNodeBuilder) {
155 }
157
158 fn children(&self) -> Vec<WidgetId> {
159 self.child_id.into_iter().collect()
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use crate::test_widgets::FillWidget;
167 use crate::widget_tree::WidgetTree;
168 use teksilo_canvas::{DrawCommand, SizeProposal};
169 use teksilo_tokens::Color;
170
171 fn set_opacities(frame: &teksilo_canvas::RenderFrame) -> Vec<f32> {
172 frame
173 .draw_order
174 .iter()
175 .filter_map(|c| match c {
176 DrawCommand::SetOpacity(v) => Some(*v),
177 _ => None,
178 })
179 .collect()
180 }
181
182 #[test]
183 fn window_active_defaults_true() {
184 let tree = WidgetTree::new();
187 assert!(tree.is_window_active());
188 assert!(tree.window_active_signal().get());
189 }
190
191 #[test]
192 fn window_active_state_is_per_tree() {
193 let mut a = WidgetTree::new();
196 let b = WidgetTree::new();
197 a.set_window_active(false);
198 assert!(!a.is_window_active(), "tree A is inactive");
199 assert!(b.is_window_active(), "tree B is unaffected");
200 }
201
202 #[test]
203 fn factor_is_clamped() {
204 assert_eq!(DimWhenInactive::new().factor(2.0).factor, 1.0);
205 assert_eq!(DimWhenInactive::new().factor(-1.0).factor, 0.0);
206 assert_eq!(DimWhenInactive::new().factor, DEFAULT_DIM_FACTOR);
207 }
208
209 #[test]
210 fn dims_subtree_only_when_window_inactive() {
211 let mut tree = WidgetTree::new().with_theme(crate::presets::intui::light());
212 tree.add(
213 DimWhenInactive::new()
214 .factor(0.5)
215 .child(FillWidget::new().background(Color::RED)),
216 );
217 tree.layout(SizeProposal::exact(100.0, 50.0));
218
219 let ops = set_opacities(&tree.render());
221 assert!(
222 !ops.iter().any(|o| *o < 0.99),
223 "active window must not dim, got {ops:?}"
224 );
225
226 tree.set_window_active(false);
228 let ops = set_opacities(&tree.render());
229 assert!(
230 ops.iter().any(|o| (*o - 0.5).abs() < 1e-3),
231 "inactive window must dim to the factor, got {ops:?}"
232 );
233
234 tree.set_window_active(true);
236 let ops = set_opacities(&tree.render());
237 assert!(
238 !ops.iter().any(|o| *o < 0.99),
239 "reactivated window must not dim, got {ops:?}"
240 );
241 }
242}