use alloc::rc::Rc;
use alloc::vec::Vec;
use core::cell::RefCell;
use crate::anim::{ANIM_SCALE, Easing, Tween};
use crate::font::FontId;
use crate::object::ObjectStates;
use crate::object_anim::{ObjectAnimId, ObjectAnims};
use crate::style::Style;
use crate::widget::{Color, Rect};
#[derive(Debug, Default, Clone)]
pub struct TransitionOverride {
pub bg_color: Option<Color>,
pub border_color: Option<Color>,
pub border_width: Option<u8>,
pub alpha: Option<u8>,
pub radius: Option<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnimProp {
BgColor,
BorderColor,
BorderWidth,
Alpha,
Radius,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnimPropValue {
Color(Color),
Scalar(u8),
}
#[derive(Debug, Clone, Copy)]
pub struct TransitionDesc {
pub duration_ticks: u32,
pub delay_ticks: u32,
pub easing: Easing,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
pub struct Part(pub u32);
impl Part {
pub const MAIN: Self = Self(0);
pub const SCROLLBAR: Self = Self(1);
pub const INDICATOR: Self = Self(2);
pub const KNOB: Self = Self(3);
pub const SELECTED: Self = Self(4);
pub const ITEMS: Self = Self(5);
pub const CURSOR: Self = Self(6);
pub const fn custom(id: u32) -> Self {
Self(id)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum TextAlign {
#[default]
Left,
Center,
Right,
Auto,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TextStyle {
pub text_color: Color,
pub font_id: FontId,
pub letter_spacing: i8,
pub line_spacing: i8,
pub text_align: TextAlign,
}
impl Default for TextStyle {
fn default() -> Self {
Self {
text_color: Color(0, 0, 0, 255),
font_id: FontId::DEFAULT,
letter_spacing: 0,
line_spacing: 0,
text_align: TextAlign::Left,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Selector {
pub part: Part,
pub states: ObjectStates,
}
impl Selector {
pub const fn part(part: Part) -> Self {
Self {
part,
states: ObjectStates::DEFAULT,
}
}
pub const fn new(part: Part, states: ObjectStates) -> Self {
Self { part, states }
}
pub const fn matches(&self, part: Part, node_states: ObjectStates) -> bool {
self.part.0 == part.0 && node_states.contains(self.states)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct StylePatch {
pub bg_color: Option<Color>,
pub border_color: Option<Color>,
pub border_width: Option<u8>,
pub alpha: Option<u8>,
pub radius: Option<u8>,
pub text_color: Option<Color>,
pub font_id: Option<FontId>,
pub letter_spacing: Option<i8>,
pub line_spacing: Option<i8>,
pub text_align: Option<TextAlign>,
pub padding_top: Option<i32>,
pub padding_bottom: Option<i32>,
pub padding_left: Option<i32>,
pub padding_right: Option<i32>,
pub margin_top: Option<i32>,
pub margin_bottom: Option<i32>,
pub margin_left: Option<i32>,
pub margin_right: Option<i32>,
pub gap_row: Option<i32>,
pub gap_col: Option<i32>,
}
impl StylePatch {
pub const fn new() -> Self {
Self {
bg_color: None,
border_color: None,
border_width: None,
alpha: None,
radius: None,
text_color: None,
font_id: None,
letter_spacing: None,
line_spacing: None,
text_align: None,
padding_top: None,
padding_bottom: None,
padding_left: None,
padding_right: None,
margin_top: None,
margin_bottom: None,
margin_left: None,
margin_right: None,
gap_row: None,
gap_col: None,
}
}
pub fn builder() -> StylePatchBuilder {
StylePatchBuilder(Self::new())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct StylePatchBuilder(StylePatch);
impl StylePatchBuilder {
pub fn new() -> Self {
Self(StylePatch::new())
}
pub fn bg_color(mut self, color: Color) -> Self {
self.0.bg_color = Some(color);
self
}
pub fn border_color(mut self, color: Color) -> Self {
self.0.border_color = Some(color);
self
}
pub fn border_width(mut self, w: u8) -> Self {
self.0.border_width = Some(w);
self
}
pub fn alpha(mut self, a: u8) -> Self {
self.0.alpha = Some(a);
self
}
pub fn radius(mut self, r: u8) -> Self {
self.0.radius = Some(r);
self
}
pub fn text_color(mut self, color: Color) -> Self {
self.0.text_color = Some(color);
self
}
pub fn font_id(mut self, font_id: FontId) -> Self {
self.0.font_id = Some(font_id);
self
}
pub fn letter_spacing(mut self, spacing: i8) -> Self {
self.0.letter_spacing = Some(spacing);
self
}
pub fn line_spacing(mut self, spacing: i8) -> Self {
self.0.line_spacing = Some(spacing);
self
}
pub fn text_align(mut self, align: TextAlign) -> Self {
self.0.text_align = Some(align);
self
}
pub fn build(self) -> StylePatch {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InheritedContext {
pub alpha: Option<u8>,
pub text_color: Option<Color>,
pub font_id: Option<FontId>,
pub letter_spacing: Option<i8>,
pub line_spacing: Option<i8>,
pub text_align: Option<TextAlign>,
}
impl InheritedContext {
pub const EMPTY: Self = Self {
alpha: None,
text_color: None,
font_id: None,
letter_spacing: None,
line_spacing: None,
text_align: None,
};
pub const fn with_resolved(resolved_alpha: u8) -> Self {
let text = TextStyle {
text_color: Color(0, 0, 0, 255),
font_id: FontId::DEFAULT,
letter_spacing: 0,
line_spacing: 0,
text_align: TextAlign::Left,
};
Self::with_resolved_styles(resolved_alpha, text)
}
pub const fn with_resolved_styles(resolved_alpha: u8, resolved_text: TextStyle) -> Self {
Self {
alpha: Some(resolved_alpha),
text_color: Some(resolved_text.text_color),
font_id: Some(resolved_text.font_id),
letter_spacing: Some(resolved_text.letter_spacing),
line_spacing: Some(resolved_text.line_spacing),
text_align: Some(resolved_text.text_align),
}
}
}
pub struct StyleState {
local: Vec<(Selector, StylePatch)>,
added: Vec<(Selector, &'static StylePatch)>,
theme: Vec<(Selector, StylePatch)>,
pub(crate) transition_override: Rc<RefCell<TransitionOverride>>,
}
impl StyleState {
pub fn new() -> Self {
Self {
local: Vec::new(),
added: Vec::new(),
theme: Vec::new(),
transition_override: Rc::new(RefCell::new(TransitionOverride::default())),
}
}
pub fn transition_override_handle(&self) -> Rc<RefCell<TransitionOverride>> {
Rc::clone(&self.transition_override)
}
pub fn local_entries(&self) -> &[(Selector, StylePatch)] {
&self.local
}
pub fn added_entries(&self) -> &[(Selector, &'static StylePatch)] {
&self.added
}
}
impl Default for StyleState {
fn default() -> Self {
Self::new()
}
}
pub fn push_local(state: &mut StyleState, patch: StylePatch, selector: Selector) {
state.local.push((selector, patch));
}
pub fn push_added(state: &mut StyleState, patch: &'static StylePatch, selector: Selector) {
state.added.push((selector, patch));
}
pub fn push_theme(state: &mut StyleState, patch: StylePatch, selector: Selector) {
state.theme.push((selector, patch));
}
pub fn clear_theme(state: &mut StyleState) -> usize {
let n = state.theme.len();
state.theme.clear();
n
}
pub fn remove_local_matching(state: &mut StyleState, part: Part, states: ObjectStates) -> usize {
let before = state.local.len();
state.local.retain(|(sel, _)| !sel.matches(part, states));
before - state.local.len()
}
pub fn remove_all_local_by_part(state: &mut StyleState, part: Part) -> usize {
let before = state.local.len();
state.local.retain(|(sel, _)| sel.part != part);
before - state.local.len()
}
pub fn remove_all_local(state: &mut StyleState) -> usize {
let count = state.local.len();
state.local.clear();
count
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResolvedStyles {
pub style: Style,
pub text: TextStyle,
pub child_context: InheritedContext,
}
#[derive(Debug, Default)]
struct CascadeWinners {
bg_color: Option<Color>,
border_color: Option<Color>,
border_width: Option<u8>,
alpha: Option<u8>,
radius: Option<u8>,
text_color: Option<Color>,
font_id: Option<FontId>,
letter_spacing: Option<i8>,
line_spacing: Option<i8>,
text_align: Option<TextAlign>,
}
impl CascadeWinners {
fn fill_from_patch(&mut self, patch: &StylePatch) {
if self.bg_color.is_none() {
self.bg_color = patch.bg_color;
}
if self.border_color.is_none() {
self.border_color = patch.border_color;
}
if self.border_width.is_none() {
self.border_width = patch.border_width;
}
if self.alpha.is_none() {
self.alpha = patch.alpha;
}
if self.radius.is_none() {
self.radius = patch.radius;
}
if self.text_color.is_none() {
self.text_color = patch.text_color;
}
if self.font_id.is_none() {
self.font_id = patch.font_id;
}
if self.letter_spacing.is_none() {
self.letter_spacing = patch.letter_spacing;
}
if self.line_spacing.is_none() {
self.line_spacing = patch.line_spacing;
}
if self.text_align.is_none() {
self.text_align = patch.text_align;
}
}
}
pub fn resolve_styles(
node_style: Option<&StyleState>,
node_states: ObjectStates,
part: Part,
inherited: &InheritedContext,
) -> ResolvedStyles {
let mut winners = CascadeWinners::default();
if let Some(s) = node_style {
{
let ov = s.transition_override.borrow();
if ov.bg_color.is_some() {
winners.bg_color = ov.bg_color;
}
if ov.border_color.is_some() {
winners.border_color = ov.border_color;
}
if ov.border_width.is_some() {
winners.border_width = ov.border_width;
}
if ov.alpha.is_some() {
winners.alpha = ov.alpha;
}
if ov.radius.is_some() {
winners.radius = ov.radius;
}
}
for (sel, patch) in s.local.iter().rev() {
if sel.matches(part, node_states) {
winners.fill_from_patch(patch);
}
}
for (sel, patch) in s.added.iter().rev() {
if sel.matches(part, node_states) {
winners.fill_from_patch(patch);
}
}
for (sel, patch) in s.theme.iter().rev() {
if sel.matches(part, node_states) {
winners.fill_from_patch(patch);
}
}
}
if winners.alpha.is_none() {
winners.alpha = inherited.alpha;
}
if winners.text_color.is_none() {
winners.text_color = inherited.text_color;
}
if winners.font_id.is_none() {
winners.font_id = inherited.font_id;
}
if winners.letter_spacing.is_none() {
winners.letter_spacing = inherited.letter_spacing;
}
if winners.line_spacing.is_none() {
winners.line_spacing = inherited.line_spacing;
}
if winners.text_align.is_none() {
winners.text_align = inherited.text_align;
}
let defaults = Style::default();
let text_defaults = TextStyle::default();
let style = Style {
bg_color: winners.bg_color.unwrap_or(defaults.bg_color),
border_color: winners.border_color.unwrap_or(defaults.border_color),
border_width: winners.border_width.unwrap_or(defaults.border_width),
alpha: winners.alpha.unwrap_or(defaults.alpha),
radius: winners.radius.unwrap_or(defaults.radius),
};
let text = TextStyle {
text_color: winners.text_color.unwrap_or(text_defaults.text_color),
font_id: winners.font_id.unwrap_or(text_defaults.font_id),
letter_spacing: winners
.letter_spacing
.unwrap_or(text_defaults.letter_spacing),
line_spacing: winners.line_spacing.unwrap_or(text_defaults.line_spacing),
text_align: winners.text_align.unwrap_or(text_defaults.text_align),
};
let child_context = InheritedContext::with_resolved_styles(style.alpha, text);
ResolvedStyles {
style,
text,
child_context,
}
}
pub fn resolve(
node_style: Option<&StyleState>,
node_states: ObjectStates,
part: Part,
inherited: &InheritedContext,
) -> (Style, InheritedContext) {
let resolved = resolve_styles(node_style, node_states, part, inherited);
(resolved.style, resolved.child_context)
}
pub fn start_transition(
node: &mut crate::object::ObjectNode,
anims: &mut ObjectAnims,
prop: AnimProp,
from: AnimPropValue,
to: AnimPropValue,
desc: TransitionDesc,
node_bounds: Rect,
) -> ObjectAnimId {
node.style
.get_or_insert_with(|| alloc::boxed::Box::new(StyleState::new()));
let override_rc = Rc::clone(
&node
.style
.as_ref()
.expect("just inserted above")
.transition_override,
);
let tween = Tween::new(0, ANIM_SCALE, desc.duration_ticks).with_easing(desc.easing);
let apply_rc = Rc::clone(&override_rc);
let apply: alloc::boxed::Box<dyn FnMut(i32) -> Option<Rect>> =
alloc::boxed::Box::new(move |v: i32| {
let interpolated = interpolate_prop(from, to, v);
let mut ov = apply_rc.borrow_mut();
write_override(&mut ov, prop, interpolated);
Some(node_bounds)
});
let complete_rc = Rc::clone(&override_rc);
let on_complete: alloc::boxed::Box<dyn FnOnce()> = alloc::boxed::Box::new(move || {
let mut ov = complete_rc.borrow_mut();
clear_override(&mut ov, prop);
});
anims.bind(node, tween, apply, desc.delay_ticks, Some(on_complete))
}
pub fn cancel_transition(
node: &mut crate::object::ObjectNode,
anims: &mut ObjectAnims,
old_id: ObjectAnimId,
prop: AnimProp,
) -> bool {
let found = anims.cancel(node, old_id);
if let Some(style) = node.style.as_ref() {
let mut ov = style.transition_override.borrow_mut();
clear_override(&mut ov, prop);
}
found
}
fn interpolate_prop(from: AnimPropValue, to: AnimPropValue, v: i32) -> AnimPropValue {
match (from, to) {
(AnimPropValue::Color(c_from), AnimPropValue::Color(c_to)) => {
AnimPropValue::Color(c_from.lerp(c_to, v, ANIM_SCALE))
}
(AnimPropValue::Scalar(s_from), AnimPropValue::Scalar(s_to)) => {
let interp = (s_from as i32 + (s_to as i32 - s_from as i32) * v / ANIM_SCALE)
.clamp(0, 255) as u8;
AnimPropValue::Scalar(interp)
}
(from, _) => from,
}
}
fn write_override(ov: &mut TransitionOverride, prop: AnimProp, value: AnimPropValue) {
match (prop, value) {
(AnimProp::BgColor, AnimPropValue::Color(c)) => ov.bg_color = Some(c),
(AnimProp::BorderColor, AnimPropValue::Color(c)) => ov.border_color = Some(c),
(AnimProp::BorderWidth, AnimPropValue::Scalar(s)) => ov.border_width = Some(s),
(AnimProp::Alpha, AnimPropValue::Scalar(s)) => ov.alpha = Some(s),
(AnimProp::Radius, AnimPropValue::Scalar(s)) => ov.radius = Some(s),
_ => {}
}
}
fn clear_override(ov: &mut TransitionOverride, prop: AnimProp) {
match prop {
AnimProp::BgColor => ov.bg_color = None,
AnimProp::BorderColor => ov.border_color = None,
AnimProp::BorderWidth => ov.border_width = None,
AnimProp::Alpha => ov.alpha = None,
AnimProp::Radius => ov.radius = None,
}
}
pub fn resolve_tree<F>(root: &crate::object::ObjectNode, visitor: &mut F)
where
F: FnMut(&crate::object::ObjectNode, &Style),
{
resolve_tree_inner(root, &InheritedContext::EMPTY, visitor);
}
fn resolve_tree_inner<F>(
node: &crate::object::ObjectNode,
inherited: &InheritedContext,
visitor: &mut F,
) where
F: FnMut(&crate::object::ObjectNode, &Style),
{
let (style, child_ctx) = resolve(node.style.as_deref(), node.states(), Part::MAIN, inherited);
visitor(node, &style);
for child in node.children() {
resolve_tree_inner(child, &child_ctx, visitor);
}
}
pub fn resolve_tree_with_text<F>(root: &crate::object::ObjectNode, visitor: &mut F)
where
F: FnMut(&crate::object::ObjectNode, &Style, &TextStyle),
{
resolve_tree_with_text_inner(root, &InheritedContext::EMPTY, visitor);
}
fn resolve_tree_with_text_inner<F>(
node: &crate::object::ObjectNode,
inherited: &InheritedContext,
visitor: &mut F,
) where
F: FnMut(&crate::object::ObjectNode, &Style, &TextStyle),
{
let resolved = resolve_styles(node.style.as_deref(), node.states(), Part::MAIN, inherited);
visitor(node, &resolved.style, &resolved.text);
for child in node.children() {
resolve_tree_with_text_inner(child, &resolved.child_context, visitor);
}
}
#[cfg(test)]
mod tests {
use alloc::rc::Rc;
use core::cell::RefCell;
use super::*;
use crate::object::{ObjectNode, ObjectStates};
use crate::widget::{Color, Rect, Widget};
struct Dummy;
impl Widget for Dummy {
fn bounds(&self) -> Rect {
Rect {
x: 0,
y: 0,
width: 10,
height: 10,
}
}
fn draw(&self, _r: &mut dyn crate::renderer::Renderer) {}
fn handle_event(&mut self, _e: &crate::event::Event) -> bool {
false
}
}
fn make_node() -> ObjectNode {
ObjectNode::new(Rc::new(RefCell::new(Dummy)))
}
fn red() -> Color {
Color(255, 0, 0, 255)
}
fn blue() -> Color {
Color(0, 0, 255, 255)
}
fn green() -> Color {
Color(0, 255, 0, 255)
}
#[test]
fn selector_main_pressed_matches_pressed_state() {
let sel = Selector::new(Part::MAIN, ObjectStates::PRESSED);
assert!(sel.matches(Part::MAIN, ObjectStates::PRESSED));
assert!(!sel.matches(Part::MAIN, ObjectStates::DEFAULT));
}
#[test]
fn selector_part_matches_any_state() {
let sel = Selector::part(Part::MAIN);
assert!(sel.matches(Part::MAIN, ObjectStates::DEFAULT));
assert!(sel.matches(Part::MAIN, ObjectStates::PRESSED));
assert!(sel.matches(Part::MAIN, ObjectStates::FOCUSED));
}
#[test]
fn selector_does_not_match_wrong_part() {
let sel = Selector::part(Part::INDICATOR);
assert!(!sel.matches(Part::MAIN, ObjectStates::DEFAULT));
}
#[test]
fn selector_multi_state_requires_all_bits() {
let mask = ObjectStates::from_bits_truncate(
ObjectStates::PRESSED.bits() | ObjectStates::FOCUSED.bits(),
);
let sel = Selector::new(Part::MAIN, mask);
assert!(sel.matches(Part::MAIN, mask));
assert!(!sel.matches(Part::MAIN, ObjectStates::PRESSED));
}
#[test]
fn local_overrides_added_for_same_field() {
let mut node = make_node();
static ADDED_PATCH: StylePatch = StylePatch {
bg_color: Some(Color(0, 0, 255, 255)),
border_color: None,
border_width: None,
alpha: None,
radius: None,
text_color: None,
font_id: None,
letter_spacing: None,
line_spacing: None,
text_align: None,
padding_top: None,
padding_bottom: None,
padding_left: None,
padding_right: None,
margin_top: None,
margin_bottom: None,
margin_left: None,
margin_right: None,
gap_row: None,
gap_col: None,
};
node.add_style(&ADDED_PATCH, Selector::part(Part::MAIN));
let local_patch = StylePatch {
bg_color: Some(Color(255, 0, 0, 255)),
..StylePatch::new()
};
node.add_local_style(local_patch, Selector::part(Part::MAIN));
let (style, _) = resolve(
node.style.as_deref(),
node.states(),
Part::MAIN,
&InheritedContext::EMPTY,
);
assert_eq!(style.bg_color, red());
}
#[test]
fn last_added_local_wins_among_matching_locals() {
let mut node = make_node();
node.add_local_style(
StylePatch {
bg_color: Some(red()),
..StylePatch::new()
},
Selector::part(Part::MAIN),
);
node.add_local_style(
StylePatch {
bg_color: Some(blue()),
..StylePatch::new()
},
Selector::part(Part::MAIN),
);
let (style, _) = resolve(
node.style.as_deref(),
node.states(),
Part::MAIN,
&InheritedContext::EMPTY,
);
assert_eq!(style.bg_color, blue());
}
#[test]
fn patch_only_bg_leaves_other_fields_at_default() {
let mut node = make_node();
node.add_local_style(
StylePatch {
bg_color: Some(red()),
..StylePatch::new()
},
Selector::part(Part::MAIN),
);
let (style, _) = resolve(
node.style.as_deref(),
node.states(),
Part::MAIN,
&InheritedContext::EMPTY,
);
let defaults = Style::default();
assert_eq!(style.bg_color, red(), "bg_color from patch");
assert_eq!(
style.border_color, defaults.border_color,
"border_color is default"
);
assert_eq!(
style.border_width, defaults.border_width,
"border_width is default"
);
assert_eq!(style.alpha, defaults.alpha, "alpha is default");
assert_eq!(style.radius, defaults.radius, "radius is default");
}
#[test]
fn state_driven_base_and_pressed_override() {
let mut node = make_node();
node.add_local_style(
StylePatch {
bg_color: Some(blue()),
..StylePatch::new()
},
Selector::new(Part::MAIN, ObjectStates::DEFAULT),
);
node.add_local_style(
StylePatch {
bg_color: Some(red()),
..StylePatch::new()
},
Selector::new(Part::MAIN, ObjectStates::PRESSED),
);
let (style_default, _) = resolve(
node.style.as_deref(),
ObjectStates::DEFAULT,
Part::MAIN,
&InheritedContext::EMPTY,
);
assert_eq!(
style_default.bg_color,
blue(),
"default state should give blue"
);
let (style_pressed, _) = resolve(
node.style.as_deref(),
ObjectStates::PRESSED,
Part::MAIN,
&InheritedContext::EMPTY,
);
assert_eq!(
style_pressed.bg_color,
red(),
"pressed state should give red"
);
}
#[test]
fn remove_local_styles_removes_matching_entries() {
let mut node = make_node();
node.add_local_style(
StylePatch {
bg_color: Some(red()),
..StylePatch::new()
},
Selector::part(Part::MAIN),
);
node.add_local_style(
StylePatch {
bg_color: Some(green()),
..StylePatch::new()
},
Selector::part(Part::SCROLLBAR),
);
let removed = node.remove_local_styles(Part::MAIN, ObjectStates::DEFAULT);
assert_eq!(removed, 1);
let (style, _) = resolve(
node.style.as_deref(),
ObjectStates::DEFAULT,
Part::MAIN,
&InheritedContext::EMPTY,
);
assert_eq!(style.bg_color, Style::default().bg_color);
}
#[test]
fn remove_all_local_styles_clears_everything() {
let mut node = make_node();
node.add_local_style(
StylePatch {
bg_color: Some(red()),
..StylePatch::new()
},
Selector::part(Part::MAIN),
);
node.add_local_style(
StylePatch {
bg_color: Some(blue()),
..StylePatch::new()
},
Selector::part(Part::INDICATOR),
);
let removed = node.remove_all_local_styles();
assert_eq!(removed, 2);
let (style, _) = resolve(
node.style.as_deref(),
ObjectStates::DEFAULT,
Part::MAIN,
&InheritedContext::EMPTY,
);
assert_eq!(style.bg_color, Style::default().bg_color);
}
#[test]
fn alpha_inherits_from_parent_context() {
let parent_ctx = InheritedContext::with_resolved(128);
let mut child = make_node();
child.add_local_style(
StylePatch {
bg_color: Some(red()),
..StylePatch::new()
},
Selector::part(Part::MAIN),
);
let (child_style, _) = resolve(
child.style.as_deref(),
child.states(),
Part::MAIN,
&parent_ctx,
);
assert_eq!(child_style.alpha, 128, "child should inherit parent alpha");
}
#[test]
fn alpha_own_patch_overrides_inheritance() {
let parent_ctx = InheritedContext::with_resolved(128);
let mut child = make_node();
child.add_local_style(
StylePatch {
alpha: Some(200),
..StylePatch::new()
},
Selector::part(Part::MAIN),
);
let (child_style, grandchild_ctx) = resolve(
child.style.as_deref(),
child.states(),
Part::MAIN,
&parent_ctx,
);
assert_eq!(child_style.alpha, 200, "child's own patch overrides");
assert_eq!(grandchild_ctx.alpha, Some(200));
}
#[test]
fn grandchild_inherits_overriding_ancestor_alpha() {
let parent_ctx = InheritedContext::with_resolved(100);
let mut child = make_node();
child.add_local_style(
StylePatch {
alpha: Some(200),
..StylePatch::new()
},
Selector::part(Part::MAIN),
);
let (_, grandchild_ctx) = resolve(
child.style.as_deref(),
child.states(),
Part::MAIN,
&parent_ctx,
);
assert_eq!(grandchild_ctx.alpha, Some(200));
let grandchild = make_node();
let (gs, _) = resolve(
grandchild.style.as_deref(),
grandchild.states(),
Part::MAIN,
&grandchild_ctx,
);
assert_eq!(gs.alpha, 200);
}
#[test]
fn no_alpha_anywhere_resolves_to_default_255() {
let node = make_node();
let (style, _) = resolve(
node.style.as_deref(),
node.states(),
Part::MAIN,
&InheritedContext::EMPTY,
);
assert_eq!(style.alpha, 255);
}
#[test]
fn bg_color_does_not_inherit() {
let mut parent = make_node();
parent.add_local_style(
StylePatch {
bg_color: Some(red()),
..StylePatch::new()
},
Selector::part(Part::MAIN),
);
let (_, child_ctx) = resolve(
parent.style.as_deref(),
parent.states(),
Part::MAIN,
&InheritedContext::EMPTY,
);
let child = make_node();
let (child_style, _) = resolve(
child.style.as_deref(),
child.states(),
Part::MAIN,
&child_ctx,
);
assert_eq!(child_style.bg_color, Style::default().bg_color);
}
#[test]
fn text_style_defaults_resolve_without_patches() {
let node = make_node();
let resolved = resolve_styles(
node.style.as_deref(),
node.states(),
Part::MAIN,
&InheritedContext::EMPTY,
);
assert_eq!(resolved.text, TextStyle::default());
assert_eq!(resolved.child_context.text_color, Some(Color(0, 0, 0, 255)));
assert_eq!(resolved.child_context.font_id, Some(FontId::DEFAULT));
}
#[test]
fn text_style_inherits_from_parent_context() {
let parent_text = TextStyle {
text_color: green(),
font_id: FontId(7),
letter_spacing: 2,
line_spacing: 3,
text_align: TextAlign::Center,
};
let parent_ctx = InheritedContext::with_resolved_styles(128, parent_text);
let child = make_node();
let resolved = resolve_styles(
child.style.as_deref(),
child.states(),
Part::MAIN,
&parent_ctx,
);
assert_eq!(resolved.text, parent_text);
assert_eq!(resolved.style.alpha, 128);
}
#[test]
fn local_text_patch_overrides_inherited_text() {
let parent_text = TextStyle {
text_color: green(),
font_id: FontId(7),
letter_spacing: 2,
line_spacing: 3,
text_align: TextAlign::Right,
};
let parent_ctx = InheritedContext::with_resolved_styles(255, parent_text);
let mut child = make_node();
child.add_local_style(
StylePatch::builder()
.text_color(red())
.font_id(FontId(9))
.letter_spacing(4)
.line_spacing(5)
.text_align(TextAlign::Center)
.build(),
Selector::part(Part::MAIN),
);
let resolved = resolve_styles(
child.style.as_deref(),
child.states(),
Part::MAIN,
&parent_ctx,
);
assert_eq!(resolved.text.text_color, red());
assert_eq!(resolved.text.font_id, FontId(9));
assert_eq!(resolved.text.letter_spacing, 4);
assert_eq!(resolved.text.line_spacing, 5);
assert_eq!(resolved.text.text_align, TextAlign::Center);
}
#[test]
fn resolve_tree_visits_all_nodes_with_inherited_alpha() {
use alloc::collections::BTreeMap;
use alloc::string::String;
let root_widget = Rc::new(RefCell::new(Dummy));
let child_widget = Rc::new(RefCell::new(Dummy));
let mut root = ObjectNode::new(root_widget).with_tag("root");
let child = ObjectNode::new(child_widget).with_tag("child");
root.add_local_style(
StylePatch {
alpha: Some(50),
..StylePatch::new()
},
Selector::part(Part::MAIN),
);
root.append_child(child);
let mut alpha_map: BTreeMap<String, u8> = BTreeMap::new();
resolve_tree(&root, &mut |node, style| {
if let Some(tag) = node.tag() {
alpha_map.insert(tag.to_string(), style.alpha);
}
});
assert_eq!(alpha_map.get("root"), Some(&50), "root has its own alpha");
assert_eq!(
alpha_map.get("child"),
Some(&50),
"child inherits root alpha"
);
}
#[test]
fn resolve_tree_with_text_threads_text_context() {
use alloc::collections::BTreeMap;
use alloc::string::String;
let root_widget = Rc::new(RefCell::new(Dummy));
let child_widget = Rc::new(RefCell::new(Dummy));
let mut root = ObjectNode::new(root_widget).with_tag("root");
let child = ObjectNode::new(child_widget).with_tag("child");
root.add_local_style(
StylePatch::builder()
.text_color(blue())
.font_id(FontId(11))
.letter_spacing(1)
.line_spacing(2)
.text_align(TextAlign::Right)
.build(),
Selector::part(Part::MAIN),
);
root.append_child(child);
let mut text_colors: BTreeMap<String, Color> = BTreeMap::new();
let mut font_ids: BTreeMap<String, FontId> = BTreeMap::new();
resolve_tree_with_text(&root, &mut |node, _style, text| {
if let Some(tag) = node.tag() {
text_colors.insert(tag.to_string(), text.text_color);
font_ids.insert(tag.to_string(), text.font_id);
}
});
assert_eq!(text_colors.get("root"), Some(&blue()));
assert_eq!(text_colors.get("child"), Some(&blue()));
assert_eq!(font_ids.get("root"), Some(&FontId(11)));
assert_eq!(font_ids.get("child"), Some(&FontId(11)));
}
#[test]
fn style_builder_still_works() {
use crate::style::StyleBuilder;
let s = StyleBuilder::new()
.bg_color(red())
.border_width(2)
.alpha(128)
.build();
assert_eq!(s.bg_color, red());
assert_eq!(s.border_width, 2);
assert_eq!(s.alpha, 128);
}
#[test]
fn object_node_methods_round_trip() {
let mut node = make_node();
assert!(node.style.is_none(), "initially no style slot");
node.add_local_style(
StylePatch {
bg_color: Some(green()),
..StylePatch::new()
},
Selector::part(Part::MAIN),
);
assert!(node.style.is_some(), "slot allocated after add");
static SP: StylePatch = StylePatch {
border_width: Some(3),
bg_color: None,
border_color: None,
alpha: None,
radius: None,
text_color: None,
font_id: None,
letter_spacing: None,
line_spacing: None,
text_align: None,
padding_top: None,
padding_bottom: None,
padding_left: None,
padding_right: None,
margin_top: None,
margin_bottom: None,
margin_left: None,
margin_right: None,
gap_row: None,
gap_col: None,
};
node.add_style(&SP, Selector::part(Part::MAIN));
let (s, _) = resolve(
node.style.as_deref(),
node.states(),
Part::MAIN,
&InheritedContext::EMPTY,
);
assert_eq!(s.bg_color, green());
assert_eq!(s.border_width, 3);
let removed = node.remove_local_styles(Part::MAIN, ObjectStates::DEFAULT);
assert_eq!(removed, 1);
}
fn tick_n(oa: &mut crate::object_anim::ObjectAnims, root: &mut ObjectNode, n: u32) {
for _ in 0..n {
oa.tick(root, &mut |_| {});
}
}
#[test]
fn transition_override_wins_above_local_styles() {
let mut node = make_node();
node.add_local_style(
StylePatch {
bg_color: Some(red()),
..StylePatch::new()
},
Selector::part(Part::MAIN),
);
let override_rc = node
.style
.get_or_insert_with(|| alloc::boxed::Box::new(StyleState::new()))
.transition_override_handle();
override_rc.borrow_mut().bg_color = Some(blue());
let (style, _) = resolve(
node.style.as_deref(),
node.states(),
Part::MAIN,
&InheritedContext::EMPTY,
);
assert_eq!(
style.bg_color,
blue(),
"transition override must beat local styles"
);
}
#[test]
fn resolve_without_override_identical_to_before() {
let mut node = make_node();
node.add_local_style(
StylePatch {
bg_color: Some(red()),
border_width: Some(3),
..StylePatch::new()
},
Selector::part(Part::MAIN),
);
let (style, _) = resolve(
node.style.as_deref(),
node.states(),
Part::MAIN,
&InheritedContext::EMPTY,
);
assert_eq!(style.bg_color, red(), "bg_color from local patch unchanged");
assert_eq!(
style.border_width, 3,
"border_width from local patch unchanged"
);
}
#[test]
fn start_transition_animates_override() {
let mut node = make_node();
let mut oa = crate::object_anim::ObjectAnims::new();
let bounds = Rect {
x: 0,
y: 0,
width: 10,
height: 10,
};
let _id = start_transition(
&mut node,
&mut oa,
AnimProp::BgColor,
AnimPropValue::Color(red()),
AnimPropValue::Color(blue()),
TransitionDesc {
duration_ticks: 256,
delay_ticks: 0,
easing: crate::anim::Easing::Linear,
},
bounds,
);
tick_n(&mut oa, &mut node, 128);
let (style_mid, _) = resolve(
node.style.as_deref(),
node.states(),
Part::MAIN,
&InheritedContext::EMPTY,
);
assert_ne!(
style_mid.bg_color,
red(),
"should have started transitioning"
);
assert_ne!(style_mid.bg_color, blue(), "should not be at end yet");
tick_n(&mut oa, &mut node, 128);
let (style_after, _) = resolve(
node.style.as_deref(),
node.states(),
Part::MAIN,
&InheritedContext::EMPTY,
);
assert_eq!(
style_after.bg_color,
Style::default().bg_color,
"override should be cleared after transition completes"
);
}
#[test]
fn transition_cancel_stops_without_completion() {
let mut node = make_node();
let mut oa = crate::object_anim::ObjectAnims::new();
let bounds = Rect {
x: 0,
y: 0,
width: 10,
height: 10,
};
let id = start_transition(
&mut node,
&mut oa,
AnimProp::Alpha,
AnimPropValue::Scalar(255),
AnimPropValue::Scalar(0),
TransitionDesc {
duration_ticks: 100,
delay_ticks: 0,
easing: crate::anim::Easing::Linear,
},
bounds,
);
tick_n(&mut oa, &mut node, 10);
{
let ov_rc = node.style.as_ref().unwrap().transition_override_handle();
assert!(
ov_rc.borrow().alpha.is_some(),
"override should be set after a few ticks"
);
}
cancel_transition(&mut node, &mut oa, id, AnimProp::Alpha);
let ov_rc = node.style.as_ref().unwrap().transition_override_handle();
assert!(
ov_rc.borrow().alpha.is_none(),
"override should be cleared after cancel_transition"
);
}
#[test]
fn determinism_check() {
use crate::anim::Easing;
let bounds = Rect {
x: 0,
y: 0,
width: 10,
height: 10,
};
let desc = TransitionDesc {
duration_ticks: 50,
delay_ticks: 0,
easing: Easing::EaseOut,
};
let sample = || {
let mut node = make_node();
let mut oa = crate::object_anim::ObjectAnims::new();
start_transition(
&mut node,
&mut oa,
AnimProp::BgColor,
AnimPropValue::Color(red()),
AnimPropValue::Color(blue()),
desc,
bounds,
);
tick_n(&mut oa, &mut node, 25);
let (style, _) = resolve(
node.style.as_deref(),
node.states(),
Part::MAIN,
&InheritedContext::EMPTY,
);
style.bg_color
};
assert_eq!(sample(), sample(), "transitions are deterministic");
}
}