use crate::{
ActiveFocusTrigger, ByteIndex, CapacityConfig, ComponentMask, Context, DEFAULT_BASIC,
DEFAULT_FLEX, DndStore, ElementState, EntityId, EventListeners, FocusStore, InputContents,
InputOp, LayoutPoint, MichiuError, MichiuString, Modifiers, MouseButton, OptionTraceExt,
Overflow, Pipeline, RenderStore, ResizeStore, ResolvedGeometry, ScrollStore, ScrollbarStore,
SelectedRectsSparse, SelectionStartIndexSparse, SystemStore, TextEditStore, TextEngine,
TextLayoutSize, TextSelectionsSparse, TopologyStore, UserAction, UserSelect, VirtualKey,
define_vec, handle_on_click, handle_on_cursor_moved, handle_on_hover, handle_on_keyboard_input,
handle_on_mouse_enter, handle_on_mouse_input, handle_on_mouse_leave, handle_on_mouse_wheel,
handle_on_right_click, soa::MichiuSoA,
};
use slotmap::SparseSecondaryMap;
use std::ops::Range;
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct ActiveInteractionStates {
pub hovered: Option<EntityId>,
pub focused: Option<EntityId>,
pub pressed: Option<EntityId>,
pub dragged: Option<EntityId>,
}
impl ActiveInteractionStates {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub(crate) fn clear_entity(&mut self, id: EntityId) {
if self.hovered == Some(id) {
self.hovered = None;
}
if self.focused == Some(id) {
self.focused = None;
}
if self.pressed == Some(id) {
self.pressed = None;
}
if self.dragged == Some(id) {
self.dragged = None;
}
}
}
define_vec!(pub struct PendingActions(UserAction));
#[derive(Debug, Default, derive_more::Deref, derive_more::DerefMut, derive_more::IntoIterator)]
#[into_iterator(owned, ref, ref_mut)]
pub struct EventListenersSparse(SparseSecondaryMap<EntityId, EventListeners>);
impl MichiuSoA for EventListenersSparse {
type Item = EventListeners;
#[inline]
fn find(&self, id: EntityId) -> Option<&Self::Item> {
self.0.get(id)
}
#[inline]
fn find_mut(&mut self, id: EntityId) -> Option<&mut Self::Item> {
self.0.get_mut(id)
}
}
pub struct EventStore {
pub(crate) evt_listeners: EventListenersSparse,
pub(crate) evt_interaction_states: ActiveInteractionStates,
pub(crate) evt_current_pointer_position: Option<LayoutPoint>,
pub(crate) evt_pending_actions: PendingActions,
}
impl Default for EventStore {
fn default() -> Self {
Self::new()
}
}
impl EventStore {
#[inline]
#[must_use]
pub(crate) fn new() -> Self {
Self {
evt_listeners: EventListenersSparse(SparseSecondaryMap::new()),
evt_interaction_states: ActiveInteractionStates::new(),
evt_current_pointer_position: None,
evt_pending_actions: PendingActions(Vec::new()),
}
}
#[inline]
#[must_use]
pub(crate) fn with_capacity(c: &CapacityConfig) -> Self {
Self {
evt_listeners: EventListenersSparse(SparseSecondaryMap::with_capacity(c.evt_listeners)),
evt_pending_actions: PendingActions(Vec::with_capacity(c.evt_pending_actions)),
..Default::default()
}
}
#[inline]
pub(crate) fn clear(&mut self) {
self.evt_listeners.clear();
self.evt_interaction_states = ActiveInteractionStates::new();
self.evt_current_pointer_position = None;
}
#[inline]
pub(crate) fn despawn(&mut self, id: EntityId) {
self.evt_listeners.remove(id);
self.evt_interaction_states.clear_entity(id);
}
#[inline]
pub fn current_pointer_position_mut(&mut self) -> &mut Option<LayoutPoint> {
&mut self.evt_current_pointer_position
}
#[inline]
pub fn interaction_states_mut(&mut self) -> &mut ActiveInteractionStates {
&mut self.evt_interaction_states
}
#[inline]
pub fn listeners_mut(&mut self) -> &mut EventListenersSparse {
&mut self.evt_listeners
}
#[inline]
pub fn pending_actions_mut(&mut self) -> &mut PendingActions {
&mut self.evt_pending_actions
}
}
impl EventStore {
fn resolve_hover_state(cx: &mut Context, target_id: Option<EntityId>) {
let old_id = cx.events.evt_interaction_states.hovered;
if old_id == target_id {
return;
}
cx.events.evt_interaction_states.hovered = target_id;
if let Some(old_id) = old_id {
Pipeline::update_state(cx, old_id, ComponentMask::STATE_HOVERED, false);
handle_on_mouse_leave(cx, old_id);
}
if let Some(new_id) = target_id {
Pipeline::update_state(cx, new_id, ComponentMask::STATE_HOVERED, true);
handle_on_mouse_enter(cx, new_id);
handle_on_hover(cx, new_id);
}
}
#[track_caller]
#[inline]
fn propagate_cursor_move_events(
cx: &mut Context,
target_id: Option<EntityId>,
logical_pos: LayoutPoint,
) {
let Some(id) = target_id else {
return;
};
let has_listener = cx
.events
.evt_listeners
.find(id)
.is_some_and(|l| l.on_cursor_moved.is_some());
if has_listener && let Some(&rect) = cx.outputs.out_rects.find(id) {
let relative_pos = LayoutPoint::new(logical_pos.x - rect.x, logical_pos.y - rect.y);
handle_on_cursor_moved(cx, id, relative_pos);
}
}
#[track_caller]
pub(crate) fn inject_pointer_move(cx: &mut Context, logical_pos: LayoutPoint) {
let prev_pos = cx.events.evt_current_pointer_position;
cx.events.evt_current_pointer_position = Some(logical_pos);
if let Some(ref state) = cx.states.resize.res_resizing_state {
ResizeStore::sync_resizing_drag(
logical_pos,
state,
cx.window.win_last_size,
&mut cx.topology.topo_active_masks,
&cx.topology.topo_parents,
&mut cx.layouts.lay_dirty_entities,
&mut cx.layouts.lay_taffy_tree,
&mut cx.layouts.lay_basic,
&mut cx.layouts.lay_base_basic,
&cx.layouts.lay_taffy_nodes,
&mut cx.renders.rnd_dirty_entities,
&cx.outputs.out_rects,
&mut cx.debug,
);
return; }
ScrollStore::sync_scrollbar_drag(
logical_pos,
cx.window.win_last_size,
&mut cx.topology.topo_active_masks,
&cx.topology.topo_parents,
&mut cx.layouts.lay_dirty_entities,
&mut cx.layouts.lay_taffy_tree,
&mut cx.layouts.scrollbar.bar_styles,
&cx.layouts.lay_taffy_nodes,
&cx.layouts.lay_resolved_basic,
&mut cx.renders.rnd_dirty_entities,
&mut cx.states.scroll.sc_offsets,
&cx.outputs.out_rects,
&cx.states.scroll.sc_sizes,
&mut cx.debug,
);
let hit_id = TopologyStore::hit_test(
logical_pos,
cx.window.win_last_size,
&cx.events.evt_interaction_states,
&mut cx.topology.topo_active_masks,
&mut cx.topology.topo_effective_z_indices,
&mut cx.topology.topo_sorted_entities,
&mut cx.topology.topo_sort_cache,
&mut cx.topology.topo_is_sort_dirty,
&cx.topology.topo_parents,
&cx.topology.topo_flat_dfs_sequence,
&cx.renders.rnd_visual,
&cx.renders.rnd_base_visual,
&mut cx.outputs.out_clip_rects,
&cx.outputs.out_rects,
&mut cx.debug,
);
let target_id = cx.events.evt_interaction_states.pressed.or(hit_id);
let prev_resize_hover = cx.states.resize.res_active_resize_hover;
cx.states.resize.res_active_resize_hover = None;
let (_, found_resize_hover) = ResizeStore::found_resize_hover(
target_id,
logical_pos,
&cx.topology.topo_active_masks,
&cx.topology.topo_parents,
&cx.layouts.lay_basic,
&cx.outputs.out_rects,
&mut cx.debug,
);
if let Some((id, dir)) = found_resize_hover {
cx.states.resize.res_active_resize_hover = Some((id, dir));
ResizeStore::apply_resizable_cursor_style(id, dir, &mut cx.renders.rnd_visual);
RenderStore::mark_render_dirty(
id,
&mut cx.topology.topo_active_masks,
&mut cx.renders.rnd_dirty_entities,
);
}
if let Some((prev_id, _)) = prev_resize_hover {
let now_id = cx.states.resize.res_active_resize_hover.map(|(id, _)| id);
if Some(prev_id) != now_id {
RenderStore::resolve_element_style_state(
prev_id,
false,
cx.window.win_last_size,
&cx.system.sys_text_buffers,
&cx.reactive.react_element_effects,
&cx.contents.cont_input_contents,
&mut cx.topology.topo_active_masks,
&mut cx.topology.topo_is_sort_dirty,
&cx.topology.topo_entities,
&cx.topology.topo_parents,
&cx.topology.topo_children,
&mut cx.layouts.lay_dirty_entities,
&mut cx.layouts.lay_taffy_tree,
&mut cx.layouts.lay_basic,
&cx.layouts.lay_taffy_nodes,
&cx.layouts.lay_base_basic,
&mut cx.renders.rnd_dirty_entities,
&mut cx.renders.rnd_visual,
&mut cx.renders.rnd_active_transitions,
&mut cx.renders.rnd_active_animations,
&cx.renders.rnd_base_visual,
&cx.renders.rnd_interaction,
&cx.outputs.out_rects,
&mut cx.debug,
);
RenderStore::mark_render_dirty(
prev_id,
&mut cx.topology.topo_active_masks,
&mut cx.renders.rnd_dirty_entities,
);
}
}
if let Some(pressed_id) = cx.events.evt_interaction_states.pressed {
let user_select = cx.renders.rnd_visual.user_select(pressed_id);
if user_select == UserSelect::Text
&& let Some(start_pos) = cx
.states
.edit
.edit_selection_start_index
.find(pressed_id)
.copied()
{
if let Some(contents) = cx.contents.cont_input_contents.find(pressed_id) {
let is_placeholder = contents.to_michiu().is_empty();
let is_ime = contents
.ime_state
.as_ref()
.is_none_or(|s| s.composition_text.is_empty());
if is_placeholder && is_ime && !contents.placeholder_select {
return;
}
}
let flex =
cx.layouts
.lay_resolved_flex
.find_or(pressed_id, &DEFAULT_FLEX, &mut cx.debug);
let resolved_geom = ResolvedGeometry::resolved(
pressed_id,
&cx.states.scroll.sc_offsets,
&cx.layouts.lay_resolved_basic,
&cx.outputs.out_rects,
&mut cx.debug,
);
let auto_wrap = cx.renders.rnd_visual.auto_wrap(pressed_id);
let max_width_opt = resolved_geom.calc_max_width(auto_wrap);
let buffer = SystemStore::get_or_create_text_buffer(
pressed_id,
max_width_opt,
&cx.system.sys_text_buffers,
|| {
let text = cx.contents.cont_text_contents.at(pressed_id);
let font = cx.renders.rnd_visual.font(pressed_id);
let spans = cx.contents.cont_text_spans.span(pressed_id);
cx.system.sys_text_engine.create_buffer(
text,
&font,
flex.text_align,
max_width_opt,
auto_wrap,
spans,
)
},
);
let text_size =
if let Some(contents) = &cx.contents.cont_input_contents.find(pressed_id) {
contents.last_layout.map_or(TextLayoutSize::DEFAULT, |r| {
TextLayoutSize::new(r.width, r.height, Some(contents.is_multiline))
})
} else {
TextEngine::get_layout_size(&buffer).set_multiline(Some(false))
};
let local = resolved_geom.pressed_local_point(
logical_pos,
flex.text_align,
flex.align_items,
text_size,
);
TextEditStore::handle_text_selection_click(
pressed_id,
start_pos,
local,
&buffer,
cx.window.win_scale_factor,
cx.window.win_last_size,
&mut cx.system.sys_text_engine,
&cx.system.sys_text_buffers,
&mut cx.contents.cont_text_contents,
&mut cx.contents.cont_input_contents,
&cx.contents.cont_text_spans,
&mut cx.topology.topo_active_masks,
&cx.topology.topo_parents,
&mut cx.layouts.lay_dirty_entities,
&mut cx.layouts.lay_taffy_tree,
&mut cx.layouts.scrollbar.bar_styles,
&cx.layouts.lay_taffy_nodes,
&cx.layouts.lay_resolved_basic,
&cx.layouts.lay_resolved_flex,
&cx.layouts.lay_resolved_grid,
&mut cx.renders.rnd_dirty_entities,
&mut cx.renders.rnd_visual,
&cx.renders.rnd_base_visual,
&mut cx.states.scroll.sc_offsets,
&mut cx.states.edit.edit_selections,
&mut cx.states.edit.edit_selected_rects,
&cx.outputs.out_rects,
&cx.states.scroll.sc_sizes,
&mut cx.debug,
);
}
}
if hit_id != cx.events.evt_interaction_states.hovered {
EventStore::resolve_hover_state(cx, hit_id);
}
EventStore::propagate_cursor_move_events(cx, hit_id, logical_pos);
DndStore::propagate_dnd_drag_events(cx, prev_pos, logical_pos);
let Some(ref drag_state) = cx.states.dnd.dnd_active_drag_state else {
return;
};
let root = TopologyStore::find_root_entity(
&cx.topology.topo_entities,
&cx.topology.topo_parents,
&cx.topology.topo_flat_dfs_sequence,
)
.unwrap_or_trace(None, &mut cx.debug, || MichiuError::RootEntityNotFound);
let src_id = drag_state.source_entity;
let placeholder_id = drag_state.placeholder_entity;
let drag_prop = *cx.states.dnd.dnd_drag_properties.at(src_id);
DndStore::update_inset_based_relative_local(
root,
placeholder_id,
logical_pos,
&drag_prop,
drag_state,
&mut cx.topology.topo_active_masks,
&cx.topology.topo_parents,
&mut cx.layouts.lay_dirty_entities,
&mut cx.layouts.lay_taffy_tree,
&mut cx.layouts.lay_basic,
&mut cx.layouts.lay_base_basic,
&cx.layouts.lay_taffy_nodes,
&mut cx.renders.rnd_dirty_entities,
&cx.outputs.out_rects,
&mut cx.debug,
);
let found_drop_target = DndStore::detect_drop_target_during_intrusion(
src_id,
hit_id,
placeholder_id,
&cx.topology.topo_active_masks,
&cx.topology.topo_parents,
);
DndStore::sync_state_drag_in(cx, found_drop_target);
DndStore::callback_drag_prop(cx, src_id, found_drop_target, &drag_prop);
}
#[track_caller]
fn handle_pointer_pressed(cx: &mut Context, button: MouseButton, modifiers: Modifiers) {
let current_hovered = cx.events.evt_interaction_states.hovered;
if button == MouseButton::Left
&& let Some((id, dir)) = cx.states.resize.res_active_resize_hover
{
ResizeStore::state_pressed_resize_drag(
id,
dir,
&mut cx.events.evt_interaction_states,
&mut cx.states.resize.res_resizing_state,
cx.events.evt_current_pointer_position,
&cx.topology.topo_parents,
&mut cx.layouts.lay_basic,
&mut cx.layouts.lay_base_basic,
&cx.outputs.out_rects,
&mut cx.debug,
);
RenderStore::mark_render_dirty(
id,
&mut cx.topology.topo_active_masks,
&mut cx.renders.rnd_dirty_entities,
);
return; }
if let Some(pointer_pos) = cx.events.evt_current_pointer_position
&& let Some(target_id) = current_hovered
{
let clicked_scrollbar = ScrollbarStore::hit_decision_element_scrollbar(
target_id,
pointer_pos,
cx.window.win_last_size,
&mut cx.events.evt_interaction_states,
&mut cx.topology.topo_active_masks,
&cx.topology.topo_parents,
&mut cx.layouts.lay_dirty_entities,
&mut cx.layouts.lay_taffy_tree,
&mut cx.layouts.scrollbar.bar_styles,
&cx.layouts.lay_taffy_nodes,
&cx.layouts.lay_resolved_basic,
&mut cx.renders.rnd_dirty_entities,
&mut cx.states.scroll.sc_offsets,
&cx.outputs.out_rects,
&cx.states.scroll.sc_sizes,
&mut cx.debug,
);
if clicked_scrollbar {
return; }
}
let Some(target_id) = current_hovered else {
return;
};
cx.events.evt_interaction_states.pressed = Some(target_id);
Pipeline::update_state(cx, target_id, ComponentMask::STATE_PRESSED, true);
let user_select = cx.renders.rnd_visual.user_select(target_id);
let is_input = cx
.topology
.topo_active_masks
.at(target_id)
.has_input_content();
if user_select == UserSelect::Text
&& !is_input
&& let Some(pointer_pos) = cx.events.evt_current_pointer_position
{
TextEditStore::handle_user_select_text(
target_id,
pointer_pos,
modifiers.shift,
&mut cx.system.sys_text_engine,
&cx.system.sys_text_buffers,
&cx.contents.cont_text_contents,
&cx.contents.cont_text_spans,
&cx.contents.cont_input_contents,
&mut cx.topology.topo_active_masks,
&cx.layouts.lay_resolved_basic,
&cx.layouts.lay_resolved_flex,
&mut cx.renders.rnd_dirty_entities,
&cx.renders.rnd_visual,
&mut cx.states.edit.edit_selections,
&mut cx.states.edit.edit_selection_start_index,
&mut cx.states.edit.edit_selected_rects,
&cx.outputs.out_rects,
&cx.states.scroll.sc_offsets,
&mut cx.debug,
);
}
if !FocusStore::should_prevent_focus_steal(cx, target_id) {
let is_focusable = FocusStore::restrict_focusable_element(
target_id,
&cx.topology.topo_active_masks,
&cx.renders.rnd_visual,
);
if is_focusable {
FocusStore::auto_focus_switch_by_trigger(cx, target_id, ActiveFocusTrigger::Mouse);
} else {
FocusStore::handle_remove_focus(cx);
}
}
handle_on_mouse_input(cx, target_id, button, modifiers, ElementState::Pressed);
}
#[inline]
fn handle_pointer_released(cx: &mut Context, button: MouseButton, modifiers: Modifiers) {
if let Some(state) = cx.states.resize.res_resizing_state.take() {
let id = state.entity_id;
cx.events.evt_interaction_states.pressed = None;
if let Some(pos) = cx.events.evt_current_pointer_position {
EventStore::inject_pointer_move(cx, pos);
}
RenderStore::mark_render_dirty(
id,
&mut cx.topology.topo_active_masks,
&mut cx.renders.rnd_dirty_entities,
);
return;
}
if let Some(drag_state) = cx.states.dnd.dnd_active_drag_state.take() {
DndStore::handle_dnd_drop(cx, &drag_state);
return;
}
let dirty_ids =
ScrollbarStore::get_scrollbar_dirty_ids(&mut cx.layouts.scrollbar.bar_styles);
for id in dirty_ids {
RenderStore::mark_render_dirty(
id,
&mut cx.topology.topo_active_masks,
&mut cx.renders.rnd_dirty_entities,
);
}
if let Some(pressed_id) = cx.events.evt_interaction_states.pressed {
Pipeline::update_state(cx, pressed_id, ComponentMask::STATE_PRESSED, false);
Pipeline::update_state(cx, pressed_id, ComponentMask::STATE_DRAGGED, false);
cx.events.evt_interaction_states.dragged = None;
if let Some(contents) = cx.contents.cont_input_contents.find_mut(pressed_id) {
contents.is_selecting = false;
}
handle_on_mouse_input(cx, pressed_id, button, modifiers, ElementState::Released);
if cx.events.evt_interaction_states.hovered == Some(pressed_id) {
match button {
MouseButton::Left => handle_on_click(cx, pressed_id),
MouseButton::Right => handle_on_right_click(cx, pressed_id),
_ => {}
}
}
cx.events.evt_interaction_states.pressed = None;
}
}
#[inline]
pub(crate) fn inject_pointer_button(
cx: &mut Context,
button: MouseButton,
state: ElementState,
modifiers: Modifiers,
) {
match state {
ElementState::Pressed => EventStore::handle_pointer_pressed(cx, button, modifiers),
ElementState::Released => EventStore::handle_pointer_released(cx, button, modifiers),
}
}
#[track_caller]
pub(crate) fn inject_pointer_double_click(cx: &mut Context) {
let current_hovered = cx.events.evt_interaction_states.hovered;
let Some(target_id) = current_hovered else {
return;
};
let user_select = cx.renders.rnd_visual.user_select(target_id);
if user_select != UserSelect::Text {
return;
}
let Some(pointer_pos) = cx.events.evt_current_pointer_position else {
return;
};
if let Some(contents) = cx.contents.cont_input_contents.find(target_id) {
let is_placeholder = contents.to_michiu().is_empty()
&& contents
.ime_state
.as_ref()
.is_none_or(|s| s.composition_text.is_empty());
if is_placeholder && !contents.placeholder_select {
return;
}
}
let resolved_geom = ResolvedGeometry::resolved(
target_id,
&cx.states.scroll.sc_offsets,
&cx.layouts.lay_resolved_basic,
&cx.outputs.out_rects,
&mut cx.debug,
);
let auto_wrap = cx.renders.rnd_visual.auto_wrap(target_id);
let max_width_opt = resolved_geom.calc_max_width(auto_wrap);
let text = cx.contents.cont_text_contents.at(target_id);
let buffer = SystemStore::get_or_create_text_buffer(
target_id,
max_width_opt,
&cx.system.sys_text_buffers,
|| {
let font = cx.renders.rnd_visual.font(target_id);
let spans = cx.contents.cont_text_spans.span(target_id);
let flex =
cx.layouts
.lay_resolved_flex
.find_or(target_id, &DEFAULT_FLEX, &mut cx.debug);
cx.system.sys_text_engine.create_buffer(
text,
&font,
flex.text_align,
max_width_opt,
auto_wrap,
spans,
)
},
);
let rect = resolved_geom.rect;
let border = resolved_geom.border;
let padding = resolved_geom.padding;
let local_x = pointer_pos.x - (rect.x + border.left + padding.left);
let local_y = pointer_pos.y - (rect.y + border.top + padding.top);
let (clicked_index, _) =
TextEngine::hit_test_point(&buffer, LayoutPoint::new(local_x, local_y));
let range = text.find_word_boundaries(clicked_index);
cx.states
.edit
.edit_selections
.insert(target_id, range.clone());
cx.states
.edit
.edit_selection_start_index
.insert(target_id, range.start);
TextEditStore::update_selection_rects(
target_id,
&buffer,
&mut cx.states.edit.edit_selected_rects,
&cx.states.edit.edit_selections,
&cx.contents.cont_input_contents,
);
if let Some(contents) = cx.contents.cont_input_contents.find_mut(target_id) {
contents.selected_range = range;
contents.selection_reversed = false; TextEditStore::apply_input_update(
target_id,
InputOp::MousePress,
cx.window.win_scale_factor,
cx.window.win_last_size,
&mut cx.system.sys_text_engine,
&cx.system.sys_text_buffers,
&mut cx.contents.cont_text_contents,
&mut cx.contents.cont_input_contents,
&cx.contents.cont_text_spans,
&mut cx.topology.topo_active_masks,
&cx.topology.topo_parents,
&mut cx.layouts.lay_dirty_entities,
&mut cx.layouts.lay_taffy_tree,
&mut cx.layouts.scrollbar.bar_styles,
&cx.layouts.lay_taffy_nodes,
&cx.layouts.lay_resolved_basic,
&cx.layouts.lay_resolved_flex,
&cx.layouts.lay_resolved_grid,
&mut cx.renders.rnd_dirty_entities,
&mut cx.renders.rnd_visual,
&cx.renders.rnd_base_visual,
&mut cx.states.scroll.sc_offsets,
&mut cx.states.edit.edit_selections,
&mut cx.states.edit.edit_selected_rects,
&cx.outputs.out_rects,
&cx.states.scroll.sc_sizes,
&mut cx.debug,
);
} else {
RenderStore::mark_render_dirty(
target_id,
&mut cx.topology.topo_active_masks,
&mut cx.renders.rnd_dirty_entities,
);
}
}
#[track_caller]
pub(crate) fn inject_mouse_wheel(cx: &mut Context, scroll_x: f32, scroll_y: f32) {
let mut curr = cx.events.evt_interaction_states.hovered;
while let Some(curr_id) = curr {
let has_listener = cx
.events
.evt_listeners
.find(curr_id)
.is_some_and(|l| l.on_mouse_wheel.is_some());
if has_listener {
handle_on_mouse_wheel(cx, curr_id, scroll_x, scroll_y);
break; }
let has_overflow = cx
.topology
.topo_active_masks
.at(curr_id)
.has(ComponentMask::STYLE_OVERFLOW);
if has_overflow {
let basic =
cx.layouts
.lay_resolved_basic
.find_or(curr_id, &DEFAULT_BASIC, &mut cx.debug);
let dy = if scroll_y != 0.0
&& (basic.overflow.y == Overflow::Scroll
|| basic.overflow.y == Overflow::Hidden)
{
scroll_y
} else {
0.0
};
let dx = if scroll_x != 0.0
&& (basic.overflow.x == Overflow::Scroll
|| basic.overflow.x == Overflow::Hidden)
{
scroll_x
} else {
0.0
};
if (dx != 0.0 || dy != 0.0)
&& ScrollStore::scroll_by(
curr_id,
dx,
dy,
cx.window.win_last_size,
&mut cx.topology.topo_active_masks,
&cx.topology.topo_parents,
&mut cx.layouts.lay_dirty_entities,
&mut cx.layouts.lay_taffy_tree,
&mut cx.layouts.scrollbar.bar_styles,
&cx.layouts.lay_taffy_nodes,
&cx.layouts.lay_resolved_basic,
&mut cx.states.scroll.sc_offsets,
&cx.outputs.out_rects,
&cx.states.scroll.sc_sizes,
&mut cx.debug,
)
{
break; }
}
curr = *cx.topology.topo_parents.at(curr_id);
}
}
#[track_caller]
pub(crate) fn inject_keyboard_key(
cx: &mut Context,
key: VirtualKey,
state: ElementState,
modifiers: Modifiers,
) {
if state == ElementState::Pressed && key == VirtualKey::TAB {
EventStore::cycle_keyboard_focus_internal(cx, modifiers.shift);
return;
}
let Some(focused_id) = cx.events.evt_interaction_states.focused else {
return;
};
if state == ElementState::Pressed && (key == VirtualKey::RETURN || key == VirtualKey::SPACE)
{
let has_input_contents = cx
.topology
.topo_active_masks
.at(focused_id)
.has_input_content();
if !has_input_contents {
handle_on_click(cx, focused_id);
return;
}
}
if state == ElementState::Pressed && modifiers.ctrl && key == VirtualKey::A {
let user_select = cx.renders.rnd_visual.user_select(focused_id);
if user_select == UserSelect::Text {
TextEditStore::handle_select_all(
focused_id,
cx.window.win_scale_factor,
cx.window.win_last_size,
&mut cx.system.sys_text_engine,
&cx.system.sys_text_buffers,
&mut cx.contents.cont_text_contents,
&mut cx.contents.cont_input_contents,
&cx.contents.cont_text_spans,
&mut cx.topology.topo_active_masks,
&cx.topology.topo_parents,
&mut cx.layouts.lay_dirty_entities,
&mut cx.layouts.lay_taffy_tree,
&mut cx.layouts.scrollbar.bar_styles,
&cx.layouts.lay_taffy_nodes,
&cx.layouts.lay_resolved_basic,
&cx.layouts.lay_resolved_flex,
&cx.layouts.lay_resolved_grid,
&mut cx.renders.rnd_dirty_entities,
&mut cx.renders.rnd_visual,
&cx.renders.rnd_base_visual,
&mut cx.states.scroll.sc_offsets,
&mut cx.states.edit.edit_selections,
&mut cx.states.edit.edit_selected_rects,
&cx.outputs.out_rects,
&cx.states.scroll.sc_sizes,
&mut cx.debug,
);
return;
}
}
handle_on_keyboard_input(cx, focused_id, key, modifiers, state);
}
pub(crate) fn cycle_keyboard_focus_internal(cx: &mut Context, reverse: bool) {
if cx.topology.topo_flat_dfs_sequence.is_empty() {
return;
}
let len = cx.topology.topo_flat_dfs_sequence.len();
let current_focused = cx.events.evt_interaction_states.focused;
let start_idx = current_focused
.and_then(|id| {
cx.topology
.topo_flat_dfs_sequence
.iter()
.position(|&x| x == id)
})
.unwrap_or(if reverse { len - 1 } else { 0 });
let mut target_id = None;
for i in 1..len {
let idx = if reverse {
(start_idx + len - i) % len
} else {
(start_idx + i) % len
};
let Some(candidate_id) = cx.topology.topo_flat_dfs_sequence.get(idx).copied() else {
continue;
};
if FocusStore::is_keyboard_focusable(
candidate_id,
&cx.topology.topo_entities,
&cx.topology.topo_active_masks,
&cx.topology.topo_parents,
&cx.layouts.lay_basic,
&cx.renders.rnd_visual,
) {
target_id = Some(candidate_id);
break;
}
}
let Some(candidate_id) = target_id else {
return;
};
if let Some(old_id) = cx.events.evt_interaction_states.focused {
FocusStore::set_focused_by_trigger(cx, old_id, false, ActiveFocusTrigger::Keyboard);
}
FocusStore::set_focused_by_trigger(cx, candidate_id, true, ActiveFocusTrigger::Keyboard);
cx.events.evt_interaction_states.focused = Some(candidate_id);
RenderStore::mark_render_dirty(
candidate_id,
&mut cx.topology.topo_active_masks,
&mut cx.renders.rnd_dirty_entities,
);
}
fn handle_paste(
focused_id: EntityId,
text: &MichiuString,
contents: &mut InputContents,
edit_selections: &mut TextSelectionsSparse,
edit_selected_rects: &mut SelectedRectsSparse,
) {
let text_val = contents.to_michiu();
let range = contents.selected_range.clone();
let allowed_len = if let Some(max) = contents.max_length {
let selected_char_count = text_val.slice(range.clone()).chars().count();
let current_len_after_delete = text_val.char_count().0 - selected_char_count;
if current_len_after_delete >= max.0 {
return; }
max.0 - current_len_after_delete
} else {
usize::MAX
};
let mut pasted = String::new();
let mut chars_added = 0;
for c in text.chars() {
if contents.numeric_only && !c.is_numeric() && c != '.' && c != '-' {
continue;
}
if chars_added >= allowed_len {
break;
}
pasted.push(c);
chars_added += 1;
}
if pasted.is_empty() && range.start == range.end {
return;
}
contents.record_undo(text_val, range.clone());
let new_caret = contents.update_michiu(|m| m.replace_range(range, &pasted));
let new_range = new_caret..new_caret;
contents.selected_range = new_range.clone();
edit_selections.insert(focused_id, new_range);
edit_selected_rects.remove(focused_id);
}
#[track_caller]
pub(crate) fn inject_paste(cx: &mut Context, text: &MichiuString) {
let Some(focused_id) = cx.events.evt_interaction_states.focused else {
return;
};
if !cx
.topology
.topo_active_masks
.at(focused_id)
.has_input_content()
{
return;
}
let contents = cx.contents.cont_input_contents.at_mut(focused_id);
EventStore::handle_paste(
focused_id,
text,
contents,
&mut cx.states.edit.edit_selections,
&mut cx.states.edit.edit_selected_rects,
);
TextEditStore::update_input_caret_position(
focused_id,
cx.window.win_scale_factor,
cx.window.win_last_size,
&mut cx.system.sys_text_engine,
&cx.system.sys_text_buffers,
&mut cx.contents.cont_text_contents,
&mut cx.contents.cont_input_contents,
&cx.contents.cont_text_spans,
&mut cx.topology.topo_active_masks,
&cx.topology.topo_parents,
&mut cx.layouts.lay_dirty_entities,
&mut cx.layouts.lay_taffy_tree,
&mut cx.layouts.scrollbar.bar_styles,
&cx.layouts.lay_taffy_nodes,
&cx.layouts.lay_resolved_basic,
&cx.layouts.lay_resolved_flex,
&cx.layouts.lay_resolved_grid,
&mut cx.renders.rnd_visual,
&cx.renders.rnd_base_visual,
&mut cx.states.scroll.sc_offsets,
&mut cx.states.edit.edit_selections,
&cx.outputs.out_rects,
&cx.states.scroll.sc_sizes,
&mut cx.debug,
);
TextEditStore::apply_input_update(
focused_id,
InputOp::Paste,
cx.window.win_scale_factor,
cx.window.win_last_size,
&mut cx.system.sys_text_engine,
&cx.system.sys_text_buffers,
&mut cx.contents.cont_text_contents,
&mut cx.contents.cont_input_contents,
&cx.contents.cont_text_spans,
&mut cx.topology.topo_active_masks,
&cx.topology.topo_parents,
&mut cx.layouts.lay_dirty_entities,
&mut cx.layouts.lay_taffy_tree,
&mut cx.layouts.scrollbar.bar_styles,
&cx.layouts.lay_taffy_nodes,
&cx.layouts.lay_resolved_basic,
&cx.layouts.lay_resolved_flex,
&cx.layouts.lay_resolved_grid,
&mut cx.renders.rnd_dirty_entities,
&mut cx.renders.rnd_visual,
&cx.renders.rnd_base_visual,
&mut cx.states.scroll.sc_offsets,
&mut cx.states.edit.edit_selections,
&mut cx.states.edit.edit_selected_rects,
&cx.outputs.out_rects,
&cx.states.scroll.sc_sizes,
&mut cx.debug,
);
}
fn handle_undo(
focused_id: EntityId,
prev_sel: Range<ByteIndex>,
prev_text: MichiuString,
contents: &mut InputContents,
edit_selections: &mut TextSelectionsSparse,
edit_selected_rects: &mut SelectedRectsSparse,
) {
contents.apply_undo(prev_text, prev_sel.clone());
edit_selections.insert(focused_id, prev_sel);
edit_selected_rects.remove(focused_id);
}
#[track_caller]
pub(crate) fn inject_undo(cx: &mut Context) {
let Some(focused_id) = cx.events.evt_interaction_states.focused else {
return;
};
if !cx
.topology
.topo_active_masks
.at(focused_id)
.has_input_content()
{
return;
}
let contents = cx.contents.cont_input_contents.at_mut(focused_id);
let Some((prev_text, prev_sel)) = contents.undo_stack.pop() else {
return;
};
EventStore::handle_undo(
focused_id,
prev_sel.clone(),
prev_text,
contents,
&mut cx.states.edit.edit_selections,
&mut cx.states.edit.edit_selected_rects,
);
cx.states
.edit
.edit_selection_start_index
.insert(focused_id, prev_sel.start);
TextEditStore::apply_input_update(
focused_id,
InputOp::Undo,
cx.window.win_scale_factor,
cx.window.win_last_size,
&mut cx.system.sys_text_engine,
&cx.system.sys_text_buffers,
&mut cx.contents.cont_text_contents,
&mut cx.contents.cont_input_contents,
&cx.contents.cont_text_spans,
&mut cx.topology.topo_active_masks,
&cx.topology.topo_parents,
&mut cx.layouts.lay_dirty_entities,
&mut cx.layouts.lay_taffy_tree,
&mut cx.layouts.scrollbar.bar_styles,
&cx.layouts.lay_taffy_nodes,
&cx.layouts.lay_resolved_basic,
&cx.layouts.lay_resolved_flex,
&cx.layouts.lay_resolved_grid,
&mut cx.renders.rnd_dirty_entities,
&mut cx.renders.rnd_visual,
&cx.renders.rnd_base_visual,
&mut cx.states.scroll.sc_offsets,
&mut cx.states.edit.edit_selections,
&mut cx.states.edit.edit_selected_rects,
&cx.outputs.out_rects,
&cx.states.scroll.sc_sizes,
&mut cx.debug,
);
}
fn handle_redo(
focused_id: EntityId,
next_sel: Range<ByteIndex>,
next_text: MichiuString,
contents: &mut InputContents,
edit_selections: &mut TextSelectionsSparse,
edit_selected_rects: &mut SelectedRectsSparse,
edit_selection_start_index: &mut SelectionStartIndexSparse,
) {
contents.apply_redo(next_text, next_sel.clone());
edit_selections.insert(focused_id, next_sel.clone());
edit_selected_rects.remove(focused_id);
edit_selection_start_index.insert(focused_id, next_sel.start);
}
#[track_caller]
pub(crate) fn inject_redo(cx: &mut Context) {
let Some(focused_id) = cx.events.evt_interaction_states.focused else {
return;
};
if !cx
.topology
.topo_active_masks
.at(focused_id)
.has_input_content()
{
return;
}
let contents = cx.contents.cont_input_contents.at_mut(focused_id);
let Some((next_text, next_sel)) = contents.redo_stack.pop() else {
return;
};
EventStore::handle_redo(
focused_id,
next_sel.clone(),
next_text,
contents,
&mut cx.states.edit.edit_selections,
&mut cx.states.edit.edit_selected_rects,
&mut cx.states.edit.edit_selection_start_index,
);
TextEditStore::apply_input_update(
focused_id,
InputOp::Redo,
cx.window.win_scale_factor,
cx.window.win_last_size,
&mut cx.system.sys_text_engine,
&cx.system.sys_text_buffers,
&mut cx.contents.cont_text_contents,
&mut cx.contents.cont_input_contents,
&cx.contents.cont_text_spans,
&mut cx.topology.topo_active_masks,
&cx.topology.topo_parents,
&mut cx.layouts.lay_dirty_entities,
&mut cx.layouts.lay_taffy_tree,
&mut cx.layouts.scrollbar.bar_styles,
&cx.layouts.lay_taffy_nodes,
&cx.layouts.lay_resolved_basic,
&cx.layouts.lay_resolved_flex,
&cx.layouts.lay_resolved_grid,
&mut cx.renders.rnd_dirty_entities,
&mut cx.renders.rnd_visual,
&cx.renders.rnd_base_visual,
&mut cx.states.scroll.sc_offsets,
&mut cx.states.edit.edit_selections,
&mut cx.states.edit.edit_selected_rects,
&cx.outputs.out_rects,
&cx.states.scroll.sc_sizes,
&mut cx.debug,
);
}
fn inject_cut_internal(
focused_id: EntityId,
range: Range<ByteIndex>,
contents: &mut InputContents,
edit_selections: &mut TextSelectionsSparse,
edit_selected_rects: &mut SelectedRectsSparse,
) {
let current_text = contents.to_michiu();
let current_range = contents.selected_range.clone();
contents.record_undo(current_text, current_range);
let new_caret = contents.update_michiu(|m| m.remove_range(range));
let new_range = new_caret..new_caret;
contents.selected_range = new_range.clone();
edit_selections.insert(focused_id, new_range);
edit_selected_rects.remove(focused_id);
}
#[track_caller]
pub(crate) fn inject_cut(cx: &mut Context) -> Option<MichiuString> {
let focused_id = cx.events.evt_interaction_states.focused?;
let user_select = cx.renders.rnd_visual.user_select(focused_id);
if user_select != UserSelect::Text {
return None;
}
let range = cx.states.edit.edit_selections.at(focused_id);
if range.start >= range.end {
return None;
}
let text = cx.contents.cont_text_contents.at(focused_id);
let cut_text = text.slice(range.clone()).to_string();
let is_input = cx
.topology
.topo_active_masks
.at(focused_id)
.has_input_content();
if is_input {
let contents = cx.contents.cont_input_contents.at_mut(focused_id);
EventStore::inject_cut_internal(
focused_id,
range.clone(),
contents,
&mut cx.states.edit.edit_selections,
&mut cx.states.edit.edit_selected_rects,
);
TextEditStore::apply_input_update(
focused_id,
InputOp::Cut,
cx.window.win_scale_factor,
cx.window.win_last_size,
&mut cx.system.sys_text_engine,
&cx.system.sys_text_buffers,
&mut cx.contents.cont_text_contents,
&mut cx.contents.cont_input_contents,
&cx.contents.cont_text_spans,
&mut cx.topology.topo_active_masks,
&cx.topology.topo_parents,
&mut cx.layouts.lay_dirty_entities,
&mut cx.layouts.lay_taffy_tree,
&mut cx.layouts.scrollbar.bar_styles,
&cx.layouts.lay_taffy_nodes,
&cx.layouts.lay_resolved_basic,
&cx.layouts.lay_resolved_flex,
&cx.layouts.lay_resolved_grid,
&mut cx.renders.rnd_dirty_entities,
&mut cx.renders.rnd_visual,
&cx.renders.rnd_base_visual,
&mut cx.states.scroll.sc_offsets,
&mut cx.states.edit.edit_selections,
&mut cx.states.edit.edit_selected_rects,
&cx.outputs.out_rects,
&cx.states.scroll.sc_sizes,
&mut cx.debug,
);
}
Some(cut_text.into())
}
}
impl EventStore {}
#[cfg(test)]
mod tests;