use std::time::{SystemTime, UNIX_EPOCH};
use crate::animation::{AnimatedValue, Curve, Keyframes};
use crate::ui::event::EventResult;
use crate::ui::{Event, View};
use crate::{
Context, Node, Overflow, ScrollbarVisibility, Size, Style, TextRenderInfo, TextStyle,
TransitionProperty,
};
pub trait ViewStyleExt: Sized {
fn style(self, style: Style) -> StyledView<Self>;
fn animate_keyframes(self, keyframes: Keyframes<Style>) -> KeyframedView<Self>;
fn tooltip(self, text: impl Into<String>) -> crate::ui::widgets::Tooltip<Self> {
crate::ui::widgets::tooltip(self, text)
}
}
impl<V> ViewStyleExt for V {
fn style(self, style: Style) -> StyledView<Self> {
StyledView { inner: self, style }
}
fn animate_keyframes(self, keyframes: Keyframes<Style>) -> KeyframedView<Self> {
KeyframedView {
inner: self,
keyframes,
}
}
}
#[derive(Default)]
pub struct StyledViewState {
pub is_hovered: bool,
pub is_active: bool,
pub is_focused: bool,
pub is_disabled: bool,
pub is_animating: bool,
pub style_anim: Option<AnimatedValue<Style>>,
}
impl StyledViewState {
pub fn new() -> Self {
Self {
is_hovered: false,
is_active: false,
is_focused: false,
is_disabled: false,
is_animating: false,
style_anim: None,
}
}
}
fn now_ms() -> f64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs_f64()
* 1000.0
}
pub struct StyledView<V> {
pub(crate) inner: V,
pub(crate) style: Style,
}
impl<State, V: View<State>> View<State> for StyledView<V> {
type Element = (V::Element, StyledViewState);
type Message = V::Message;
fn build(&self, ctx: &mut Context) -> Self::Element {
let child_el = self.inner.build(ctx);
let node = self.inner.get_node(&child_el);
node.update_constraints(ctx, |c| {
let overflow = c.overflow;
let scroll = c.scroll;
let flex_dir = self.style.flex_direction.unwrap_or(c.flex_direction);
let is_both_fixed = matches!(
self.style.base_constraints.width,
Size::Fixed(_) | Size::Percent(_)
) && matches!(
self.style.base_constraints.height,
Size::Fixed(_) | Size::Percent(_)
);
let aspect_ratio = if self.style.base_constraints.aspect_ratio > 0.0 {
self.style.base_constraints.aspect_ratio
} else if is_both_fixed {
0.0
} else {
c.aspect_ratio
};
*c = self.style.base_constraints;
c.flex_direction = flex_dir;
c.aspect_ratio = aspect_ratio;
if self.style.base_constraints.overflow == Overflow::Visible
&& overflow != Overflow::Visible
{
c.overflow = overflow;
}
c.scroll = scroll;
});
node.set_effects(ctx, self.style.base_effects.clone());
if let Some(text) = node.get_text(ctx) {
let text_owned = text.to_string();
if self.style.base_text_style != TextStyle::default() {
if let Some(mut info) = node.get_text_userdata::<TextRenderInfo>(ctx).cloned() {
info.style = self.style.base_text_style.clone();
node.set_text_with_userdata(ctx, &text_owned, info);
} else {
node.set_text_with_userdata(
ctx,
&text_owned,
self.style.base_text_style.clone(),
);
}
}
}
let mut view_state = StyledViewState::new();
view_state.is_focused = Some(node) == ctx.focused_node();
if !self.style.transitions.is_empty() {
view_state.style_anim = Some(AnimatedValue::new(self.style.clone()));
}
(child_el, view_state)
}
fn rebuild(&self, prev: &Self, ctx: &mut Context, element: &mut Self::Element) {
self.inner.rebuild(&prev.inner, ctx, &mut element.0);
let node = self.inner.get_node(&element.0);
element.1.is_focused = Some(node) == ctx.focused_node();
element.1.is_animating = self.apply_style(ctx, &mut element.1, node);
if element.1.is_animating {
ctx.request_frame();
}
}
fn rebuild_with_parent(
&self,
prev: &Self,
ctx: &mut Context,
element: &mut Self::Element,
parent: Node,
next_sibling: Option<Node>,
) {
self.inner
.rebuild_with_parent(&prev.inner, ctx, &mut element.0, parent, next_sibling);
let node = self.inner.get_node(&element.0);
element.1.is_focused = Some(node) == ctx.focused_node();
element.1.is_animating = self.apply_style(ctx, &mut element.1, node);
if element.1.is_animating {
ctx.request_frame();
}
}
fn teardown(&self, ctx: &mut Context, element: &mut Self::Element) {
self.inner.teardown(ctx, &mut element.0);
}
fn get_node(&self, element: &Self::Element) -> Node {
self.inner.get_node(&element.0)
}
fn handle_event(
&self,
element: &mut Self::Element,
state: &State,
event: Event,
ctx: &mut Context,
) -> (EventResult, Option<Self::Message>) {
let node = self.inner.get_node(&element.0);
let res = self
.inner
.handle_event(&mut element.0, state, event.clone(), ctx);
let (inner_handled, _) = res;
let mut state_changed = false;
let newly_focused = Some(node) == ctx.focused_node();
if element.1.is_focused != newly_focused {
element.1.is_focused = newly_focused;
state_changed = true;
}
match &event {
Event::CursorMoved { hit_nodes, .. } => {
let newly_hovered = hit_nodes.contains(&node);
if element.1.is_hovered != newly_hovered {
element.1.is_hovered = newly_hovered;
state_changed = true;
}
}
Event::MouseInput {
pressed, hit_nodes, ..
} => {
let is_hit = hit_nodes.contains(&node);
let new_active = if inner_handled == EventResult::Handled {
false
} else {
*pressed && is_hit
};
if element.1.is_active != new_active {
element.1.is_active = new_active;
state_changed = true;
}
}
_ => {}
}
let is_tick = matches!(event, Event::Tick { .. });
if state_changed || (is_tick && element.1.is_animating) {
element.1.is_animating = self.apply_style(ctx, &mut element.1, node);
if state_changed {
ctx.request_frame();
}
}
res
}
}
fn property_changed(prop: TransitionProperty, a: &Style, b: &Style) -> bool {
match prop {
TransitionProperty::All => true,
TransitionProperty::Opacity => {
(a.base_effects.opacity - b.base_effects.opacity).abs() > 1e-4
}
TransitionProperty::Scale => (a.base_effects.scale - b.base_effects.scale).abs() > 1e-4,
TransitionProperty::BackgroundColor => {
a.base_effects.background_color != b.base_effects.background_color
}
TransitionProperty::BorderColor => {
a.base_effects.border.color != b.base_effects.border.color
}
TransitionProperty::CornerRadius => {
a.base_effects.border.radius != b.base_effects.border.radius
}
TransitionProperty::Border => {
a.base_constraints.border != b.base_constraints.border
|| a.base_effects.border.color != b.base_effects.border.color
}
TransitionProperty::BoxShadow => {
a.base_effects.box_shadow != b.base_effects.box_shadow
|| a.base_effects.additional_shadows != b.base_effects.additional_shadows
}
TransitionProperty::Padding => a.base_constraints.padding != b.base_constraints.padding,
TransitionProperty::Width => a.base_constraints.width != b.base_constraints.width,
TransitionProperty::Height => a.base_constraints.height != b.base_constraints.height,
TransitionProperty::Size => {
a.base_constraints.width != b.base_constraints.width
|| a.base_constraints.height != b.base_constraints.height
}
TransitionProperty::Gap => (a.base_constraints.gap - b.base_constraints.gap).abs() > 1e-4,
TransitionProperty::TextColor => a.base_text_style.color != b.base_text_style.color,
TransitionProperty::FontSize => {
(a.base_text_style.font_size - b.base_text_style.font_size).abs() > 1e-4
}
TransitionProperty::Scrollbar => a.scrollbar != b.scrollbar,
}
}
impl<V> StyledView<V> {
fn compute_target_style(&self, view_state: &StyledViewState) -> Style {
if self.style.interaction.is_none() {
return self.style.clone();
}
let inter = self.style.interaction.as_ref().unwrap();
let mut target = self.style.clone();
if view_state.is_disabled {
if let Some(disabled) = &inter.disabled {
target = target.merge(disabled.clone());
}
}
if view_state.is_focused {
if let Some(focus) = &inter.focus {
target = target.merge(focus.clone());
}
}
if view_state.is_hovered {
if let Some(hover) = &inter.hover {
target = target.merge(hover.clone());
}
}
if view_state.is_active {
if let Some(active) = &inter.active {
target = target.merge(active.clone());
}
}
target
}
fn apply_style(&self, ctx: &mut Context, view_state: &mut StyledViewState, node: Node) -> bool {
let target_style = self.compute_target_style(view_state);
let mut is_animating = false;
let active_style = if !self.style.transitions.is_empty() {
let current_style = view_state
.style_anim
.as_ref()
.map(|a| &a.current)
.unwrap_or(&self.style);
let mut matching_transition = None;
for t in &self.style.transitions {
if t.property == TransitionProperty::All
|| property_changed(t.property, current_style, &target_style)
{
match matching_transition {
Some((dur, _)) if t.duration_ms > dur => {
matching_transition = Some((t.duration_ms, t.curve));
}
None => {
matching_transition = Some((t.duration_ms, t.curve));
}
_ => {}
}
}
}
let (transition_duration, transition_curve) =
matching_transition.unwrap_or_else(|| {
self.style
.transitions
.first()
.map(|t| (t.duration_ms, t.curve))
.unwrap_or((200.0, Curve::ease_out()))
});
if view_state.style_anim.is_none() {
view_state.style_anim = Some(AnimatedValue::new(target_style.clone()));
}
let anim = view_state.style_anim.as_mut().unwrap();
anim.set_target(
target_style.clone(),
now_ms(),
transition_duration,
transition_curve,
);
if anim.tick(now_ms()) {
is_animating = true;
anim.current.clone()
} else {
target_style.clone()
}
} else {
target_style.clone()
};
node.update_constraints(ctx, |c| {
let overflow = c.overflow;
let scroll = c.scroll;
let flex_dir = active_style.flex_direction.unwrap_or(c.flex_direction);
let is_both_fixed = matches!(
active_style.base_constraints.width,
Size::Fixed(_) | Size::Percent(_)
) && matches!(
active_style.base_constraints.height,
Size::Fixed(_) | Size::Percent(_)
);
let aspect_ratio = if active_style.base_constraints.aspect_ratio > 0.0 {
active_style.base_constraints.aspect_ratio
} else if is_both_fixed {
0.0
} else {
c.aspect_ratio
};
*c = active_style.base_constraints;
c.flex_direction = flex_dir;
c.aspect_ratio = aspect_ratio;
if active_style.base_constraints.overflow == Overflow::Visible
&& overflow != Overflow::Visible
{
c.overflow = overflow;
}
c.scroll = scroll;
});
if ctx.morph_suppressed_nodes.contains(&node) {
let mut suppressed = active_style.base_effects.clone();
suppressed.background_color = crate::Color::transparent;
suppressed.box_shadow = crate::BoxShadow::default();
suppressed.additional_shadows.clear();
suppressed.border.color = crate::Color::transparent;
node.set_effects(ctx, suppressed);
} else {
node.set_effects(ctx, active_style.base_effects.clone());
}
if let Some(sb) = &active_style.scrollbar {
node.set_scrollbar_style(ctx, (**sb).clone());
if sb.visibility == ScrollbarVisibility::Never {
node.update_constraints(ctx, |c| c.scrollbar_visible = false);
} else {
node.update_constraints(ctx, |c| c.scrollbar_visible = true);
}
}
if let Some(text) = node.get_text(ctx) {
let text_owned = text.to_string();
if active_style.base_text_style != TextStyle::default() {
if node.get_text_userdata::<TextRenderInfo>(ctx).is_none() {
node.set_text_with_userdata(
ctx,
&text_owned,
active_style.base_text_style.clone(),
);
}
}
}
if is_animating {
ctx.request_frame();
}
is_animating
}
}
pub struct KeyframedView<V> {
pub(crate) inner: V,
pub(crate) keyframes: Keyframes<Style>,
}
#[derive(Default)]
pub struct KeyframedViewState {
pub start_time: f64,
pub is_active: bool,
}
impl<State, V: View<State>> View<State> for KeyframedView<V> {
type Element = (V::Element, KeyframedViewState);
type Message = V::Message;
fn build(&self, ctx: &mut Context) -> Self::Element {
let child_el = self.inner.build(ctx);
let node = self.inner.get_node(&child_el);
let start_time = now_ms();
let (style, is_active) = self.keyframes.evaluate(0.0);
self.apply_evaluated_style(ctx, node, &style);
if is_active {
ctx.request_frame();
}
(
child_el,
KeyframedViewState {
start_time,
is_active,
},
)
}
fn rebuild(&self, prev: &Self, ctx: &mut Context, element: &mut Self::Element) {
self.inner.rebuild(&prev.inner, ctx, &mut element.0);
let node = self.inner.get_node(&element.0);
let elapsed = now_ms() - element.1.start_time;
let (style, is_active) = self.keyframes.evaluate(elapsed);
element.1.is_active = is_active;
self.apply_evaluated_style(ctx, node, &style);
if is_active {
ctx.request_frame();
}
}
fn teardown(&self, ctx: &mut Context, element: &mut Self::Element) {
self.inner.teardown(ctx, &mut element.0);
}
fn get_node(&self, element: &Self::Element) -> Node {
self.inner.get_node(&element.0)
}
fn handle_event(
&self,
element: &mut Self::Element,
state: &State,
event: Event,
ctx: &mut Context,
) -> (EventResult, Option<Self::Message>) {
if matches!(event, Event::Tick { .. }) && element.1.is_active {
let node = self.inner.get_node(&element.0);
let elapsed = now_ms() - element.1.start_time;
let (style, is_active) = self.keyframes.evaluate(elapsed);
element.1.is_active = is_active;
self.apply_evaluated_style(ctx, node, &style);
if is_active {
ctx.request_frame();
}
}
self.inner.handle_event(&mut element.0, state, event, ctx)
}
}
impl<V> KeyframedView<V> {
fn apply_evaluated_style(&self, ctx: &mut Context, node: Node, style: &Style) {
node.update_constraints(ctx, |c| {
let overflow = c.overflow;
let scroll = c.scroll;
let flex_dir = style.flex_direction.unwrap_or(c.flex_direction);
*c = style.base_constraints;
c.flex_direction = flex_dir;
if style.base_constraints.overflow == Overflow::Visible && overflow != Overflow::Visible
{
c.overflow = overflow;
}
c.scroll = scroll;
});
node.set_effects(ctx, style.base_effects.clone());
if let Some(text) = node.get_text(ctx) {
let text_owned = text.to_string();
if node.get_text_userdata::<TextRenderInfo>(ctx).is_none() {
node.set_text_with_userdata(ctx, &text_owned, style.base_text_style.clone());
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Edges;
use crate::FlexDirection;
use crate::ui::widgets::{column, row, text};
#[test]
fn test_row_flex_direction_preserved_when_styled() {
let mut ctx = Context::new();
let row_view =
row((text::<_, ()>("Left"), text::<_, ()>("Right"))).style(Style::new().padding(10.0));
let element = View::<()>::build(&row_view, &mut ctx);
let node = View::<()>::get_node(&row_view, &element);
let constraints = node.get_constraints(&ctx).unwrap();
assert_eq!(constraints.flex_direction, FlexDirection::Row);
}
#[test]
fn test_column_flex_direction_preserved_when_styled() {
let mut ctx = Context::new();
let col_view = column((text::<_, ()>("Top"), text::<_, ()>("Bottom")))
.style(Style::new().padding(10.0));
let element = View::<()>::build(&col_view, &mut ctx);
let node = View::<()>::get_node(&col_view, &element);
let constraints = node.get_constraints(&ctx).unwrap();
assert_eq!(constraints.flex_direction, FlexDirection::Column);
}
#[test]
fn test_row_flex_direction_explicit_override() {
let mut ctx = Context::new();
let row_view = row((text::<_, ()>("A"), text::<_, ()>("B")))
.style(Style::new().flex_direction(FlexDirection::Column));
let element = View::<()>::build(&row_view, &mut ctx);
let node = View::<()>::get_node(&row_view, &element);
let constraints = node.get_constraints(&ctx).unwrap();
assert_eq!(constraints.flex_direction, FlexDirection::Column);
}
#[test]
fn test_style_merge_and_mixins() {
let base = Style::new().padding(10.0).corner_radius(12.0);
let mixin = |s: Style| s.scale(1.2).opacity(0.8);
let combined = base.apply(mixin).when(true, |s| s.gap(8.0));
assert_eq!(combined.base_constraints.padding, Edges::all(10.0));
assert_eq!(combined.base_effects.scale, 1.2);
assert_eq!(combined.base_effects.opacity, 0.8);
assert_eq!(combined.base_constraints.gap, 8.0);
}
#[test]
fn test_frosted_glass_layout() {
use crate::rgb;
use crate::style::{AlignItems, JustifyContent, Size, Style, TextStyle};
use crate::text_property::FontWeight;
use crate::ui::ViewStyleExt;
use crate::ui::widgets::{column, row, text};
let mut ctx = Context::new();
ctx.set_text_sizing_func(move |ctx, _node, text, userdata, avail_w, avail_h| {
let default_style = TextStyle::default();
let (style, spans) = if let Some(info) =
userdata.and_then(|u| u.downcast_ref::<crate::TextRenderInfo>())
{
(&info.style, &info.spans[..])
} else if let Some(style) = userdata.and_then(|u| u.downcast_ref::<TextStyle>()) {
(style, &[][..])
} else {
(&default_style, &[][..])
};
let text_ctx = ctx.text_context.clone();
crate::text::measure_text(text, style, avail_w, avail_h, &text_ctx, spans)
});
let view = row((column((
row((
text::<_, ()>("Frosted Acrylic Glass Inspector").style(
Style::new().set_text_style(TextStyle {
font_size: 16.0,
color: rgb!(15, 23, 42),
font_weight: FontWeight::BOLD,
..Default::default()
}),
),
text::<_, ()>("Blur Active").style(
Style::new()
.padding_xy(10.0, 4.0)
.set_text_style(TextStyle {
font_size: 11.5,
color: rgb!(5, 150, 105),
..Default::default()
}),
),
))
.style(
Style::new()
.width(Size::Percent(1.0))
.align_items(AlignItems::Center)
.justify_content(JustifyContent::SpaceBetween),
),
))
.style(
Style::new()
.width(Size::Fixed(720))
.padding(22.0)
.blur(0.65),
),));
let element = View::<()>::build(&view, &mut ctx);
let root_node = View::<()>::get_node(&view, &element);
ctx.root_attach(root_node);
ctx.compute_layout(900.0, 720.0);
ctx.build_render_list(crate::style::Rect {
x: 0.0,
y: 0.0,
w: 900.0,
h: 720.0,
});
for (idx, cmd) in ctx.render_list().enumerate() {
println!(
"CMD {}: kind={:?}, node={:?}, computed={:?}, clip={:?}",
idx,
cmd.kind(),
cmd.node(),
cmd.computed(),
cmd.clip()
);
if let Some(txt) = cmd.node().get_text(&ctx) {
println!(" -> text: {:?}", txt);
}
}
println!(
"Node 3 text: {:?}",
crate::Node(crate::layout::NodeId {
index: 3,
generation: 0
})
.get_text(&ctx)
);
println!(
"Node 3 computed: {:?}",
crate::Node(crate::layout::NodeId {
index: 3,
generation: 0
})
.get_computed(&ctx)
);
}
#[test]
fn test_styled_view_focus_ring() {
let mut ctx = Context::new();
let view = crate::ui::widgets::input_text().style(
Style::new()
.border(1.0, crate::rgb!(100, 100, 100))
.on_focus(|s| s.border(3.0, crate::rgb!(0, 120, 255))),
);
let mut el = view.build(&mut ctx);
let node = view.get_node(&el);
let cons_unfocused = node.get_constraints(&ctx).unwrap();
assert_eq!(cons_unfocused.border.top, 1.0);
ctx.request_focus(node);
view.rebuild(&view, &mut ctx, &mut el);
let cons_focused = node.get_constraints(&ctx).unwrap();
assert_eq!(cons_focused.border.top, 3.0);
}
#[test]
fn test_nested_styled_view_active_suppression() {
use crate::ui::widgets::button;
let mut ctx = Context::new();
let inner_btn = button::<_, ()>("Inner Button");
let parent_card =
row((inner_btn,)).style(Style::new().scale(1.0).on_active(|s| s.scale(0.8)));
let mut el = View::<()>::build(&parent_card, &mut ctx);
let parent_node = View::<()>::get_node(&parent_card, &el);
let child_node = View::<()>::get_node(&parent_card.inner.children.0, &el.0.1.0);
let _ = View::<()>::handle_event(
&parent_card,
&mut el,
&(),
Event::MouseInput {
button: winit::event::MouseButton::Left,
pressed: true,
hit_nodes: vec![child_node, parent_node],
x: 0.0,
y: 0.0,
},
&mut ctx,
);
assert!(!el.1.is_active);
let parent_eff = ctx.effects.get(&parent_node).cloned().unwrap_or_default();
assert_eq!(parent_eff.scale, 1.0);
let _ = View::<()>::handle_event(
&parent_card,
&mut el,
&(),
Event::MouseInput {
button: winit::event::MouseButton::Left,
pressed: false,
hit_nodes: vec![child_node, parent_node],
x: 0.0,
y: 0.0,
},
&mut ctx,
);
let _ = View::<()>::handle_event(
&parent_card,
&mut el,
&(),
Event::MouseInput {
button: winit::event::MouseButton::Left,
pressed: true,
hit_nodes: vec![parent_node],
x: 0.0,
y: 0.0,
},
&mut ctx,
);
assert!(el.1.is_active);
let parent_eff = ctx.effects.get(&parent_node).cloned().unwrap_or_default();
assert_eq!(parent_eff.scale, 0.8);
}
#[test]
fn test_calculator_button_hover_and_layout_stability() {
use crate::Color;
use crate::style::{AlignItems, JustifyContent, Size};
use crate::ui::widgets::text;
let mut ctx = Context::new();
let btn0 = text::<_, ()>("7").style(
Style::new()
.padding(20.0)
.width(Size::Fill)
.height(Size::Fill)
.justify_content(JustifyContent::Center)
.align_items(AlignItems::Center)
.on_hover(|s| s.bg_color(Color::white)),
);
let btn1 = text::<_, ()>("8").style(
Style::new()
.padding(20.0)
.width(Size::Fill)
.height(Size::Fill)
.justify_content(JustifyContent::Center)
.align_items(AlignItems::Center)
.on_hover(|s| s.bg_color(Color::white)),
);
let btn2 = text::<_, ()>("9").style(
Style::new()
.padding(20.0)
.width(Size::Fill)
.height(Size::Fill)
.justify_content(JustifyContent::Center)
.align_items(AlignItems::Center)
.on_hover(|s| s.bg_color(Color::white)),
);
let btn3 = text::<_, ()>("/").style(
Style::new()
.padding(20.0)
.width(Size::Fill)
.height(Size::Fill)
.justify_content(JustifyContent::Center)
.align_items(AlignItems::Center)
.on_hover(|s| s.bg_color(Color::white)),
);
let row_view = row((btn0, btn1, btn2, btn3)).style(
Style::new()
.width(Size::Percent(1.0))
.height(Size::Fill)
.gap(10.0)
.flex_direction(FlexDirection::Row),
);
let root_view = column((row_view,)).style(
Style::new()
.width(Size::Fixed(400))
.height(Size::Fixed(400))
.flex_direction(FlexDirection::Column),
);
let mut el = View::<()>::build(&root_view, &mut ctx);
let root_node = View::<()>::get_node(&root_view, &el);
ctx.root_attach(root_node);
ctx.compute_layout(400.0, 400.0);
let row_el = &el.0.1.0;
let b0_node =
View::<()>::get_node(&root_view.inner.children.0.inner.children.0, &row_el.0.1.0);
let b1_node =
View::<()>::get_node(&root_view.inner.children.0.inner.children.1, &row_el.0.1.1);
let comp0 = b0_node.get_computed(&ctx).unwrap();
let comp1 = b1_node.get_computed(&ctx).unwrap();
assert!((comp0.w - 92.5).abs() < 1e-3, "comp0.w = {}", comp0.w);
assert!((comp1.w - 92.5).abs() < 1e-3, "comp1.w = {}", comp1.w);
let _ = View::<()>::handle_event(
&root_view,
&mut el,
&(),
Event::CursorMoved {
x: 10.0,
y: 10.0,
delta_x: 0.0,
delta_y: 0.0,
hit_nodes: vec![b0_node],
},
&mut ctx,
);
ctx.compute_layout(400.0, 400.0);
let comp0_after = b0_node.get_computed(&ctx).unwrap();
let comp1_after = b1_node.get_computed(&ctx).unwrap();
assert!(
(comp0_after.w - 92.5).abs() < 1e-3,
"comp0_after.w = {}",
comp0_after.w
);
assert!(
(comp1_after.w - 92.5).abs() < 1e-3,
"comp1_after.w = {}",
comp1_after.w
);
let _ = View::<()>::handle_event(
&root_view,
&mut el,
&(),
Event::CursorMoved {
x: 110.0,
y: 10.0,
delta_x: 0.0,
delta_y: 0.0,
hit_nodes: vec![b1_node],
},
&mut ctx,
);
ctx.compute_layout(400.0, 400.0);
let comp0_after2 = b0_node.get_computed(&ctx).unwrap();
let comp1_after2 = b1_node.get_computed(&ctx).unwrap();
assert!(
(comp0_after2.w - 92.5).abs() < 1e-3,
"comp0_after2.w = {}",
comp0_after2.w
);
assert!(
(comp1_after2.w - 92.5).abs() < 1e-3,
"comp1_after2.w = {}",
comp1_after2.w
);
}
#[test]
fn test_effects_merge_explicit_opacity_one() {
let base = Style::new().opacity(0.7);
let hover = Style::new().opacity(1.0);
let merged = base.merge(hover);
assert_eq!(merged.base_effects.opacity, 1.0);
let base2 = Style::new().opacity(0.7);
let active = Style::new().scale(0.95);
let merged2 = base2.merge(active);
assert_eq!(merged2.base_effects.opacity, 0.7);
assert_eq!(merged2.base_effects.scale, 0.95);
}
#[test]
fn test_opacity_hover_and_active_composition() {
let mut ctx = Context::new();
let styled = crate::ui::widgets::container((crate::ui::widgets::text::<_, ()>("test"),))
.style(
Style::new()
.opacity(0.7)
.on_hover(|s| s.opacity(1.0))
.on_active(|s| s.scale(0.98))
.transition(TransitionProperty::Opacity, 150.0, Curve::ease_in_out()),
);
let mut el = View::<()>::build(&styled, &mut ctx);
let node = View::<()>::get_node(&styled, &el);
let initial_effects = node.get_effects(&ctx).unwrap();
assert_eq!(initial_effects.opacity, 0.7);
assert_eq!(initial_effects.scale, 1.0);
let _ = View::<()>::handle_event(
&styled,
&mut el,
&(),
Event::CursorMoved {
x: 0.0,
y: 0.0,
delta_x: 0.0,
delta_y: 0.0,
hit_nodes: vec![node],
},
&mut ctx,
);
assert!(el.1.is_hovered);
let target = styled.compute_target_style(&el.1);
assert_eq!(target.base_effects.opacity, 1.0);
assert_eq!(target.base_effects.scale, 1.0);
let _ = View::<()>::handle_event(
&styled,
&mut el,
&(),
Event::MouseInput {
button: winit::event::MouseButton::Left,
pressed: true,
x: 0.0,
y: 0.0,
hit_nodes: vec![node],
},
&mut ctx,
);
assert!(el.1.is_hovered);
assert!(el.1.is_active);
let target_active = styled.compute_target_style(&el.1);
assert_eq!(target_active.base_effects.opacity, 1.0);
assert_eq!(target_active.base_effects.scale, 0.98);
let _ = View::<()>::handle_event(
&styled,
&mut el,
&(),
Event::MouseInput {
button: winit::event::MouseButton::Left,
pressed: false,
x: 0.0,
y: 0.0,
hit_nodes: vec![node],
},
&mut ctx,
);
assert!(el.1.is_hovered);
assert!(!el.1.is_active);
let target_hover_only = styled.compute_target_style(&el.1);
assert_eq!(target_hover_only.base_effects.opacity, 1.0);
assert_eq!(target_hover_only.base_effects.scale, 1.0);
let _ = View::<()>::handle_event(
&styled,
&mut el,
&(),
Event::CursorMoved {
x: 500.0,
y: 500.0,
delta_x: 0.0,
delta_y: 0.0,
hit_nodes: vec![],
},
&mut ctx,
);
assert!(!el.1.is_hovered);
assert!(!el.1.is_active);
let target_reset = styled.compute_target_style(&el.1);
assert_eq!(target_reset.base_effects.opacity, 0.7);
assert_eq!(target_reset.base_effects.scale, 1.0);
}
#[test]
fn test_property_specific_transition_matching() {
let mut ctx = Context::new();
let styled = crate::ui::widgets::container((crate::ui::widgets::text::<_, ()>("test"),))
.style(
Style::new()
.opacity(0.7)
.on_hover(|s| s.opacity(1.0))
.transition(TransitionProperty::Opacity, 150.0, Curve::ease_in_out()),
);
let mut el = View::<()>::build(&styled, &mut ctx);
let node = View::<()>::get_node(&styled, &el);
let _ = View::<()>::handle_event(
&styled,
&mut el,
&(),
Event::CursorMoved {
x: 0.0,
y: 0.0,
delta_x: 0.0,
delta_y: 0.0,
hit_nodes: vec![node],
},
&mut ctx,
);
assert!(el.1.is_hovered);
assert!(el.1.style_anim.is_some());
let anim = el.1.style_anim.as_ref().unwrap();
assert_eq!(anim.duration, 150.0);
assert_eq!(anim.curve, Curve::ease_in_out());
}
}