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