use teksilo_canvas::{Point, Rect, SizeProposal};
use crate::accessibility::AccessNodeBuilder;
use crate::build_context::BuildContext;
use crate::widget::{LayoutContext, PendingChild, Widget, WidgetPlacement};
use crate::widget_id::WidgetId;
pub const DEFAULT_DIM_FACTOR: f32 = 0.7;
pub struct DimWhenInactive {
pending_child: Option<PendingChild>,
child_id: Option<WidgetId>,
factor: f32,
}
impl DimWhenInactive {
pub fn new() -> Self {
Self {
pending_child: None,
child_id: None,
factor: DEFAULT_DIM_FACTOR,
}
}
pub fn child(mut self, widget: impl Widget + 'static) -> Self {
self.pending_child = Some(PendingChild::Deferred(Box::new(widget)));
self
}
pub fn child_id(mut self, id: WidgetId) -> Self {
self.pending_child = Some(PendingChild::Id(id));
self
}
pub fn factor(mut self, factor: f32) -> Self {
self.factor = factor.clamp(0.0, 1.0);
self
}
}
impl Default for DimWhenInactive {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for DimWhenInactive {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DimWhenInactive")
.field("factor", &self.factor)
.finish()
}
}
impl Widget for DimWhenInactive {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
if let Some(pending) = self.pending_child.take() {
self.child_id = Some(match pending {
PendingChild::Id(id) => id,
PendingChild::Deferred(w) => ctx.add_boxed(w),
});
}
let Some(child_id) = self.child_id else {
return vec![];
};
let factor = self.factor;
let opacity = ctx
.window_active_signal()
.map(move |&active| if active { 1.0 } else { factor });
let id = ctx.self_id();
ctx.set_opacity(id, opacity);
vec![child_id]
}
fn layout_response(
&self,
proposal: SizeProposal,
ctx: &LayoutContext,
) -> crate::widget::LayoutResponse {
self.child_id
.and_then(|id| ctx.child_size(id, proposal))
.unwrap_or_else(|| proposal.resolve(0.0, 0.0))
.into()
}
fn place_children(
&self,
bounds: Rect,
_proposal: SizeProposal,
children: &mut [WidgetPlacement],
_ctx: &LayoutContext,
) {
for child in children.iter_mut() {
child.origin = Point::new(bounds.x, bounds.y);
child.size = bounds.size();
}
}
fn accessibility(&self, _builder: &mut AccessNodeBuilder) {
}
fn children(&self) -> Vec<WidgetId> {
self.child_id.into_iter().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_widgets::FillWidget;
use crate::widget_tree::WidgetTree;
use teksilo_canvas::{DrawCommand, SizeProposal};
use teksilo_tokens::Color;
fn set_opacities(frame: &teksilo_canvas::RenderFrame) -> Vec<f32> {
frame
.draw_order
.iter()
.filter_map(|c| match c {
DrawCommand::SetOpacity(v) => Some(*v),
_ => None,
})
.collect()
}
#[test]
fn window_active_defaults_true() {
let tree = WidgetTree::new();
assert!(tree.is_window_active());
assert!(tree.window_active_signal().get());
}
#[test]
fn window_active_state_is_per_tree() {
let mut a = WidgetTree::new();
let b = WidgetTree::new();
a.set_window_active(false);
assert!(!a.is_window_active(), "tree A is inactive");
assert!(b.is_window_active(), "tree B is unaffected");
}
#[test]
fn factor_is_clamped() {
assert_eq!(DimWhenInactive::new().factor(2.0).factor, 1.0);
assert_eq!(DimWhenInactive::new().factor(-1.0).factor, 0.0);
assert_eq!(DimWhenInactive::new().factor, DEFAULT_DIM_FACTOR);
}
#[test]
fn dims_subtree_only_when_window_inactive() {
let mut tree = WidgetTree::new().with_theme(crate::presets::intui::light());
tree.add(
DimWhenInactive::new()
.factor(0.5)
.child(FillWidget::new().background(Color::RED)),
);
tree.layout(SizeProposal::exact(100.0, 50.0));
let ops = set_opacities(&tree.render());
assert!(
!ops.iter().any(|o| *o < 0.99),
"active window must not dim, got {ops:?}"
);
tree.set_window_active(false);
let ops = set_opacities(&tree.render());
assert!(
ops.iter().any(|o| (*o - 0.5).abs() < 1e-3),
"inactive window must dim to the factor, got {ops:?}"
);
tree.set_window_active(true);
let ops = set_opacities(&tree.render());
assert!(
!ops.iter().any(|o| *o < 0.99),
"reactivated window must not dim, got {ops:?}"
);
}
}