use crate::{
ActiveInteractionStates, ActiveMasksSecondary, ActiveTransition, AnimationCurve,
BaseBasicLayoutsSecondary, BasicLayout, BasicLayoutsSecondary, BorderAlignment, BorderStyle,
CapacityConfig, CascadeInteractionState, ChildrenSecondary, ClipRectsSecondary, Color,
ComponentMask, Context, CursorIcon, DEFAULT_BASIC, DebugStore, DirtyLayoutEntitiesVec,
EdgeInsets, EffectCategory, ElementEffectsSecondary, EntitiesSlot, EntityId, FocusTrigger,
Focusable, FontDate, GlobalCursorIcon, IDENTITY_MATRIX, InputContentsSparse, InteractionScope,
InteractionStyles, LayoutPoint, LayoutSize, LayoutStore, MichiuSoA, OutputStore,
ParentsSecondary, PlaybackCount, Point, PointerEvents, PropertyList, RectsSecondary,
ScrollbarDisplay, ScrollbarStylesSparse, StyleTarget, SystemStore, TaffyNodesSecondary,
TaffyTreeEntityId, TargetStyle, TextBufferSparse, ThisStyle, TopologyStore, TransitionValue,
UserSelect, Val, VisualProperty, define_secondary, define_sparse_secondary, define_vec,
};
use rustc_hash::{FxBuildHasher, FxHashSet};
use slotmap::{SecondaryMap, SparseSecondaryMap};
use std::{
sync::Arc,
time::{Duration, Instant},
};
#[derive(Debug, Clone)]
pub struct ActiveAnimation {
pub property: PropertyList,
pub start_time: Instant,
pub duration: Duration,
pub iteration_count: PlaybackCount,
pub curve: AnimationCurve,
pub start_value: TransitionValue,
pub end_value: TransitionValue,
}
define_secondary!(pub struct VisualPropertiesSecondary(VisualProperty));
define_secondary!(pub struct BaseVisualPropertiesSecondary(VisualProperty));
define_secondary!(pub struct InteractionPropertiesSecondary(InteractionStyles));
define_sparse_secondary!(pub struct ActiveTransitionsSparse(Vec<ActiveTransition>));
define_sparse_secondary!(pub struct ActiveAnimationsSparse(Vec<ActiveAnimation>));
define_vec!(pub struct DirtyRenderEntitiesVec(EntityId));
impl VisualPropertiesSecondary {
#[inline]
pub(crate) fn auto_wrap(&self, id: EntityId) -> bool {
self.find(id).and_then(|v| v.auto_wrap).unwrap_or(false)
}
#[inline]
pub(crate) fn font(&self, id: EntityId) -> FontDate {
self.find(id).map(|v| v.font.clone()).unwrap_or_default()
}
#[inline]
pub(crate) fn user_select(&self, id: EntityId) -> UserSelect {
self.find(id)
.and_then(|v| v.user_select)
.unwrap_or_default()
}
#[inline]
pub(crate) fn pointer_events(
&self,
id: EntityId,
rnd_base_visual: &BaseVisualPropertiesSecondary,
) -> PointerEvents {
self.find(id)
.and_then(|v| v.pointer_events)
.or_else(|| rnd_base_visual.find(id).and_then(|v| v.pointer_events))
.unwrap_or_default()
}
#[inline]
pub(crate) fn focusable(&self, id: EntityId) -> Focusable {
self.find(id).and_then(|v| v.focusable).unwrap_or_default()
}
}
pub type ActiveWebviewsHashSet = FxHashSet<EntityId>;
pub struct RenderStore {
pub(crate) rnd_dirty_entities: DirtyRenderEntitiesVec,
pub(crate) rnd_visual: VisualPropertiesSecondary,
pub(crate) rnd_base_visual: BaseVisualPropertiesSecondary,
pub(crate) rnd_interaction: InteractionPropertiesSecondary,
pub(crate) rnd_active_transitions: ActiveTransitionsSparse,
pub(crate) rnd_active_animations: ActiveAnimationsSparse,
pub(crate) rnd_active_webviews: ActiveWebviewsHashSet,
pub(crate) rnd_last_tick_time: Option<Instant>,
}
impl Default for RenderStore {
fn default() -> Self {
Self::new()
}
}
impl RenderStore {
#[must_use]
#[inline]
pub(crate) fn new() -> Self {
Self {
rnd_dirty_entities: DirtyRenderEntitiesVec(Vec::new()),
rnd_visual: VisualPropertiesSecondary(SecondaryMap::new()),
rnd_base_visual: BaseVisualPropertiesSecondary(SecondaryMap::new()),
rnd_interaction: InteractionPropertiesSecondary(SecondaryMap::new()),
rnd_active_transitions: ActiveTransitionsSparse(SparseSecondaryMap::new()),
rnd_active_animations: ActiveAnimationsSparse(SparseSecondaryMap::new()),
rnd_active_webviews: FxHashSet::default(),
rnd_last_tick_time: None,
}
}
#[inline]
#[must_use]
pub(crate) fn with_capacity(c: &CapacityConfig) -> Self {
Self {
rnd_dirty_entities: DirtyRenderEntitiesVec(Vec::with_capacity(c.rnd_dirty_entities)),
rnd_visual: VisualPropertiesSecondary(SecondaryMap::with_capacity(c.rnd_visual)),
rnd_base_visual: BaseVisualPropertiesSecondary(SecondaryMap::with_capacity(
c.rnd_base_visual,
)),
rnd_interaction: InteractionPropertiesSecondary(SecondaryMap::with_capacity(
c.rnd_interaction,
)),
rnd_active_transitions: ActiveTransitionsSparse(SparseSecondaryMap::with_capacity(
c.rnd_active_transitions,
)),
rnd_active_animations: ActiveAnimationsSparse(SparseSecondaryMap::with_capacity(
c.rnd_active_animations,
)),
rnd_active_webviews: FxHashSet::with_capacity_and_hasher(
c.rnd_active_webviews,
FxBuildHasher,
),
..Default::default()
}
}
#[inline]
pub(crate) fn clear(&mut self) {
self.rnd_dirty_entities.clear();
self.rnd_visual.clear();
self.rnd_base_visual.clear();
self.rnd_interaction.clear();
self.rnd_active_transitions.clear();
self.rnd_active_animations.clear();
self.rnd_active_webviews.clear();
self.rnd_last_tick_time = None;
}
#[inline]
pub(crate) fn despawn(&mut self, id: EntityId) {
self.rnd_dirty_entities.retain(|&x| x != id);
self.rnd_visual.remove(id);
self.rnd_base_visual.remove(id);
self.rnd_interaction.remove(id);
self.rnd_active_transitions.remove(id);
self.rnd_active_animations.remove(id);
self.rnd_active_webviews.remove(&id);
}
}
impl RenderStore {
#[inline]
pub(crate) fn mark_render_dirty(
id: EntityId,
topo_active_masks: &mut ActiveMasksSecondary,
rnd_dirty_entities: &mut DirtyRenderEntitiesVec,
) {
let mask = topo_active_masks.at_mut(id);
if mask.has(ComponentMask::STATE_QUEUED_RENDER) {
return;
}
mask.set(ComponentMask::STATE_QUEUED_RENDER);
rnd_dirty_entities.push(id);
}
#[inline]
pub(crate) fn clear_render_dirty(
topo_active_masks: &mut ActiveMasksSecondary,
rnd_dirty_entities: &mut DirtyRenderEntitiesVec,
) {
for id in rnd_dirty_entities.drain(..) {
let Some(mask) = topo_active_masks.find_mut(id) else {
continue;
};
mask.unset(ComponentMask::STATE_QUEUED_RENDER);
}
rnd_dirty_entities.clear();
}
#[inline]
pub(crate) fn get_visual_property_mut<'a>(
id: EntityId,
target: StyleTarget,
rnd_base_visual: &'a mut BaseVisualPropertiesSecondary,
rnd_interaction: &'a mut InteractionPropertiesSecondary,
) -> &'a mut VisualProperty {
if target == StyleTarget::Base {
rnd_base_visual.at_mut(id)
} else {
if !rnd_interaction.contains_key(id) {
rnd_interaction.insert(id, InteractionStyles::default());
}
let styles = rnd_interaction.at_mut(id);
let style_ref = styles.get_style_target_mut(target);
&mut Arc::make_mut(&mut style_ref.inner).visual_property
}
}
pub(crate) fn has_active_frame(
evt_interaction_states: &ActiveInteractionStates,
evt_current_pointer_position: Option<&LayoutPoint>,
cont_input_contents: &InputContentsSparse,
bar_styles: &ScrollbarStylesSparse,
rnd_visual: &VisualPropertiesSecondary,
rnd_active_transitions: &ActiveTransitionsSparse,
rnd_active_animations: &ActiveAnimationsSparse,
out_clip_rects: &ClipRectsSecondary,
) -> bool {
let has_drag_autoscroll = OutputStore::is_drag_autoscroll_active(
evt_interaction_states,
evt_current_pointer_position,
rnd_visual,
out_clip_rects,
);
let has_transitions = !rnd_active_transitions.is_empty()
&& rnd_active_transitions.values().any(|list| !list.is_empty());
let has_keyframes = !rnd_active_animations.is_empty()
&& rnd_active_animations.values().any(|list| !list.is_empty());
let has_blinking_input = evt_interaction_states
.focused
.and_then(|id| cont_input_contents.find(id))
.is_some_and(|c| c.has_caret && c.is_blink);
let has_active_transient_scrollbar = bar_styles.values().any(|sb_state| {
sb_state.style.display == ScrollbarDisplay::Transient
&& sb_state
.last_scroll_time
.is_some_and(|t| t.elapsed() < Duration::from_millis(1500))
});
has_transitions
|| has_keyframes
|| has_blinking_input
|| has_active_transient_scrollbar
|| has_drag_autoscroll
}
pub(crate) fn does_state_require_layout(
id: EntityId,
state_flag: u128,
rnd_interaction: &InteractionPropertiesSecondary,
) -> bool {
let Some(interaction) = rnd_interaction.find(id) else {
return false;
};
let target_style = interaction.get_self_style(state_flag);
let Some(style) = target_style else {
return false;
};
let mask = style.inner.mask;
mask.has_basic_layout()
|| mask.has_flex_layout()
|| mask.has_grid_layout()
|| mask.has(ComponentMask::STYLE_FONT_SIZE)
|| mask.has(ComponentMask::STYLE_AUTO_WRAP)
}
pub(crate) fn resolv_focus_style(
id: EntityId,
active_mask: &ComponentMask,
flag: u128,
topo_parents: &ParentsSecondary,
rnd_visual: &VisualPropertiesSecondary,
rnd_interaction: &InteractionPropertiesSecondary,
) -> Option<ThisStyle> {
if !active_mask.has(flag) {
return None;
}
let self_style = rnd_interaction.find(id).and_then(|interaction| {
if flag == ComponentMask::STATE_FOCUSED_VISIBLE {
interaction.focused_visible.clone()
} else {
interaction.focused.clone()
}
});
if let Some(style) = self_style {
return Some(style);
}
let focus_mode = rnd_visual.focusable(id);
let is_trigger_match = match (flag, focus_mode) {
(ComponentMask::STATE_FOCUSED, Focusable::Inherit(_)) => true,
(ComponentMask::STATE_FOCUSED_VISIBLE, Focusable::Inherit(trigger)) => {
trigger == FocusTrigger::Keyboard || trigger == FocusTrigger::Both
}
_ => false,
};
if !is_trigger_match {
return None;
}
let mut curr = *topo_parents.at(id);
while let Some(curr_id) = curr {
if let Some(parent_interaction) = rnd_interaction.find(curr_id) {
let parent_style = if flag == ComponentMask::STATE_FOCUSED_VISIBLE {
&parent_interaction.focused_visible
} else {
&parent_interaction.focused
};
if let Some(p_style) = parent_style {
return Some(p_style.clone());
}
}
curr = *topo_parents.at(curr_id);
}
None
}
#[inline]
pub(crate) fn cascade_self_interaction(
interaction: &InteractionStyles,
active_mask: &ComponentMask,
resolved_focus: Option<&ThisStyle>,
resolved_focus_visible: Option<&ThisStyle>,
mut apply: impl FnMut(&ThisStyle),
) {
for state in CascadeInteractionState::ALL {
if !active_mask.has(state.mask()) {
continue;
}
let style = match state {
CascadeInteractionState::Focused => resolved_focus.or(interaction.focused.as_ref()),
CascadeInteractionState::FocusedVisible => {
resolved_focus_visible.or(interaction.focused_visible.as_ref())
}
_ => interaction.get_scope_style(InteractionScope::SelfTarget, state),
};
if let Some(style) = style {
apply(style);
}
}
}
#[inline]
pub(crate) fn cascade_relational_interaction(
interaction: &InteractionStyles,
scope: InteractionScope,
mut has_relation_state: impl FnMut(u128) -> bool,
mut apply: impl FnMut(&ThisStyle),
) {
for state in CascadeInteractionState::ALL {
let Some(style) = interaction.get_scope_style(scope, state) else {
continue;
};
if has_relation_state(state.mask()) {
apply(style);
}
}
let any_style = match scope {
InteractionScope::Parent => interaction.any_parent.as_ref(),
InteractionScope::Within => interaction.any_within.as_ref(),
InteractionScope::SelfTarget => None,
};
if let Some(style) = any_style
&& has_relation_state(ComponentMask::STYLE_ACTIVE_INTERACTION_PROPERTY)
{
apply(style);
}
}
#[inline]
pub(crate) fn apply_basic_layout_cascade(
id: EntityId,
target_layout: &mut BasicLayout,
active_mask: &ComponentMask,
resolved_focus: Option<&ThisStyle>,
resolved_focus_visible: Option<&ThisStyle>,
topo_entities: &EntitiesSlot,
topo_active_masks: &ActiveMasksSecondary,
topo_parents: &ParentsSecondary,
topo_children: &ChildrenSecondary,
rnd_interaction: &InteractionPropertiesSecondary,
) {
let Some(interaction) = rnd_interaction.find(id) else {
return;
};
RenderStore::cascade_self_interaction(
interaction,
active_mask,
resolved_focus,
resolved_focus_visible,
|style| {
target_layout.override_with(&style.inner.basic_layout, style.inner.mask);
},
);
if active_mask.has(ComponentMask::STYLE_INTERACTION_PARENT) {
RenderStore::cascade_relational_interaction(
interaction,
InteractionScope::Parent,
|flag| {
TopologyStore::has_parent_with_state(
id,
flag,
topo_entities,
topo_active_masks,
topo_parents,
)
},
|style| target_layout.override_with(&style.inner.basic_layout, style.inner.mask),
);
}
if active_mask.has(ComponentMask::STYLE_INTERACTION_WITHIN) {
RenderStore::cascade_relational_interaction(
interaction,
InteractionScope::Within,
|flag| {
TopologyStore::has_descendant_with_state(
id,
flag,
topo_entities,
topo_active_masks,
topo_children,
)
},
|style| target_layout.override_with(&style.inner.basic_layout, style.inner.mask),
);
}
}
#[inline]
pub(crate) fn apply_interaction_cascades(
id: EntityId,
target: &mut TargetStyle,
active_mask: &ComponentMask,
resolved_focus: Option<&ThisStyle>,
resolved_focus_visible: Option<&ThisStyle>,
topo_entities: &EntitiesSlot,
topo_active_masks: &ActiveMasksSecondary,
topo_parents: &ParentsSecondary,
topo_children: &ChildrenSecondary,
rnd_interaction: &InteractionPropertiesSecondary,
) {
let Some(interaction) = rnd_interaction.find(id) else {
return;
};
RenderStore::cascade_self_interaction(
interaction,
active_mask,
resolved_focus,
resolved_focus_visible,
|style| {
style.inner.apply_visual_property(target);
},
);
if active_mask.has(ComponentMask::STYLE_INTERACTION_PARENT) {
RenderStore::cascade_relational_interaction(
interaction,
InteractionScope::Parent,
|flag| {
TopologyStore::has_parent_with_state(
id,
flag,
topo_entities,
topo_active_masks,
topo_parents,
)
},
|style| style.inner.apply_visual_property(target),
);
}
if active_mask.has(ComponentMask::STYLE_INTERACTION_WITHIN) {
RenderStore::cascade_relational_interaction(
interaction,
InteractionScope::Within,
|flag| {
TopologyStore::has_descendant_with_state(
id,
flag,
topo_entities,
topo_active_masks,
topo_children,
)
},
|style| style.inner.apply_visual_property(target),
);
}
}
pub(crate) fn apply_animation_value(
id: EntityId,
property: PropertyList,
value: &TransitionValue,
topo_active_masks: &mut ActiveMasksSecondary,
topo_parents: &ParentsSecondary,
lay_dirty_entities: &mut DirtyLayoutEntitiesVec,
lay_taffy_tree: &mut TaffyTreeEntityId,
lay_basic: &mut BasicLayoutsSecondary,
lay_taffy_nodes: &TaffyNodesSecondary,
rnd_visual: &mut VisualPropertiesSecondary,
debug: &mut DebugStore,
) {
if !rnd_visual.contains_key(id) {
rnd_visual.insert(id, VisualProperty::default());
}
let v = rnd_visual.at_mut(id);
let mut is_layout_dirty = false;
match *value {
TransitionValue::Color(c) => {
if property == PropertyList::BackgroundColor {
v.bg_color = Some(c);
} else if property == PropertyList::BorderColor {
v.border_color = Some(c);
}
}
TransitionValue::Opacity(o) => {
v.opacity = Some(o);
}
TransitionValue::Transform(m) => {
v.transform = Some(m);
}
TransitionValue::CornerRadius(cr) => {
v.corner_radius = Some(cr);
}
TransitionValue::Width(w) => {
let layout = lay_basic.at_mut(id);
layout.size.width = Val::Px(w);
is_layout_dirty = true;
}
TransitionValue::Height(h) => {
let layout = lay_basic.at_mut(id);
layout.size.height = Val::Px(h);
is_layout_dirty = true;
}
TransitionValue::BoxShadow(shadow) => {
v.shadow_params = Some(shadow);
v.shadow_color = Some(shadow.color);
}
}
if is_layout_dirty {
LayoutStore::mark_layout_dirty(
id,
topo_active_masks,
topo_parents,
lay_dirty_entities,
lay_taffy_tree,
lay_taffy_nodes,
debug,
);
}
}
fn trigger_keyframe_animations_if_needed(
id: EntityId,
rnd_active_animations: &mut ActiveAnimationsSparse,
rnd_visual: &VisualPropertiesSecondary,
) {
let Some(visual) = rnd_visual.find(id) else {
return;
};
if visual.keyframe_animations.is_empty() {
return;
}
let now = Instant::now();
if !rnd_active_animations.contains_key(id) {
rnd_active_animations.insert(id, Vec::new());
}
let active_list = rnd_active_animations.at_mut(id);
for anim in &visual.keyframe_animations {
if active_list.iter().any(|a| a.property == anim.property) {
continue;
}
let (start_val, end_val) = match anim.property {
PropertyList::Transform => {
let start = TransitionValue::Transform(IDENTITY_MATRIX);
let end_transform = crate::Transform::new().rotate(std::f32::consts::PI * 2.0);
let end = TransitionValue::Transform(end_transform.matrix);
(start, end)
}
PropertyList::Opacity => {
(TransitionValue::Opacity(1.0), TransitionValue::Opacity(0.0)) }
_ => continue, };
active_list.push(ActiveAnimation {
property: anim.property,
start_time: now,
duration: anim.duration,
iteration_count: anim.iteration_count,
curve: anim.curve,
start_value: start_val,
end_value: end_val,
});
}
}
pub(crate) fn resolve_element_style_state(
id: EntityId,
allow_transition: bool,
win_last_size: Option<LayoutSize>,
sys_text_buffers: &TextBufferSparse,
react_element_effects: &ElementEffectsSecondary,
cont_input_contents: &InputContentsSparse,
topo_active_masks: &mut ActiveMasksSecondary,
topo_is_sort_dirty: &mut bool,
topo_entities: &EntitiesSlot,
topo_parents: &ParentsSecondary,
topo_children: &ChildrenSecondary,
lay_dirty_entities: &mut DirtyLayoutEntitiesVec,
lay_taffy_tree: &mut TaffyTreeEntityId,
lay_basic: &mut BasicLayoutsSecondary,
lay_taffy_nodes: &TaffyNodesSecondary,
lay_base_basic: &BaseBasicLayoutsSecondary,
rnd_dirty_entities: &mut DirtyRenderEntitiesVec,
rnd_visual: &mut VisualPropertiesSecondary,
rnd_active_transitions: &mut ActiveTransitionsSparse,
rnd_active_animations: &mut ActiveAnimationsSparse,
rnd_base_visual: &BaseVisualPropertiesSecondary,
rnd_interaction: &InteractionPropertiesSecondary,
out_rects: &RectsSecondary,
debug: &mut DebugStore,
) {
RenderStore::resolve_visual_styles(
id,
allow_transition,
sys_text_buffers,
react_element_effects,
cont_input_contents,
topo_active_masks,
topo_is_sort_dirty,
topo_entities,
topo_parents,
topo_children,
lay_dirty_entities,
lay_taffy_tree,
lay_taffy_nodes,
rnd_dirty_entities,
rnd_visual,
rnd_active_transitions,
rnd_base_visual,
rnd_interaction,
debug,
);
RenderStore::resolve_layout_styles(
id,
allow_transition,
win_last_size,
react_element_effects,
topo_entities,
topo_active_masks,
topo_parents,
topo_children,
lay_dirty_entities,
lay_taffy_tree,
lay_basic,
lay_taffy_nodes,
lay_base_basic,
rnd_active_transitions,
rnd_visual,
rnd_base_visual,
rnd_interaction,
out_rects,
debug,
);
RenderStore::trigger_keyframe_animations_if_needed(id, rnd_active_animations, rnd_visual);
}
fn resolve_layout_styles(
id: EntityId,
allow_transition: bool,
win_last_size: Option<LayoutSize>,
react_element_effects: &ElementEffectsSecondary,
topo_entities: &EntitiesSlot,
topo_active_masks: &mut ActiveMasksSecondary,
topo_parents: &ParentsSecondary,
topo_children: &ChildrenSecondary,
lay_dirty_entities: &mut DirtyLayoutEntitiesVec,
lay_taffy_tree: &mut TaffyTreeEntityId,
lay_basic: &mut BasicLayoutsSecondary,
lay_taffy_nodes: &TaffyNodesSecondary,
lay_base_basic: &BaseBasicLayoutsSecondary,
rnd_active_transitions: &mut ActiveTransitionsSparse,
rnd_visual: &VisualPropertiesSecondary,
rnd_base_visual: &BaseVisualPropertiesSecondary,
rnd_interaction: &InteractionPropertiesSecondary,
out_rects: &RectsSecondary,
debug: &mut DebugStore,
) {
let active_mask = topo_active_masks.at(id);
let has_base_layout = lay_base_basic.contains_key(id);
let has_active_layout = lay_basic.contains_key(id);
if !has_base_layout && !has_active_layout {
return;
}
let active_layout = lay_basic.find_or(id, &DEFAULT_BASIC, debug);
let base_layout = lay_base_basic.find_or_default(id, debug);
let mut target_layout = base_layout;
let resolv_focus = |flag| {
RenderStore::resolv_focus_style(
id,
active_mask,
flag,
topo_parents,
rnd_visual,
rnd_interaction,
)
};
let resolved_focus = resolv_focus(ComponentMask::STATE_FOCUSED);
let resolved_focus_visible = resolv_focus(ComponentMask::STATE_FOCUSED_VISIBLE);
RenderStore::apply_basic_layout_cascade(
id,
&mut target_layout,
active_mask,
resolved_focus.as_ref(),
resolved_focus_visible.as_ref(),
topo_entities,
topo_active_masks,
topo_parents,
topo_children,
rnd_interaction,
);
let mut to_px = |val, is_width| {
OutputStore::val_to_px(
id,
val,
is_width,
win_last_size,
topo_parents,
out_rects,
debug,
)
};
let target_w = to_px(target_layout.size.width, true);
let current_w = to_px(active_layout.size.width, true);
let target_h = to_px(target_layout.size.height, false);
let current_h = to_px(active_layout.size.height, false);
let mut width_triggered = false;
let mut height_triggered = false;
let mut if_needed = |prop, start, end| {
RenderStore::trigger_transition_if_needed(
id,
prop,
start,
end,
react_element_effects,
rnd_active_transitions,
rnd_base_visual,
)
};
let can_trigger_width = rnd_visual.find(id).is_some_and(|v| {
v.transitions.iter().any(|t| {
t.property_list == PropertyList::Width || t.property_list == PropertyList::Size
})
});
if allow_transition
&& can_trigger_width
&& has_active_layout
&& (current_w - target_w).abs() > 0.01
{
width_triggered = if_needed(
PropertyList::Width,
TransitionValue::Width(current_w),
TransitionValue::Width(target_w),
);
}
let can_trigger_height = rnd_visual.find(id).is_some_and(|v| {
v.transitions.iter().any(|t| {
t.property_list == PropertyList::Height || t.property_list == PropertyList::Size
})
});
if allow_transition
&& can_trigger_height
&& has_active_layout
&& (current_h - target_h).abs() > 0.01
{
height_triggered = if_needed(
PropertyList::Height,
TransitionValue::Height(current_h),
TransitionValue::Height(target_h),
);
}
if !lay_basic.contains_key(id) {
lay_basic.insert(id, BasicLayout::default());
}
let active_layout_mut = lay_basic.at_mut(id);
let is_layout_changed = *active_layout_mut != target_layout;
*active_layout_mut = target_layout;
if width_triggered {
active_layout_mut.size.width = Val::Px(current_w);
}
if height_triggered {
active_layout_mut.size.height = Val::Px(current_h);
}
if is_layout_changed {
LayoutStore::mark_layout_dirty(
id,
topo_active_masks,
topo_parents,
lay_dirty_entities,
lay_taffy_tree,
lay_taffy_nodes,
debug,
);
}
}
fn resolve_visual_styles(
id: EntityId,
allow_transition: bool,
sys_text_buffers: &TextBufferSparse,
react_element_effects: &ElementEffectsSecondary,
cont_input_contents: &InputContentsSparse,
topo_active_masks: &mut ActiveMasksSecondary,
topo_is_sort_dirty: &mut bool,
topo_entities: &EntitiesSlot,
topo_parents: &ParentsSecondary,
topo_children: &ChildrenSecondary,
lay_dirty_entities: &mut DirtyLayoutEntitiesVec,
lay_taffy_tree: &mut TaffyTreeEntityId,
lay_taffy_nodes: &TaffyNodesSecondary,
rnd_dirty_entities: &mut DirtyRenderEntitiesVec,
rnd_visual: &mut VisualPropertiesSecondary,
rnd_active_transitions: &mut ActiveTransitionsSparse,
rnd_base_visual: &BaseVisualPropertiesSecondary,
rnd_interaction: &InteractionPropertiesSecondary,
debug: &mut DebugStore,
) {
let active_mask = topo_active_masks.at(id);
let has_base_visual = rnd_base_visual.contains_key(id);
let has_active_visual = rnd_visual.contains_key(id);
let has_interaction_styles = rnd_interaction.contains_key(id);
if !has_base_visual && !has_active_visual && !has_interaction_styles {
return;
}
let current = VisualProperty::get_current_style(id, rnd_visual);
let mut target = VisualProperty::get_target_style(id, rnd_base_visual);
let resolv_focus = |flag| {
RenderStore::resolv_focus_style(
id,
active_mask,
flag,
topo_parents,
rnd_visual,
rnd_interaction,
)
};
let focused_style_resolved = resolv_focus(ComponentMask::STATE_FOCUSED);
let focused_visible_style_resolved = resolv_focus(ComponentMask::STATE_FOCUSED_VISIBLE);
RenderStore::apply_interaction_cascades(
id,
&mut target,
active_mask,
focused_style_resolved.as_ref(),
focused_visible_style_resolved.as_ref(),
topo_entities,
topo_active_masks,
topo_parents,
topo_children,
rnd_interaction,
);
let mut is_placeholder_active = false;
if let Some(contents) = cont_input_contents.find(id) {
let has_no_ime = contents
.ime_state
.as_ref()
.is_none_or(|s| s.composition_text.is_empty());
if contents.text_empty() && has_no_ime {
is_placeholder_active = true;
}
}
let target_bg_val = target.bg_color.unwrap_or_default();
let bg_changed = current.bg_color != target_bg_val;
let target_border_val = target.border_color.unwrap_or_default();
let border_changed = current.border_color != target_border_val;
let target_outline_width_val = target.outline_width.unwrap_or_default();
let outline_width_changed = current.outline_width != target_outline_width_val;
let target_outline_color_val = target.outline_color.unwrap_or_default();
let outline_color_changed = current.outline_color != target_outline_color_val;
let target_outline_offset_val = target.outline_offset.unwrap_or(0.0);
let outline_offset_changed =
(current.outline_offset - target_outline_offset_val).abs() > 0.001;
let target_opacity_val = target.opacity.unwrap_or(1.0);
let opacity_changed = (current.opacity - target_opacity_val).abs() > 0.001;
let target_transform_val = target.transform.unwrap_or(IDENTITY_MATRIX);
let transform_changed = current.transform != target_transform_val;
let target_transform_origin_val = target.transform_origin.unwrap_or(Point::ORIGIN);
let transform_origin_changed = current.transform_origin != target_transform_origin_val;
let target_radius_val = target.corner_radius.unwrap_or_default();
let radius_changed = current.corner_radius != target_radius_val;
let target_shadow_val = target.shadow_params.unwrap_or_default();
let shadow_changed = current.shadow_params != target_shadow_val;
let target_text_color = target.text_color.unwrap_or(Color::WHITE);
let text_color_changed = current.text_color != target_text_color;
let target_font_size = target.font.size.unwrap_or(16.0);
let target_font_family = target.font.family.clone();
let target_font_weight = target.font.weight.unwrap_or(400);
let target_font_style = target.font.style.unwrap_or(0);
let target_auto_wrap = target.auto_wrap.unwrap_or(false);
let font_changed = (current.font_size - target_font_size).abs() >= 0.01
|| current.font_family != target_font_family
|| current.font_weight != target_font_weight
|| current.font_style != target_font_style
|| current.auto_wrap != target_auto_wrap;
let target_pointer_events = target.pointer_events.unwrap_or_default();
let pointer_events_changed = current.pointer_events != target_pointer_events;
let mut if_needed = |prop, start, end| {
RenderStore::trigger_transition_if_needed(
id,
prop,
start,
end,
react_element_effects,
rnd_active_transitions,
rnd_base_visual,
)
};
let mut bg_triggered = false;
if allow_transition && bg_changed && has_active_visual {
bg_triggered = if_needed(
PropertyList::BackgroundColor,
TransitionValue::Color(current.bg_color),
TransitionValue::Color(target_bg_val),
);
}
let mut border_triggered = false;
if border_changed && has_active_visual {
border_triggered = if_needed(
PropertyList::BorderColor,
TransitionValue::Color(current.border_color),
TransitionValue::Color(target_border_val),
);
}
let mut opacity_triggered = false;
if opacity_changed && has_active_visual {
opacity_triggered = if_needed(
PropertyList::Opacity,
TransitionValue::Opacity(current.opacity),
TransitionValue::Opacity(target_opacity_val),
);
}
let mut transform_triggered = false;
if transform_changed {
transform_triggered = if_needed(
PropertyList::Transform,
TransitionValue::Transform(current.transform),
TransitionValue::Transform(target_transform_val),
);
}
let mut radius_triggered = false;
if radius_changed && has_active_visual {
radius_triggered = if_needed(
PropertyList::CornerRadius,
TransitionValue::CornerRadius(current.corner_radius),
TransitionValue::CornerRadius(target_radius_val),
);
}
let mut shadow_triggered = false;
if shadow_changed && has_active_visual {
shadow_triggered = if_needed(
PropertyList::BoxShadow,
TransitionValue::BoxShadow(current.shadow_params),
TransitionValue::BoxShadow(target_shadow_val),
);
}
if bg_triggered
|| border_triggered
|| opacity_triggered
|| transform_triggered
|| radius_triggered
|| shadow_triggered
|| bg_changed
|| border_changed
|| opacity_changed
|| transform_changed
|| radius_changed
|| shadow_changed
|| outline_width_changed
|| outline_color_changed
|| outline_offset_changed
|| text_color_changed
|| font_changed
|| pointer_events_changed
|| rnd_base_visual.contains_key(id)
{
if !rnd_visual.contains_key(id) {
rnd_visual.insert(id, VisualProperty::default());
}
let active_vis = rnd_visual.at_mut(id);
if !bg_triggered {
active_vis.bg_color = target.bg_color;
}
if !border_triggered {
active_vis.border_color = target.border_color;
}
if !opacity_triggered {
active_vis.opacity = target.opacity;
}
if !transform_triggered {
active_vis.transform = target.transform;
active_vis.transform_origin = target.transform_origin;
}
if !radius_triggered {
active_vis.corner_radius = target.corner_radius;
}
if is_placeholder_active {
active_vis.text_color = Some(Color::rgb_f32(0.5, 0.5, 0.5));
} else {
active_vis.text_color = target.text_color;
}
if !shadow_triggered {
active_vis.shadow_params = target.shadow_params;
active_vis.shadow_color = target.shadow_color;
}
active_vis.border_lengths = target.border_lengths;
active_vis.border_styles = target.border_styles;
active_vis.border_alignments = target.border_alignments;
active_vis.outline_width = target.outline_width;
active_vis.outline_color = target.outline_color;
active_vis.outline_lengths = target.outline_lengths;
active_vis.outline_styles = target.outline_styles;
active_vis.outline_alignments = target.outline_alignments;
active_vis.outline_offset = target.outline_offset;
active_vis.select_bg_color = target.select_bg_color;
active_vis.select_text_color = target.select_text_color;
active_vis.cursor = target.cursor;
active_vis.resizable_cursor = target.resizable_cursor;
active_vis.font.clone_from(&target.font);
active_vis.auto_wrap = target.auto_wrap;
active_vis.pointer_events = target.pointer_events;
if let Some(base_vis) = rnd_base_visual.find(id) {
active_vis.z_index = base_vis.z_index;
active_vis.backdrop = base_vis.backdrop;
active_vis.bg_gradient = base_vis.bg_gradient;
active_vis.transitions.clone_from(&base_vis.transitions);
active_vis
.keyframe_animations
.clone_from(&base_vis.keyframe_animations);
active_vis.focusable = base_vis.focusable;
active_vis.prevent_focus_steal = base_vis.prevent_focus_steal;
active_vis.prevent_focus_steal_within = base_vis.prevent_focus_steal_within;
active_vis.transform_inherit = target.transform_inherit;
active_vis.user_select = base_vis.user_select;
}
if font_changed {
SystemStore::clear_text_buffer_cache(id, sys_text_buffers);
LayoutStore::mark_layout_dirty(
id,
topo_active_masks,
topo_parents,
lay_dirty_entities,
lay_taffy_tree,
lay_taffy_nodes,
debug,
);
}
if transform_changed || transform_origin_changed {
*topo_is_sort_dirty = true;
}
RenderStore::mark_render_dirty(id, topo_active_masks, rnd_dirty_entities);
}
}
pub(crate) fn trigger_transition_if_needed(
id: EntityId,
property_list: PropertyList,
start_value: TransitionValue,
end_value: TransitionValue,
react_element_effects: &ElementEffectsSecondary,
rnd_active_transitions: &mut ActiveTransitionsSparse,
rnd_base_visual: &BaseVisualPropertiesSecondary,
) -> bool {
let is_style_evaluating = crate::signal::ACTIVE_EFFECT.with(|cell| {
if let Some(effect_id) = cell.get() {
react_element_effects.values().any(|list| {
list.iter()
.any(|(cat, eff_id)| *eff_id == effect_id && *cat == EffectCategory::Style)
})
} else {
false
}
});
if is_style_evaluating {
return false;
}
let Some(visual) = rnd_base_visual.find(id) else {
return false;
};
let Some(t) = visual
.transitions
.iter()
.find(|t| t.property_list == property_list || t.property_list == PropertyList::Size)
else {
return false;
};
let Some(entry) = rnd_active_transitions.entry(id) else {
return false;
};
let active_list = entry.or_insert_with(Vec::new);
let now = Instant::now();
if let Some(existing) = active_list
.iter_mut()
.find(|et| et.property_list == property_list)
{
if existing.end_value == end_value {
return true;
}
let elapsed = existing
.start_time
.map_or(Duration::ZERO, |st| now.duration_since(st));
let progress = (elapsed.as_secs_f32() / existing.duration.as_secs_f32()).min(1.0);
let eased_t = existing.curve.evaluate(progress);
let current_interposed_val = existing.start_value.lerp(&existing.end_value, eased_t);
existing.start_time = None;
existing.start_value = current_interposed_val;
existing.end_value = end_value;
existing.duration = t.duration;
existing.curve = t.curve;
return true; }
active_list.push(ActiveTransition {
property_list,
start_time: None,
duration: t.duration,
curve: t.curve,
start_value, end_value,
});
true
}
pub(crate) fn resolve_cursor(
hovered_id: EntityId,
evt_interaction_states: &ActiveInteractionStates,
topo_parents: &ParentsSecondary,
rnd_visual: &VisualPropertiesSecondary,
rnd_base_visual: &BaseVisualPropertiesSecondary,
) -> CursorIcon {
let start_id = evt_interaction_states.pressed.unwrap_or(hovered_id);
let mut curr = Some(start_id);
let mut global_cursor = None;
while let Some(id) = curr {
let cursor_opt = rnd_visual
.find(id)
.and_then(|v| v.cursor)
.or_else(|| rnd_base_visual.find(id).and_then(|v| v.cursor));
if let Some(cursor) = cursor_opt {
match cursor {
CursorIcon::Global(global_icon) => {
if global_cursor.is_none() {
global_cursor = Some(global_icon);
}
}
normal_cursor => {
return normal_cursor;
}
}
}
curr = *topo_parents.at(id);
}
let Some(global) = global_cursor else {
return CursorIcon::Default(None);
};
match global {
GlobalCursorIcon::Default(opt) => CursorIcon::Default(opt),
GlobalCursorIcon::Pointer(opt) => CursorIcon::Pointer(opt),
GlobalCursorIcon::Text(opt) => CursorIcon::Text(opt),
GlobalCursorIcon::Grab(opt) => CursorIcon::Grab(opt),
GlobalCursorIcon::Grabbing(opt) => CursorIcon::Grabbing(opt),
GlobalCursorIcon::NotAllowed(opt) => CursorIcon::NotAllowed(opt),
GlobalCursorIcon::ResizeNs(opt) => CursorIcon::ResizeNs(opt),
GlobalCursorIcon::ResizeEw(opt) => CursorIcon::ResizeEw(opt),
GlobalCursorIcon::ResizeNesw(opt) => CursorIcon::ResizeNesw(opt),
GlobalCursorIcon::ResizeNwse(opt) => CursorIcon::ResizeNwse(opt),
}
}
pub(crate) fn resolve_effective_transform(
id: EntityId,
topo_parents: &ParentsSecondary,
rnd_visual: &VisualPropertiesSecondary,
) -> [[f32; 4]; 4] {
let mut curr = Some(id);
let mut result = IDENTITY_MATRIX;
while let Some(curr_id) = curr {
let (self_transform, transform_inherit) = match rnd_visual.find(curr_id) {
Some(v) => (
v.transform.unwrap_or(IDENTITY_MATRIX),
v.transform_inherit.unwrap_or(false),
),
None => (IDENTITY_MATRIX, false),
};
result = OutputStore::mul_4x4(&self_transform, &result);
if transform_inherit {
curr = *topo_parents.at(curr_id);
} else {
break;
}
}
result
}
#[inline]
pub(crate) fn get_transform_and_origin(
visual: &VisualProperty,
full_transform: [[f32; 4]; 4],
) -> ([[f32; 4]; 3], [f32; 2]) {
let packed_transform = [
full_transform[0], full_transform[1], full_transform[3], ];
let origin = visual.transform_origin.map_or([0.5, 0.5], |p| [p.x, p.y]);
(packed_transform, origin)
}
#[inline]
pub(crate) fn get_outline_params(
visual: &VisualProperty,
) -> (EdgeInsets, Color, EdgeInsets, [f32; 4]) {
let o_width = visual.outline_width.unwrap_or_default();
let o_color = visual.outline_color.unwrap_or_default();
let o_lengths = visual.outline_lengths.unwrap_or(EdgeInsets::px_all(1.0));
let o_offset = visual.outline_offset.unwrap_or(0.0);
let o_styles = visual.outline_styles.unwrap_or([BorderStyle::default(); 4]);
let o_aligns = visual
.outline_alignments
.unwrap_or([BorderAlignment::default(); 4]);
let mut o_flags = 0u32;
for (idx, (&style, &align)) in o_styles.iter().zip(o_aligns.iter()).enumerate() {
let style_bits = (style as u32) & 0b11; let align_bits = (align as u32) & 0b11;
let edge_flags = style_bits | (align_bits << 2);
o_flags |= edge_flags << (idx * 4); }
let outline_offset_and_flags = [o_offset, o_flags as f32, 0.0, 0.0];
(o_width, o_color, o_lengths, outline_offset_and_flags)
}
pub(crate) fn tick_animations(
topo_active_masks: &mut ActiveMasksSecondary,
topo_is_sort_dirty: &mut bool,
topo_parents: &ParentsSecondary,
lay_taffy_tree: &mut TaffyTreeEntityId,
lay_dirty_entities: &mut DirtyLayoutEntitiesVec,
lay_basic: &mut BasicLayoutsSecondary,
lay_taffy_nodes: &TaffyNodesSecondary,
rnd_dirty_entities: &mut DirtyRenderEntitiesVec,
rnd_visual: &mut VisualPropertiesSecondary,
rnd_active_animations: &mut ActiveAnimationsSparse,
debug: &mut DebugStore,
) {
let now = Instant::now();
rnd_active_animations.retain(|id, animations| {
animations.retain_mut(|anim| {
let elapsed = now.duration_since(anim.start_time);
let elapsed_secs = elapsed.as_secs_f32();
let duration_secs = anim.duration.as_secs_f32();
let current_iteration = (elapsed_secs / duration_secs).floor() as u32;
let is_finished = match anim.iteration_count {
PlaybackCount::Count(max_count) => current_iteration >= max_count,
PlaybackCount::Infinite => false,
};
if is_finished {
RenderStore::apply_animation_value(
id,
anim.property,
&anim.end_value,
topo_active_masks,
topo_parents,
lay_dirty_entities,
lay_taffy_tree,
lay_basic,
lay_taffy_nodes,
rnd_visual,
debug,
);
return false;
}
let local_time = elapsed_secs % duration_secs;
let progress = if duration_secs > 0.0 {
(local_time / duration_secs).min(1.0)
} else {
1.0
};
let eased_t = anim.curve.evaluate(progress);
let current_val = anim.start_value.lerp(&anim.end_value, eased_t);
RenderStore::apply_animation_value(
id,
anim.property,
¤t_val,
topo_active_masks,
topo_parents,
lay_dirty_entities,
lay_taffy_tree,
lay_basic,
lay_taffy_nodes,
rnd_visual,
debug,
);
if anim.property == PropertyList::Transform {
*topo_is_sort_dirty = true;
}
RenderStore::mark_render_dirty(id, topo_active_masks, rnd_dirty_entities);
true });
!animations.is_empty()
});
}
pub(crate) fn tick_transitions(
topo_active_masks: &mut ActiveMasksSecondary,
topo_is_sort_dirty: &mut bool,
topo_parents: &ParentsSecondary,
lay_dirty_entities: &mut DirtyLayoutEntitiesVec,
lay_taffy_tree: &mut TaffyTreeEntityId,
lay_basic: &mut BasicLayoutsSecondary,
lay_taffy_nodes: &TaffyNodesSecondary,
rnd_dirty_entities: &mut DirtyRenderEntitiesVec,
rnd_visual: &mut VisualPropertiesSecondary,
rnd_active_transitions: &mut ActiveTransitionsSparse,
rnd_last_tick_time: &mut Option<Instant>,
debug: &mut DebugStore,
) {
const FRAME_TIME_120FPS: Duration = Duration::from_nanos(8_333_333);
let now = Instant::now();
if let Some(last) = rnd_last_tick_time
&& now.duration_since(*last) < FRAME_TIME_120FPS
{
return;
}
*rnd_last_tick_time = Some(now);
rnd_active_transitions.retain(|id, transitions| {
transitions.retain_mut(|t_state| {
let start_time = *t_state.start_time.get_or_insert(now);
let elapsed = now.duration_since(start_time);
let progress = (elapsed.as_secs_f32() / t_state.duration.as_secs_f32()).min(1.0);
let eased_t = t_state.curve.evaluate(progress);
let current_val = t_state.start_value.lerp(&t_state.end_value, eased_t);
match current_val {
TransitionValue::Color(c) => {
if let Some(v) = rnd_visual.find_mut(id) {
if t_state.property_list == PropertyList::BackgroundColor {
v.bg_color = Some(c);
} else if t_state.property_list == PropertyList::BorderColor {
v.border_color = Some(c);
}
}
RenderStore::mark_render_dirty(id, topo_active_masks, rnd_dirty_entities);
}
TransitionValue::Opacity(o) => {
if let Some(v) = rnd_visual.find_mut(id) {
v.opacity = Some(o);
}
RenderStore::mark_render_dirty(id, topo_active_masks, rnd_dirty_entities);
}
TransitionValue::Transform(m) => {
if let Some(v) = rnd_visual.find_mut(id) {
v.transform = Some(m);
}
*topo_is_sort_dirty = true;
RenderStore::mark_render_dirty(id, topo_active_masks, rnd_dirty_entities);
}
TransitionValue::CornerRadius(cr) => {
if let Some(v) = rnd_visual.find_mut(id) {
v.corner_radius = Some(cr);
}
RenderStore::mark_render_dirty(id, topo_active_masks, rnd_dirty_entities);
}
TransitionValue::Width(w) => {
let layout = lay_basic.at_mut(id);
layout.size.width = Val::Px(w); LayoutStore::mark_layout_dirty(
id,
topo_active_masks,
topo_parents,
lay_dirty_entities,
lay_taffy_tree,
lay_taffy_nodes,
debug,
);
}
TransitionValue::Height(h) => {
let layout = lay_basic.at_mut(id);
layout.size.height = Val::Px(h);
LayoutStore::mark_layout_dirty(
id,
topo_active_masks,
topo_parents,
lay_dirty_entities,
lay_taffy_tree,
lay_taffy_nodes,
debug,
);
}
TransitionValue::BoxShadow(shadow) => {
if let Some(v) = rnd_visual.find_mut(id) {
v.shadow_params = Some(shadow);
v.shadow_color = Some(shadow.color);
}
RenderStore::mark_render_dirty(id, topo_active_masks, rnd_dirty_entities);
}
}
progress < 1.0
});
!transitions.is_empty()
});
}
#[inline]
pub fn active_animations_mut(&mut self) -> &mut ActiveAnimationsSparse {
&mut self.rnd_active_animations
}
#[inline]
pub fn active_transitions_mut(&mut self) -> &mut ActiveTransitionsSparse {
&mut self.rnd_active_transitions
}
#[inline]
pub fn active_webviews_mut(&mut self) -> &mut FxHashSet<EntityId> {
&mut self.rnd_active_webviews
}
#[inline]
pub fn base_visual_mut(&mut self) -> &mut BaseVisualPropertiesSecondary {
&mut self.rnd_base_visual
}
#[inline]
pub fn dirty_entities_mut(&mut self) -> &mut DirtyRenderEntitiesVec {
&mut self.rnd_dirty_entities
}
#[inline]
pub fn interaction_mut(&mut self) -> &mut InteractionPropertiesSecondary {
&mut self.rnd_interaction
}
#[inline]
pub fn last_tick_time_mut(&mut self) -> &mut Option<Instant> {
&mut self.rnd_last_tick_time
}
#[inline]
pub fn visual_mut(&mut self) -> &mut VisualPropertiesSecondary {
&mut self.rnd_visual
}
}
impl Context {
#[inline]
pub(crate) fn get_visual_property_mut(
&mut self,
id: EntityId,
target: StyleTarget,
) -> &mut VisualProperty {
RenderStore::get_visual_property_mut(
id,
target,
&mut self.renders.rnd_base_visual,
&mut self.renders.rnd_interaction,
)
}
#[inline]
pub(crate) fn resolve_element_style_state(&mut self, id: EntityId, allow_transition: bool) {
RenderStore::resolve_element_style_state(
id,
allow_transition,
self.window.win_last_size,
&self.system.sys_text_buffers,
&self.reactive.react_element_effects,
&self.contents.cont_input_contents,
&mut self.topology.topo_active_masks,
&mut self.topology.topo_is_sort_dirty,
&self.topology.topo_entities,
&self.topology.topo_parents,
&self.topology.topo_children,
&mut self.layouts.lay_dirty_entities,
&mut self.layouts.lay_taffy_tree,
&mut self.layouts.lay_basic,
&self.layouts.lay_taffy_nodes,
&self.layouts.lay_base_basic,
&mut self.renders.rnd_dirty_entities,
&mut self.renders.rnd_visual,
&mut self.renders.rnd_active_transitions,
&mut self.renders.rnd_active_animations,
&self.renders.rnd_base_visual,
&self.renders.rnd_interaction,
&self.outputs.out_rects,
&mut self.debug,
);
}
}
#[cfg(test)]
mod tests;