use crate::{
ActiveInteractionStates, ActiveMasksSecondary, BaseVisualPropertiesSecondary, ByteIndex,
CapacityConfig, CharIndex, Color, ComponentMask, Context, DEFAULT_FLEX, DebugStore,
DirtyLayoutEntitiesVec, DirtyRenderEntitiesVec, EntityId, InputContents, InputContentsSparse,
LayoutPoint, LayoutRect, LayoutSize, MichiuSoA, MichiuString, ParentsSecondary, RectsSecondary,
RenderStore, ResolvedBasicSecondary, ResolvedFlexSecondary, ResolvedGeometry,
ResolvedGridSparse, ScrollOffsetsSecondary, ScrollSizesSecondary, ScrollStore,
ScrollbarStylesSparse, SystemStore, TaffyNodesSecondary, TaffyTreeEntityId, TextBufferSparse,
TextContentsSparse, TextEngine, TextLayoutSize, TextSpansSparse, TopologyStore, UserSelect,
VisualPropertiesSecondary, define_sparse_secondary,
};
use cosmic_text::Buffer;
use slotmap::SparseSecondaryMap;
use std::{ops::Range, rc::Rc};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InputOp {
Init,
TextEffect,
MousePress,
ArrowMove,
SelectAll,
CharTyped,
Backspace,
Delete,
ImeUpdated,
Paste,
Cut,
Undo,
Redo,
}
define_sparse_secondary!(pub struct SelectedRectsSparse(Vec<LayoutRect>));
define_sparse_secondary!(pub struct TextSelectionsSparse(Range<ByteIndex>));
define_sparse_secondary!(pub struct SelectionStartIndexSparse(ByteIndex));
pub(crate) struct TextEditStore {
pub(crate) edit_selections: TextSelectionsSparse,
pub(crate) edit_selection_start_index: SelectionStartIndexSparse,
pub(crate) edit_selected_rects: SelectedRectsSparse,
}
impl Default for TextEditStore {
fn default() -> Self {
Self::new()
}
}
impl TextEditStore {
#[inline]
#[must_use]
pub(crate) fn new() -> Self {
Self {
edit_selections: TextSelectionsSparse(SparseSecondaryMap::new()),
edit_selection_start_index: SelectionStartIndexSparse(SparseSecondaryMap::new()),
edit_selected_rects: SelectedRectsSparse(SparseSecondaryMap::new()),
}
}
#[inline]
#[must_use]
pub(crate) fn with_capacity(c: &CapacityConfig) -> Self {
Self {
edit_selections: TextSelectionsSparse(SparseSecondaryMap::with_capacity(
c.edit_selections,
)),
edit_selected_rects: SelectedRectsSparse(SparseSecondaryMap::with_capacity(
c.edit_selected_rects,
)),
edit_selection_start_index: SelectionStartIndexSparse(
SparseSecondaryMap::with_capacity(c.edit_selection_start_index),
),
}
}
#[inline]
pub(crate) fn clear(&mut self) {
self.edit_selections.clear();
self.edit_selection_start_index.clear();
self.edit_selected_rects.clear();
}
#[inline]
pub(crate) fn despawn(&mut self, id: EntityId) {
self.edit_selections.remove(id);
self.edit_selection_start_index.remove(id);
self.edit_selected_rects.remove(id);
}
}
impl TextEditStore {
#[inline]
pub(crate) fn truncate_unconfirmed_text(
text_val: &MichiuString,
contents: &InputContents,
max: CharIndex,
filtered_comp_text: MichiuString,
) -> MichiuString {
let range = &contents.selected_range;
let selected_chars = text_val.slice(range.clone()).chars().count();
let current_len_after_delete = text_val.char_count() - selected_chars;
if current_len_after_delete >= max {
MichiuString::default()
} else {
let allowed_chars = max - current_len_after_delete;
if filtered_comp_text.char_count().0 > allowed_chars {
let truncated: String = filtered_comp_text.chars().take(allowed_chars).collect();
MichiuString::from(truncated)
} else {
filtered_comp_text
}
}
}
#[inline]
pub(crate) fn clear_selection_highlight_rect(
id: EntityId,
topo_active_masks: &mut ActiveMasksSecondary,
cont_input_contents: &mut InputContentsSparse,
cont_text_spans: &mut TextSpansSparse,
edit_selections: &mut TextSelectionsSparse,
edit_selected_rects: &mut SelectedRectsSparse,
) {
edit_selections.remove(id);
edit_selected_rects.remove(id);
if let Some(contents) = cont_input_contents.find_mut(id) {
contents.selected_range = ByteIndex(0)..ByteIndex(0);
contents.ime_state = None;
contents.marked_range = None;
}
cont_text_spans.remove(id);
let mask = topo_active_masks.at_mut(id);
mask.unset(ComponentMask::STYLE_TEXT_SPANS);
}
pub(crate) fn calculate_caret_rect(
geom: &ResolvedGeometry,
contents: &InputContents,
scale: f32,
align_offset: LayoutPoint,
) -> LayoutRect {
let logical_x = geom.rect.x
+ geom.border.left
+ geom.padding.left
+ align_offset.x
+ contents.measured_caret.x
- geom.scroll.x;
let aligned_x = (logical_x * scale).round() / scale;
let line_height = contents.caret_line_height;
let caret_width = contents.caret_width.unwrap_or(contents.default_caret_width);
let caret_height = contents.caret_height.unwrap_or(line_height);
let vertical_center_offset = if contents.caret_height.is_some() {
(line_height - caret_height) * 0.5
} else {
0.0
};
let logical_y = geom.rect.y
+ geom.border.top
+ geom.padding.top
+ align_offset.y
+ contents.measured_caret.y
+ contents.caret_offset
- geom.scroll.y;
let aligned_y = ((logical_y + vertical_center_offset) * scale).round() / scale;
let aligned_width = (caret_width * scale).round().max(1.0) / scale;
let aligned_height = (caret_height * scale).round().max(1.0) / scale;
LayoutRect::new(aligned_x, aligned_y, aligned_width, aligned_height)
}
pub(crate) fn calc_selection_rects(
buffer: &Buffer,
range: Range<ByteIndex>,
) -> Vec<LayoutRect> {
if range.start == range.end {
return Vec::new();
}
let (start_idx, end_idx) = if range.start <= range.end {
(range.start, range.end)
} else {
(range.end, range.start)
};
let mut cursor_start = TextEngine::flat_idx_to_cursor(buffer, start_idx);
let mut cursor_end = TextEngine::flat_idx_to_cursor(buffer, end_idx);
if (cursor_start.line > cursor_end.line)
|| (cursor_start.line == cursor_end.line && cursor_start.index > cursor_end.index)
{
std::mem::swap(&mut cursor_start, &mut cursor_end);
}
let mut out_rects = Vec::new();
for run in buffer.layout_runs() {
let line_i = run.line_i;
if line_i < cursor_start.line || line_i > cursor_end.line {
continue;
}
let line_len = buffer.lines.get(line_i).map_or(0, |l| l.text().len());
let run_cursor_start = if line_i == cursor_start.line {
cursor_start
} else {
cosmic_text::Cursor {
line: line_i,
index: 0,
affinity: cosmic_text::Affinity::Before,
}
};
let run_cursor_end = if line_i == cursor_end.line {
cursor_end
} else {
cosmic_text::Cursor {
line: line_i,
index: line_len,
affinity: cosmic_text::Affinity::Before,
}
};
for (x, w) in run.highlight(run_cursor_start, run_cursor_end) {
out_rects.push(LayoutRect::new(x, run.line_top, w, run.line_height));
}
}
out_rects
}
#[inline]
pub(crate) fn update_selection_rects(
id: EntityId,
buffer: &Rc<Buffer>,
edit_selected_rects: &mut SelectedRectsSparse,
edit_selections: &TextSelectionsSparse,
cont_input_contents: &InputContentsSparse,
) {
if let Some(range) = edit_selections.get(id).cloned()
&& range.start < range.end
{
let display_range = if let Some(contents) = cont_input_contents.get(id) {
let raw_text = contents.to_michiu();
contents.raw_range_to_display_range(range, &raw_text)
} else {
range
};
let out_rects = TextEditStore::calc_selection_rects(buffer, display_range);
edit_selected_rects.insert(id, out_rects);
return;
}
edit_selected_rects.remove(id);
}
#[track_caller]
pub(crate) fn get_selected_text(
evt_interaction_states: &ActiveInteractionStates,
cont_text_contents: &TextContentsSparse,
rnd_visual: &VisualPropertiesSecondary,
edit_selections: &TextSelectionsSparse,
) -> Option<MichiuString> {
let target_id = evt_interaction_states.focused.or_else(|| {
edit_selections
.iter()
.find(|(_, range)| range.start < range.end)
.map(|(id, _)| id)
})?;
let user_select = rnd_visual.user_select(target_id);
if user_select == UserSelect::Text {
let range = edit_selections.at(target_id);
if range.start < range.end {
let text = cont_text_contents.at(target_id);
let byte_range = range.clone();
return Some(text.slice(byte_range).to_string().into());
}
}
None
}
#[track_caller]
pub(crate) fn handle_user_select_text(
id: EntityId,
pointer_pos: LayoutPoint,
pressed_shift: bool,
sys_text_engine: &mut TextEngine,
sys_text_buffers: &TextBufferSparse,
cont_text_contents: &TextContentsSparse,
cont_text_spans: &TextSpansSparse,
cont_input_contents: &InputContentsSparse,
topo_active_masks: &mut ActiveMasksSecondary,
lay_resolved_basic: &ResolvedBasicSecondary,
lay_resolved_flex: &ResolvedFlexSecondary,
rnd_dirty_entities: &mut DirtyRenderEntitiesVec,
rnd_visual: &VisualPropertiesSecondary,
edit_selections: &mut TextSelectionsSparse,
edit_selection_start_index: &mut SelectionStartIndexSparse,
edit_selected_rects: &mut SelectedRectsSparse,
out_rects: &RectsSecondary,
sc_offsets: &ScrollOffsetsSecondary,
debug: &mut DebugStore,
) {
let flex = lay_resolved_flex.find_or(id, &DEFAULT_FLEX, debug);
let auto_wrap = rnd_visual.auto_wrap(id);
let resolved_geom =
ResolvedGeometry::resolved(id, sc_offsets, lay_resolved_basic, out_rects, debug);
let content_width = resolved_geom.content_width();
let max_width_opt = (auto_wrap && content_width > 0.0).then_some(content_width);
let buffer =
SystemStore::get_or_create_text_buffer(id, max_width_opt, sys_text_buffers, || {
let text = cont_text_contents.at(id);
let font = rnd_visual.font(id);
let spans = cont_text_spans.span(id);
sys_text_engine.create_buffer(
text,
&font,
flex.text_align,
max_width_opt,
auto_wrap,
spans,
)
});
let text_size = if let Some(c) = cont_input_contents.find(id) {
let size = c
.last_layout
.map_or(LayoutSize::ZERO, |r| LayoutSize::new(r.width, r.height));
TextLayoutSize::new(size.width, size.height, Some(c.is_multiline))
} else {
TextEngine::get_layout_size(&buffer).set_multiline(Some(false))
};
let local = resolved_geom.pressed_local_point(
pointer_pos,
flex.text_align,
flex.align_items,
text_size,
);
let (clicked_index, is_trailing) = TextEngine::hit_test_point(&buffer, local);
let final_index = if is_trailing {
clicked_index + 1
} else {
clicked_index
};
if pressed_shift {
let anchor = edit_selection_start_index
.get(id)
.copied()
.unwrap_or(final_index);
if !edit_selection_start_index.contains_key(id) {
edit_selection_start_index.insert(id, final_index);
}
let range = if anchor <= final_index {
anchor..final_index
} else {
final_index..anchor
};
edit_selections.insert(id, range);
TextEditStore::update_selection_rects(
id,
&buffer,
edit_selected_rects,
edit_selections,
cont_input_contents,
);
} else {
edit_selection_start_index.insert(id, final_index);
edit_selections.insert(id, final_index..final_index);
edit_selected_rects.remove(id);
}
RenderStore::mark_render_dirty(id, topo_active_masks, rnd_dirty_entities);
}
#[track_caller]
pub(crate) fn handle_text_selection_click(
id: EntityId,
start_pos: ByteIndex,
local: LayoutPoint,
buffer: &Rc<Buffer>,
win_scale_factor: f32,
win_last_size: Option<LayoutSize>,
sys_text_engine: &mut TextEngine,
sys_text_buffers: &TextBufferSparse,
cont_text_contents: &mut TextContentsSparse,
cont_input_contents: &mut InputContentsSparse,
cont_text_spans: &TextSpansSparse,
topo_active_masks: &mut ActiveMasksSecondary,
topo_parents: &ParentsSecondary,
lay_dirty_entities: &mut DirtyLayoutEntitiesVec,
lay_taffy_tree: &mut TaffyTreeEntityId,
bar_styles: &mut ScrollbarStylesSparse,
lay_taffy_nodes: &TaffyNodesSecondary,
lay_resolved_basic: &ResolvedBasicSecondary,
lay_resolved_flex: &ResolvedFlexSecondary,
lay_resolved_grid: &ResolvedGridSparse,
rnd_dirty_entities: &mut DirtyRenderEntitiesVec,
rnd_visual: &mut VisualPropertiesSecondary,
rnd_base_visual: &BaseVisualPropertiesSecondary,
sc_offsets: &mut ScrollOffsetsSecondary,
edit_selections: &mut TextSelectionsSparse,
edit_selected_rects: &mut SelectedRectsSparse,
out_rects: &RectsSecondary,
sc_sizes: &ScrollSizesSecondary,
debug: &mut DebugStore,
) {
let (display_index, is_trailing) = TextEngine::hit_test_point(buffer, local);
let display_text = cont_text_contents.at(id);
let final_display_index = if is_trailing {
display_text.next_char_boundary(display_index)
} else {
display_index
};
let final_raw_index = if let Some(contents) = cont_input_contents.get(id) {
let raw_text = contents.to_michiu();
contents.display_byte_to_raw_byte(final_display_index, &raw_text)
} else {
final_display_index
};
let (range, is_reversed) = if start_pos <= final_raw_index {
(start_pos..final_raw_index, false)
} else {
(final_raw_index..start_pos, true)
};
edit_selections.insert(id, range.clone());
TextEditStore::update_selection_rects(
id,
buffer,
edit_selected_rects,
edit_selections,
cont_input_contents,
);
if let Some(contents) = cont_input_contents.find_mut(id) {
contents.selection_reversed = is_reversed;
contents.selected_range = range;
TextEditStore::apply_input_update(
id,
InputOp::MousePress,
win_scale_factor,
win_last_size,
sys_text_engine,
sys_text_buffers,
cont_text_contents,
cont_input_contents,
cont_text_spans,
topo_active_masks,
topo_parents,
lay_dirty_entities,
lay_taffy_tree,
bar_styles,
lay_taffy_nodes,
lay_resolved_basic,
lay_resolved_flex,
lay_resolved_grid,
rnd_dirty_entities,
rnd_visual,
rnd_base_visual,
sc_offsets,
edit_selections,
edit_selected_rects,
out_rects,
sc_sizes,
debug,
);
} else {
RenderStore::mark_render_dirty(id, topo_active_masks, rnd_dirty_entities);
}
}
#[track_caller]
pub(crate) fn handle_select_all(
id: EntityId,
win_scale_factor: f32,
win_last_size: Option<LayoutSize>,
sys_text_engine: &mut TextEngine,
sys_text_buffers: &TextBufferSparse,
cont_text_contents: &mut TextContentsSparse,
cont_input_contents: &mut InputContentsSparse,
cont_text_spans: &TextSpansSparse,
topo_active_masks: &mut ActiveMasksSecondary,
topo_parents: &ParentsSecondary,
lay_dirty_entities: &mut DirtyLayoutEntitiesVec,
lay_taffy_tree: &mut TaffyTreeEntityId,
bar_styles: &mut ScrollbarStylesSparse,
lay_taffy_nodes: &TaffyNodesSecondary,
lay_resolved_basic: &ResolvedBasicSecondary,
lay_resolved_flex: &ResolvedFlexSecondary,
lay_resolved_grid: &ResolvedGridSparse,
rnd_dirty_entities: &mut DirtyRenderEntitiesVec,
rnd_visual: &mut VisualPropertiesSecondary,
rnd_base_visual: &BaseVisualPropertiesSecondary,
sc_offsets: &mut ScrollOffsetsSecondary,
edit_selections: &mut TextSelectionsSparse,
edit_selected_rects: &mut SelectedRectsSparse,
out_rects: &RectsSecondary,
sc_sizes: &ScrollSizesSecondary,
debug: &mut DebugStore,
) {
let auto_wrap = rnd_visual.auto_wrap(id);
let resolved_geom =
ResolvedGeometry::resolved(id, sc_offsets, lay_resolved_basic, out_rects, debug);
let max_width_opt = resolved_geom.calc_max_width(auto_wrap);
let buffer =
SystemStore::get_or_create_text_buffer(id, max_width_opt, sys_text_buffers, || {
let text = cont_text_contents.at(id);
let font = rnd_visual.font(id);
let spans = cont_text_spans.span(id);
let flex = lay_resolved_flex.find_or(id, &DEFAULT_FLEX, debug);
sys_text_engine.create_buffer(
text,
&font,
flex.text_align,
max_width_opt,
auto_wrap,
spans,
)
});
let input_full_range = if let Some(contents) = cont_input_contents.find_mut(id) {
let raw_text = contents.to_michiu();
let full_range = ByteIndex(0)..raw_text.byte_len();
contents.selected_range = full_range.clone();
contents.selection_reversed = false;
Some(full_range)
} else {
None
};
if let Some(full_range) = input_full_range {
edit_selections.insert(id, full_range);
TextEditStore::update_selection_rects(
id,
&buffer,
edit_selected_rects,
edit_selections,
cont_input_contents,
);
TextEditStore::apply_input_update(
id,
InputOp::SelectAll,
win_scale_factor,
win_last_size,
sys_text_engine,
sys_text_buffers,
cont_text_contents,
cont_input_contents,
cont_text_spans,
topo_active_masks,
topo_parents,
lay_dirty_entities,
lay_taffy_tree,
bar_styles,
lay_taffy_nodes,
lay_resolved_basic,
lay_resolved_flex,
lay_resolved_grid,
rnd_dirty_entities,
rnd_visual,
rnd_base_visual,
sc_offsets,
edit_selections,
edit_selected_rects,
out_rects,
sc_sizes,
debug,
);
} else {
let text_len = cont_text_contents.at(id).byte_len();
edit_selections.insert(id, ByteIndex(0)..text_len);
TextEditStore::update_selection_rects(
id,
&buffer,
edit_selected_rects,
edit_selections,
cont_input_contents,
);
RenderStore::mark_render_dirty(id, topo_active_masks, rnd_dirty_entities);
}
}
#[track_caller]
pub(crate) fn apply_input_update(
id: EntityId,
op: InputOp,
win_scale_factor: f32,
win_last_size: Option<LayoutSize>,
sys_text_engine: &mut TextEngine,
sys_text_buffers: &TextBufferSparse,
cont_text_contents: &mut TextContentsSparse,
cont_input_contents: &mut InputContentsSparse,
cont_text_spans: &TextSpansSparse,
topo_active_masks: &mut ActiveMasksSecondary,
topo_parents: &ParentsSecondary,
lay_dirty_entities: &mut DirtyLayoutEntitiesVec,
lay_taffy_tree: &mut TaffyTreeEntityId,
bar_styles: &mut ScrollbarStylesSparse,
lay_taffy_nodes: &TaffyNodesSecondary,
lay_resolved_basic: &ResolvedBasicSecondary,
lay_resolved_flex: &ResolvedFlexSecondary,
lay_resolved_grid: &ResolvedGridSparse,
rnd_dirty_entities: &mut DirtyRenderEntitiesVec,
rnd_visual: &mut VisualPropertiesSecondary,
rnd_base_visual: &BaseVisualPropertiesSecondary,
sc_offsets: &mut ScrollOffsetsSecondary,
edit_selections: &mut TextSelectionsSparse,
edit_selected_rects: &mut SelectedRectsSparse,
out_rects: &RectsSecondary,
sc_sizes: &ScrollSizesSecondary,
debug: &mut DebugStore,
) {
let has_selection_before = edit_selected_rects.contains_key(id);
let has_selection_after = cont_input_contents
.get(id)
.is_some_and(|c| c.selected_range.start != c.selected_range.end);
if has_selection_before || has_selection_after {
let auto_wrap = rnd_visual.auto_wrap(id);
let resolved_geom =
ResolvedGeometry::resolved(id, sc_offsets, lay_resolved_basic, out_rects, debug);
let max_width_opt = resolved_geom.calc_max_width(auto_wrap);
let buffer =
SystemStore::get_or_create_text_buffer(id, max_width_opt, sys_text_buffers, || {
let text = cont_text_contents.at(id);
let font = rnd_visual.font(id);
let spans = cont_text_spans.span(id);
let flex = lay_resolved_flex.find_or(id, &DEFAULT_FLEX, debug);
sys_text_engine.create_buffer(
text,
&font,
flex.text_align,
max_width_opt,
auto_wrap,
spans,
)
});
TextEditStore::update_selection_rects(
id,
&buffer,
edit_selected_rects,
edit_selections,
cont_input_contents,
);
}
if let Some(c) = cont_input_contents.find_mut(id) {
c.needs_scroll_to_caret = true;
}
match op {
InputOp::MousePress | InputOp::ArrowMove | InputOp::SelectAll => {
TextEditStore::update_input_caret_position(
id,
win_scale_factor,
win_last_size,
sys_text_engine,
sys_text_buffers,
cont_text_contents,
cont_input_contents,
cont_text_spans,
topo_active_masks,
topo_parents,
lay_dirty_entities,
lay_taffy_tree,
bar_styles,
lay_taffy_nodes,
lay_resolved_basic,
lay_resolved_flex,
lay_resolved_grid,
rnd_visual,
rnd_base_visual,
sc_offsets,
edit_selections,
out_rects,
sc_sizes,
debug,
);
RenderStore::mark_render_dirty(id, topo_active_masks, rnd_dirty_entities);
}
InputOp::Init | InputOp::TextEffect | InputOp::ImeUpdated => {
SystemStore::clear_text_buffer_cache(id, sys_text_buffers);
TextEditStore::update_input_caret_position(
id,
win_scale_factor,
win_last_size,
sys_text_engine,
sys_text_buffers,
cont_text_contents,
cont_input_contents,
cont_text_spans,
topo_active_masks,
topo_parents,
lay_dirty_entities,
lay_taffy_tree,
bar_styles,
lay_taffy_nodes,
lay_resolved_basic,
lay_resolved_flex,
lay_resolved_grid,
rnd_visual,
rnd_base_visual,
sc_offsets,
edit_selections,
out_rects,
sc_sizes,
debug,
);
TopologyStore::mark_dirty(
id,
topo_active_masks,
topo_parents,
lay_dirty_entities,
lay_taffy_tree,
lay_taffy_nodes,
rnd_dirty_entities,
debug,
);
}
InputOp::CharTyped
| InputOp::Backspace
| InputOp::Delete
| InputOp::Paste
| InputOp::Cut
| InputOp::Undo
| InputOp::Redo => {
SystemStore::clear_text_buffer_cache(id, sys_text_buffers);
TopologyStore::mark_dirty(
id,
topo_active_masks,
topo_parents,
lay_dirty_entities,
lay_taffy_tree,
lay_taffy_nodes,
rnd_dirty_entities,
debug,
);
}
}
}
pub(crate) fn update_input_caret_position(
id: EntityId,
win_scale_factor: f32,
win_last_size: Option<LayoutSize>,
sys_text_engine: &mut TextEngine,
sys_text_buffers: &TextBufferSparse,
cont_text_contents: &mut TextContentsSparse,
cont_input_contents: &mut InputContentsSparse,
cont_text_spans: &TextSpansSparse,
topo_active_masks: &mut ActiveMasksSecondary,
topo_parents: &ParentsSecondary,
lay_dirty_entities: &mut DirtyLayoutEntitiesVec,
lay_taffy_tree: &mut TaffyTreeEntityId,
bar_styles: &mut ScrollbarStylesSparse,
lay_taffy_nodes: &TaffyNodesSecondary,
lay_resolved_basic: &ResolvedBasicSecondary,
lay_resolved_flex: &ResolvedFlexSecondary,
lay_resolved_grid: &ResolvedGridSparse,
rnd_visual: &mut VisualPropertiesSecondary,
rnd_base_visual: &BaseVisualPropertiesSecondary,
sc_offsets: &mut ScrollOffsetsSecondary,
edit_selections: &mut TextSelectionsSparse,
out_rects: &RectsSecondary,
sc_sizes: &ScrollSizesSecondary,
debug: &mut DebugStore,
) {
let ime_caret_info = TextEditStore::ime_caret_info(
id,
sys_text_engine,
sys_text_buffers,
cont_text_contents,
cont_input_contents,
cont_text_spans,
lay_resolved_basic,
lay_resolved_flex,
rnd_visual,
rnd_base_visual,
edit_selections,
out_rects,
sc_offsets,
debug,
);
let Some((caret, caret_offset, is_multiline)) = ime_caret_info else {
return;
};
let flex = lay_resolved_flex.find_or(id, &DEFAULT_FLEX, debug);
let _grid = lay_resolved_grid.find_or_default(id, debug);
let resolved_geom =
ResolvedGeometry::resolved(id, sc_offsets, lay_resolved_basic, out_rects, debug);
let contents = cont_input_contents.at_mut(id);
let should_scroll = contents.needs_scroll_to_caret;
let mut scroll_offset = sc_offsets.find_or_default(id, debug);
if should_scroll && resolved_geom.rect.width > 0.0 && resolved_geom.rect.height > 0.0 {
let viewport = resolved_geom.viewport_size();
let text_size = if let Some(layout_rect) = contents.last_layout {
TextLayoutSize::new(layout_rect.width, layout_rect.height, None)
} else {
TextLayoutSize::DEFAULT
};
let align_offset =
resolved_geom.calc_align_offset(text_size, flex.text_align, flex.align_items);
let aligned_caret_x = caret.x + align_offset.x;
let aligned_caret_y = caret.y + align_offset.y;
if aligned_caret_x < scroll_offset.x {
scroll_offset.x = aligned_caret_x.max(0.0);
} else if aligned_caret_x + caret.width > scroll_offset.x + viewport.width {
scroll_offset.x = (aligned_caret_x + caret.width - viewport.width).max(0.0);
}
if is_multiline {
if aligned_caret_y < scroll_offset.y {
scroll_offset.y = aligned_caret_y.max(0.0);
} else if aligned_caret_y + caret.height > scroll_offset.y + viewport.height {
scroll_offset.y = (aligned_caret_y + caret.height - viewport.height).max(0.0);
}
} else {
scroll_offset.y = 0.0;
}
ScrollStore::scroll_to(
id,
scroll_offset.x,
scroll_offset.y,
win_last_size,
topo_active_masks,
topo_parents,
lay_dirty_entities,
lay_taffy_tree,
bar_styles,
lay_taffy_nodes,
lay_resolved_basic,
sc_offsets,
out_rects,
sc_sizes,
debug,
);
contents.needs_scroll_to_caret = false;
}
SystemStore::sync_imm_window_position(
&resolved_geom,
win_scale_factor,
caret,
caret_offset,
scroll_offset,
);
}
#[inline]
pub(crate) fn ime_caret_info(
id: EntityId,
sys_text_engine: &mut TextEngine,
sys_text_buffers: &TextBufferSparse,
cont_text_contents: &mut TextContentsSparse,
cont_input_contents: &mut InputContentsSparse,
cont_text_spans: &TextSpansSparse,
lay_resolved_basic: &ResolvedBasicSecondary,
lay_resolved_flex: &ResolvedFlexSecondary,
rnd_visual: &mut VisualPropertiesSecondary,
rnd_base_visual: &BaseVisualPropertiesSecondary,
edit_selections: &mut TextSelectionsSparse,
out_rects: &RectsSecondary,
sc_offsets: &ScrollOffsetsSecondary,
debug: &mut DebugStore,
) -> Option<(LayoutRect, f32, bool)> {
let contents = cont_input_contents.find_mut(id)?;
edit_selections.insert(id, contents.selected_range.clone());
let text_val = contents.to_michiu();
let mut filtered_comp_text = if let Some(ref ime) = contents.ime_state
&& !ime.composition_text.is_empty()
{
if contents.numeric_only {
let filtered: String = ime
.composition_text
.chars()
.filter(|&c| c.is_numeric() || c == '.' || c == '-')
.collect();
MichiuString::from(filtered)
} else {
ime.composition_text.clone()
}
} else {
MichiuString::default()
};
if let Some(max) = contents.max_length
&& !filtered_comp_text.is_empty()
{
filtered_comp_text = TextEditStore::truncate_unconfirmed_text(
&text_val,
contents,
max,
filtered_comp_text,
);
}
if !filtered_comp_text.is_empty() {
filtered_comp_text = contents.mask_if_password(&filtered_comp_text);
}
let text_val_for_display = contents.mask_if_password(&text_val);
let display_text = if !filtered_comp_text.is_empty() {
text_val_for_display.inserted(contents.selected_range.start, &filtered_comp_text)
} else if text_val.is_empty() {
contents
.placeholder
.as_ref()
.map(std::string::ToString::to_string)
.unwrap_or_default()
.into()
} else {
text_val_for_display.to_string().into()
};
cont_text_contents.insert(id, display_text.clone());
let auto_wrap = rnd_visual.auto_wrap(id);
let resolved_geom =
ResolvedGeometry::resolved(id, sc_offsets, lay_resolved_basic, out_rects, debug);
let max_width_opt = resolved_geom.calc_max_width(auto_wrap);
let buffer =
SystemStore::get_or_create_text_buffer(id, max_width_opt, sys_text_buffers, || {
let text = cont_text_contents.at(id);
let font = rnd_visual.font(id);
let spans = cont_text_spans.span(id);
let flex = lay_resolved_flex.find_or(id, &DEFAULT_FLEX, debug);
sys_text_engine.create_buffer(
text,
&font,
flex.text_align,
max_width_opt,
auto_wrap,
spans,
)
});
let text_size = TextEngine::get_layout_size(&buffer);
contents.last_layout = Some(LayoutRect::new(0.0, 0.0, text_size.width, text_size.height));
let composition_offset = if let Some(ref ime) = contents.ime_state
&& !ime.composition_text.is_empty()
{
ime.composition_text.byte_len()
} else {
ByteIndex(0)
};
let current_caret_relative = if contents.selection_reversed {
contents.selected_range.start } else {
contents.selected_range.end };
let caret_index = if contents.is_password {
let mask = contents.mask_text.as_deref().unwrap_or("●");
let base_chars = text_val.to_char_index(current_caret_relative);
let comp_chars = contents
.ime_state
.as_ref()
.map_or(CharIndex(0), |ime| ime.composition_text.char_count());
let total_chars = base_chars.0 + comp_chars.0;
ByteIndex(total_chars * mask.len())
} else {
current_caret_relative + composition_offset
};
let (cx_offset, cy_offset, ch_height) =
TextEngine::get_caret_position(&buffer, caret_index);
contents.measured_caret = LayoutPoint::new(cx_offset, cy_offset);
contents.caret_line_height = ch_height;
let (curr_line, _tot_lines) = display_text.line_indices(caret_index);
contents.current_line_index = curr_line;
cont_text_contents.insert(id, display_text);
let visual = rnd_visual.find_mut(id)?;
let is_ime_active = contents
.ime_state
.as_ref()
.is_some_and(|ime| !ime.composition_text.is_empty());
if text_val.is_empty() && !is_ime_active {
visual.text_color = contents.placeholder_color;
} else {
let base_color = rnd_base_visual
.get(id)
.and_then(|v| v.text_color)
.unwrap_or(Color::WHITE);
visual.text_color = Some(base_color);
}
Some((
LayoutRect {
x: cx_offset,
y: cy_offset,
width: contents.caret_width.unwrap_or(contents.default_caret_width),
height: ch_height,
},
contents.caret_offset,
contents.is_multiline,
))
}
}
impl Context {
#[inline]
pub(crate) fn apply_input_update(&mut self, id: EntityId, op: InputOp) {
TextEditStore::apply_input_update(
id,
op,
self.window.win_scale_factor,
self.window.win_last_size,
&mut self.system.sys_text_engine,
&self.system.sys_text_buffers,
&mut self.contents.cont_text_contents,
&mut self.contents.cont_input_contents,
&self.contents.cont_text_spans,
&mut self.topology.topo_active_masks,
&self.topology.topo_parents,
&mut self.layouts.lay_dirty_entities,
&mut self.layouts.lay_taffy_tree,
&mut self.layouts.scrollbar.bar_styles,
&self.layouts.lay_taffy_nodes,
&self.layouts.lay_resolved_basic,
&self.layouts.lay_resolved_flex,
&self.layouts.lay_resolved_grid,
&mut self.renders.rnd_dirty_entities,
&mut self.renders.rnd_visual,
&self.renders.rnd_base_visual,
&mut self.states.scroll.sc_offsets,
&mut self.states.edit.edit_selections,
&mut self.states.edit.edit_selected_rects,
&self.outputs.out_rects,
&self.states.scroll.sc_sizes,
&mut self.debug,
);
}
}