fix(text_input): RTL text cursor and highlight fixes
This commit is contained in:
parent
c33455e9ad
commit
2299fba69b
3 changed files with 365 additions and 157 deletions
|
|
@ -3,16 +3,19 @@
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//! Track the cursor of a text input.
|
//! Track the cursor of a text input.
|
||||||
|
use iced_core::text::Affinity;
|
||||||
|
|
||||||
use super::value::Value;
|
use super::value::Value;
|
||||||
|
|
||||||
/// The cursor of a text input.
|
/// The cursor of a text input.
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
pub struct Cursor {
|
pub struct Cursor {
|
||||||
state: State,
|
state: State,
|
||||||
|
affinity: Affinity,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The state of a [`Cursor`].
|
/// The state of a [`Cursor`].
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
pub enum State {
|
pub enum State {
|
||||||
/// Cursor without a selection
|
/// Cursor without a selection
|
||||||
Index(usize),
|
Index(usize),
|
||||||
|
|
@ -31,6 +34,7 @@ impl Default for Cursor {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
state: State::Index(0),
|
state: State::Index(0),
|
||||||
|
affinity: Affinity::Before,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -193,4 +197,37 @@ impl Cursor {
|
||||||
State::Selection { start, end } => start.max(end),
|
State::Selection { start, end } => start.max(end),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the current cursor [`Affinity`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn affinity(&self) -> Affinity {
|
||||||
|
self.affinity
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the cursor [`Affinity`].
|
||||||
|
pub fn set_affinity(&mut self, affinity: Affinity) {
|
||||||
|
self.affinity = affinity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves the cursor in a visual direction, accounting for RTL text.
|
||||||
|
///
|
||||||
|
/// `forward` = `true` is visually rightward.
|
||||||
|
pub fn move_visual(&mut self, forward: bool, by_words: bool, rtl: bool, value: &Value) {
|
||||||
|
match (forward ^ rtl, by_words) {
|
||||||
|
(true, false) => self.move_right(value),
|
||||||
|
(true, true) => self.move_right_by_words(value),
|
||||||
|
(false, false) => self.move_left(value),
|
||||||
|
(false, true) => self.move_left_by_words(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extends the selection in a visual direction, accounting for RTL text.
|
||||||
|
pub fn select_visual(&mut self, forward: bool, by_words: bool, rtl: bool, value: &Value) {
|
||||||
|
match (forward ^ rtl, by_words) {
|
||||||
|
(true, false) => self.select_right(value),
|
||||||
|
(true, true) => self.select_right_by_words(value),
|
||||||
|
(false, false) => self.select_left(value),
|
||||||
|
(false, true) => self.select_left_by_words(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -937,6 +937,18 @@ where
|
||||||
self.drag_threshold,
|
self.drag_threshold,
|
||||||
self.always_active,
|
self.always_active,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let state = tree.state.downcast_mut::<State>();
|
||||||
|
let value = if self.is_secure {
|
||||||
|
self.value.secure()
|
||||||
|
} else {
|
||||||
|
self.value.clone()
|
||||||
|
};
|
||||||
|
state.scroll_offset = offset(
|
||||||
|
text_layout.children().next().unwrap().bounds(),
|
||||||
|
&value,
|
||||||
|
state,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
@ -1435,7 +1447,17 @@ pub fn update<'a, Message: Clone + 'static>(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let target = cursor_position.x - text_layout.bounds().x;
|
let target = {
|
||||||
|
let text_bounds = text_layout.bounds();
|
||||||
|
|
||||||
|
let alignment_offset = alignment_offset(
|
||||||
|
text_bounds.width,
|
||||||
|
state.value.raw().min_width(),
|
||||||
|
effective_alignment(state.value.raw()),
|
||||||
|
);
|
||||||
|
|
||||||
|
cursor_position.x - text_bounds.x - alignment_offset
|
||||||
|
};
|
||||||
|
|
||||||
let click =
|
let click =
|
||||||
mouse::Click::new(cursor_position, mouse::Button::Left, state.last_click);
|
mouse::Click::new(cursor_position, mouse::Button::Left, state.last_click);
|
||||||
|
|
@ -1454,17 +1476,30 @@ pub fn update<'a, Message: Clone + 'static>(
|
||||||
state.value.raw(),
|
state.value.raw(),
|
||||||
text_layout.bounds(),
|
text_layout.bounds(),
|
||||||
left,
|
left,
|
||||||
|
value,
|
||||||
|
state.cursor.affinity(),
|
||||||
|
state.scroll_offset,
|
||||||
);
|
);
|
||||||
|
|
||||||
let (right_position, _right_offset) = measure_cursor_and_scroll_offset(
|
let (right_position, _right_offset) = measure_cursor_and_scroll_offset(
|
||||||
state.value.raw(),
|
state.value.raw(),
|
||||||
text_layout.bounds(),
|
text_layout.bounds(),
|
||||||
right,
|
right,
|
||||||
|
value,
|
||||||
|
state.cursor.affinity(),
|
||||||
|
state.scroll_offset,
|
||||||
);
|
);
|
||||||
|
|
||||||
let width = right_position - left_position;
|
let selection_start = left_position.min(right_position);
|
||||||
|
let width = (right_position - left_position).abs();
|
||||||
|
let alignment_offset = alignment_offset(
|
||||||
|
text_layout.bounds().width,
|
||||||
|
state.value.raw().min_width(),
|
||||||
|
effective_alignment(state.value.raw()),
|
||||||
|
);
|
||||||
let selection_bounds = Rectangle {
|
let selection_bounds = Rectangle {
|
||||||
x: text_layout.bounds().x + left_position,
|
x: text_layout.bounds().x + alignment_offset + selection_start
|
||||||
|
- state.scroll_offset,
|
||||||
y: text_layout.bounds().y,
|
y: text_layout.bounds().y,
|
||||||
width,
|
width,
|
||||||
height: text_layout.bounds().height,
|
height: text_layout.bounds().height,
|
||||||
|
|
@ -1492,10 +1527,11 @@ pub fn update<'a, Message: Clone + 'static>(
|
||||||
if is_secure {
|
if is_secure {
|
||||||
state.cursor.select_all(value);
|
state.cursor.select_all(value);
|
||||||
} else {
|
} else {
|
||||||
let position =
|
let (position, affinity) =
|
||||||
find_cursor_position(text_layout.bounds(), value, state, target)
|
find_cursor_position(text_layout.bounds(), value, state, target)
|
||||||
.unwrap_or(0);
|
.unwrap_or((0, text::Affinity::Before));
|
||||||
|
|
||||||
|
state.cursor.set_affinity(affinity);
|
||||||
state.cursor.select_range(
|
state.cursor.select_range(
|
||||||
value.previous_start_of_word(position),
|
value.previous_start_of_word(position),
|
||||||
value.next_end_of_word(position),
|
value.next_end_of_word(position),
|
||||||
|
|
@ -1561,7 +1597,17 @@ pub fn update<'a, Message: Clone + 'static>(
|
||||||
// clear selection and place cursor at click position
|
// clear selection and place cursor at click position
|
||||||
update_cache(state, value);
|
update_cache(state, value);
|
||||||
if let Some(position) = cursor.position_over(layout.bounds()) {
|
if let Some(position) = cursor.position_over(layout.bounds()) {
|
||||||
let target = position.x - text_layout.bounds().x;
|
let target = {
|
||||||
|
let text_bounds = text_layout.bounds();
|
||||||
|
|
||||||
|
let alignment_offset = alignment_offset(
|
||||||
|
text_bounds.width,
|
||||||
|
state.value.raw().min_width(),
|
||||||
|
effective_alignment(state.value.raw()),
|
||||||
|
);
|
||||||
|
|
||||||
|
position.x - text_bounds.x - alignment_offset
|
||||||
|
};
|
||||||
state.setting_selection(value, text_layout.bounds(), target);
|
state.setting_selection(value, text_layout.bounds(), target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1576,12 +1622,24 @@ pub fn update<'a, Message: Clone + 'static>(
|
||||||
let state = state();
|
let state = state();
|
||||||
|
|
||||||
if matches!(state.dragging_state, Some(DraggingState::Selection)) {
|
if matches!(state.dragging_state, Some(DraggingState::Selection)) {
|
||||||
let target = position.x - text_layout.bounds().x;
|
let target = {
|
||||||
|
let text_bounds = text_layout.bounds();
|
||||||
|
|
||||||
|
let alignment_offset = alignment_offset(
|
||||||
|
text_bounds.width,
|
||||||
|
state.value.raw().min_width(),
|
||||||
|
effective_alignment(state.value.raw()),
|
||||||
|
);
|
||||||
|
|
||||||
|
position.x - text_bounds.x - alignment_offset
|
||||||
|
};
|
||||||
|
|
||||||
update_cache(state, value);
|
update_cache(state, value);
|
||||||
let position =
|
let (position, affinity) =
|
||||||
find_cursor_position(text_layout.bounds(), value, state, target).unwrap_or(0);
|
find_cursor_position(text_layout.bounds(), value, state, target)
|
||||||
|
.unwrap_or((0, text::Affinity::Before));
|
||||||
|
|
||||||
|
state.cursor.set_affinity(affinity);
|
||||||
state
|
state
|
||||||
.cursor
|
.cursor
|
||||||
.select_range(state.cursor.start(value), position);
|
.select_range(state.cursor.start(value), position);
|
||||||
|
|
@ -1860,29 +1918,23 @@ pub fn update<'a, Message: Clone + 'static>(
|
||||||
update_cache(state, &value);
|
update_cache(state, &value);
|
||||||
}
|
}
|
||||||
keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => {
|
keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => {
|
||||||
if platform::is_jump_modifier_pressed(modifiers) && !is_secure {
|
let rtl = state.value.raw().is_rtl(0).unwrap_or(false);
|
||||||
|
let by_words = platform::is_jump_modifier_pressed(modifiers) && !is_secure;
|
||||||
|
|
||||||
if modifiers.shift() {
|
if modifiers.shift() {
|
||||||
state.cursor.select_left_by_words(value);
|
state.cursor.select_visual(false, by_words, rtl, value);
|
||||||
} else {
|
} else {
|
||||||
state.cursor.move_left_by_words(value);
|
state.cursor.move_visual(false, by_words, rtl, value);
|
||||||
}
|
|
||||||
} else if modifiers.shift() {
|
|
||||||
state.cursor.select_left(value);
|
|
||||||
} else {
|
|
||||||
state.cursor.move_left(value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keyboard::Key::Named(keyboard::key::Named::ArrowRight) => {
|
keyboard::Key::Named(keyboard::key::Named::ArrowRight) => {
|
||||||
if platform::is_jump_modifier_pressed(modifiers) && !is_secure {
|
let rtl = state.value.raw().is_rtl(0).unwrap_or(false);
|
||||||
|
let by_words = platform::is_jump_modifier_pressed(modifiers) && !is_secure;
|
||||||
|
|
||||||
if modifiers.shift() {
|
if modifiers.shift() {
|
||||||
state.cursor.select_right_by_words(value);
|
state.cursor.select_visual(true, by_words, rtl, value);
|
||||||
} else {
|
} else {
|
||||||
state.cursor.move_right_by_words(value);
|
state.cursor.move_visual(true, by_words, rtl, value);
|
||||||
}
|
|
||||||
} else if modifiers.shift() {
|
|
||||||
state.cursor.select_right(value);
|
|
||||||
} else {
|
|
||||||
state.cursor.move_right(value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keyboard::Key::Named(keyboard::key::Named::Home) => {
|
keyboard::Key::Named(keyboard::key::Named::Home) => {
|
||||||
|
|
@ -2016,18 +2068,27 @@ pub fn update<'a, Message: Clone + 'static>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if accepted {
|
if accepted {
|
||||||
let target = *x as f32 - text_layout.bounds().x;
|
let target = {
|
||||||
|
let text_bounds = text_layout.bounds();
|
||||||
|
|
||||||
|
let alignment_offset = alignment_offset(
|
||||||
|
text_bounds.width,
|
||||||
|
state.value.raw().min_width(),
|
||||||
|
effective_alignment(state.value.raw()),
|
||||||
|
);
|
||||||
|
|
||||||
|
*x as f32 - text_bounds.x - alignment_offset
|
||||||
|
};
|
||||||
state.dnd_offer =
|
state.dnd_offer =
|
||||||
DndOfferState::HandlingOffer(mime_types.clone(), DndAction::empty());
|
DndOfferState::HandlingOffer(mime_types.clone(), DndAction::empty());
|
||||||
// existing logic for setting the selection
|
// existing logic for setting the selection
|
||||||
let position = if target > 0.0 {
|
|
||||||
update_cache(state, value);
|
update_cache(state, value);
|
||||||
|
let (position, affinity) =
|
||||||
find_cursor_position(text_layout.bounds(), value, state, target)
|
find_cursor_position(text_layout.bounds(), value, state, target)
|
||||||
} else {
|
.unwrap_or((0, text::Affinity::Before));
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
state.cursor.move_to(position.unwrap_or(0));
|
state.cursor.set_affinity(affinity);
|
||||||
|
state.cursor.move_to(position);
|
||||||
shell.capture_event();
|
shell.capture_event();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -2038,16 +2099,25 @@ pub fn update<'a, Message: Clone + 'static>(
|
||||||
{
|
{
|
||||||
let state = state();
|
let state = state();
|
||||||
|
|
||||||
let target = *x as f32 - text_layout.bounds().x;
|
let target = {
|
||||||
// existing logic for setting the selection
|
let text_bounds = text_layout.bounds();
|
||||||
let position = if target > 0.0 {
|
|
||||||
update_cache(state, value);
|
|
||||||
find_cursor_position(text_layout.bounds(), value, state, target)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
state.cursor.move_to(position.unwrap_or(0));
|
let alignment_offset = alignment_offset(
|
||||||
|
text_bounds.width,
|
||||||
|
state.value.raw().min_width(),
|
||||||
|
effective_alignment(state.value.raw()),
|
||||||
|
);
|
||||||
|
|
||||||
|
*x as f32 - text_bounds.x - alignment_offset
|
||||||
|
};
|
||||||
|
// existing logic for setting the selection
|
||||||
|
update_cache(state, value);
|
||||||
|
let (position, affinity) =
|
||||||
|
find_cursor_position(text_layout.bounds(), value, state, target)
|
||||||
|
.unwrap_or((0, text::Affinity::Before));
|
||||||
|
|
||||||
|
state.cursor.set_affinity(affinity);
|
||||||
|
state.cursor.move_to(position);
|
||||||
shell.capture_event();
|
shell.capture_event();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -2340,7 +2410,7 @@ pub fn draw<'a, Message>(
|
||||||
let handling_dnd_offer = !matches!(state.dnd_offer, DndOfferState::None);
|
let handling_dnd_offer = !matches!(state.dnd_offer, DndOfferState::None);
|
||||||
#[cfg(not(all(feature = "wayland", target_os = "linux")))]
|
#[cfg(not(all(feature = "wayland", target_os = "linux")))]
|
||||||
let handling_dnd_offer = false;
|
let handling_dnd_offer = false;
|
||||||
let (cursor, offset) = if let Some(focus) =
|
let (cursors, offset, is_selecting) = if let Some(focus) =
|
||||||
state.is_focused.filter(|f| f.focused).or_else(|| {
|
state.is_focused.filter(|f| f.focused).or_else(|| {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
handling_dnd_offer.then_some(Focus {
|
handling_dnd_offer.then_some(Focus {
|
||||||
|
|
@ -2352,25 +2422,24 @@ pub fn draw<'a, Message>(
|
||||||
}) {
|
}) {
|
||||||
match state.cursor.state(value) {
|
match state.cursor.state(value) {
|
||||||
cursor::State::Index(position) => {
|
cursor::State::Index(position) => {
|
||||||
let (text_value_width, offset) =
|
let (text_value_width, _) = measure_cursor_and_scroll_offset(
|
||||||
measure_cursor_and_scroll_offset(state.value.raw(), text_bounds, position);
|
state.value.raw(),
|
||||||
|
text_bounds,
|
||||||
|
position,
|
||||||
|
value,
|
||||||
|
state.cursor.affinity(),
|
||||||
|
state.scroll_offset,
|
||||||
|
);
|
||||||
let is_cursor_visible = handling_dnd_offer
|
let is_cursor_visible = handling_dnd_offer
|
||||||
|| ((focus.now - focus.updated_at).as_millis() / CURSOR_BLINK_INTERVAL_MILLIS)
|
|| ((focus.now - focus.updated_at).as_millis() / CURSOR_BLINK_INTERVAL_MILLIS)
|
||||||
.is_multiple_of(2);
|
.is_multiple_of(2);
|
||||||
if is_cursor_visible {
|
|
||||||
if dnd_icon {
|
if is_cursor_visible && !dnd_icon {
|
||||||
(None, 0.0)
|
|
||||||
} else {
|
|
||||||
(
|
(
|
||||||
Some((
|
vec![(
|
||||||
renderer::Quad {
|
renderer::Quad {
|
||||||
bounds: Rectangle {
|
bounds: Rectangle {
|
||||||
x: text_bounds.x + text_value_width - offset
|
x: (text_bounds.x + text_value_width).floor(),
|
||||||
+ if text_value_width < 0. {
|
|
||||||
actual_width
|
|
||||||
} else {
|
|
||||||
0.
|
|
||||||
},
|
|
||||||
y: text_bounds.y,
|
y: text_bounds.y,
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
height: text_bounds.height,
|
height: text_bounds.height,
|
||||||
|
|
@ -2388,42 +2457,43 @@ pub fn draw<'a, Message>(
|
||||||
snap: true,
|
snap: true,
|
||||||
},
|
},
|
||||||
text_color,
|
text_color,
|
||||||
)),
|
)],
|
||||||
offset,
|
state.scroll_offset,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
(None, offset)
|
(
|
||||||
|
Vec::<(renderer::Quad, Color)>::new(),
|
||||||
|
if dnd_icon { 0.0 } else { state.scroll_offset },
|
||||||
|
false,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cursor::State::Selection { start, end } => {
|
cursor::State::Selection { start, end } => {
|
||||||
let left = start.min(end);
|
let left = start.min(end);
|
||||||
let right = end.max(start);
|
let right = end.max(start);
|
||||||
|
|
||||||
let value_paragraph = &state.value;
|
|
||||||
let (left_position, left_offset) =
|
|
||||||
measure_cursor_and_scroll_offset(value_paragraph.raw(), text_bounds, left);
|
|
||||||
|
|
||||||
let (right_position, right_offset) =
|
|
||||||
measure_cursor_and_scroll_offset(value_paragraph.raw(), text_bounds, right);
|
|
||||||
|
|
||||||
let width = right_position - left_position;
|
|
||||||
if dnd_icon {
|
if dnd_icon {
|
||||||
(None, 0.0)
|
(Vec::<(renderer::Quad, Color)>::new(), 0.0, true)
|
||||||
} else {
|
} else {
|
||||||
|
let lo_byte = value.byte_index_at_grapheme(left);
|
||||||
|
let hi_byte = value.byte_index_at_grapheme(right);
|
||||||
|
|
||||||
|
let rects = state.value.raw().highlight(
|
||||||
|
0,
|
||||||
|
(lo_byte, text::Affinity::After),
|
||||||
|
(hi_byte, text::Affinity::Before),
|
||||||
|
);
|
||||||
|
|
||||||
|
let cursors: Vec<(renderer::Quad, Color)> = rects
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| {
|
||||||
(
|
(
|
||||||
Some((
|
|
||||||
renderer::Quad {
|
renderer::Quad {
|
||||||
bounds: Rectangle {
|
bounds: Rectangle {
|
||||||
x: text_bounds.x
|
x: text_bounds.x + r.x,
|
||||||
+ left_position
|
|
||||||
+ if left_position < 0. || right_position < 0. {
|
|
||||||
actual_width
|
|
||||||
} else {
|
|
||||||
0.
|
|
||||||
},
|
|
||||||
y: text_bounds.y,
|
y: text_bounds.y,
|
||||||
width,
|
width: r.width,
|
||||||
height: text_bounds.height,
|
height: text_bounds.height,
|
||||||
},
|
},
|
||||||
border: Border {
|
border: Border {
|
||||||
|
|
@ -2439,29 +2509,48 @@ pub fn draw<'a, Message>(
|
||||||
snap: true,
|
snap: true,
|
||||||
},
|
},
|
||||||
appearance.selected_fill,
|
appearance.selected_fill,
|
||||||
)),
|
|
||||||
if end == right {
|
|
||||||
right_offset
|
|
||||||
} else {
|
|
||||||
left_offset
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
(cursors, state.scroll_offset, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
(None, 0.0)
|
let unfocused_offset = match effective_alignment(state.value.raw()) {
|
||||||
|
alignment::Horizontal::Right => {
|
||||||
|
(state.value.raw().min_width() - text_bounds.width).max(0.0)
|
||||||
|
}
|
||||||
|
_ => 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
Vec::<(renderer::Quad, Color)>::new(),
|
||||||
|
unfocused_offset,
|
||||||
|
false,
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let render = |renderer: &mut crate::Renderer| {
|
let render = |renderer: &mut crate::Renderer| {
|
||||||
if let Some((cursor, color)) = cursor {
|
let alignment_offset = alignment_offset(
|
||||||
renderer.fill_quad(cursor, color);
|
text_bounds.width,
|
||||||
|
state.value.raw().min_width(),
|
||||||
|
effective_alignment(state.value.raw()),
|
||||||
|
);
|
||||||
|
|
||||||
|
if !cursors.is_empty() {
|
||||||
|
renderer.with_translation(Vector::new(alignment_offset - offset, 0.0), |renderer| {
|
||||||
|
for (quad, color) in &cursors {
|
||||||
|
renderer.fill_quad(*quad, *color);
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
renderer.with_translation(Vector::ZERO, |_| {});
|
renderer.with_translation(Vector::ZERO, |_| {});
|
||||||
}
|
}
|
||||||
|
|
||||||
let bounds = Rectangle {
|
let bounds = Rectangle {
|
||||||
x: text_bounds.x - offset,
|
x: text_bounds.x + alignment_offset - offset,
|
||||||
y: text_bounds.center_y(),
|
y: text_bounds.center_y(),
|
||||||
width: actual_width,
|
width: actual_width,
|
||||||
..text_bounds
|
..text_bounds
|
||||||
|
|
@ -2482,7 +2571,7 @@ pub fn draw<'a, Message>(
|
||||||
font,
|
font,
|
||||||
bounds: bounds.size(),
|
bounds: bounds.size(),
|
||||||
size: iced::Pixels(size),
|
size: iced::Pixels(size),
|
||||||
align_x: text::Alignment::Left,
|
align_x: text::Alignment::Default,
|
||||||
align_y: alignment::Vertical::Center,
|
align_y: alignment::Vertical::Center,
|
||||||
line_height: text::LineHeight::default(),
|
line_height: text::LineHeight::default(),
|
||||||
shaping: text::Shaping::Advanced,
|
shaping: text::Shaping::Advanced,
|
||||||
|
|
@ -2495,7 +2584,11 @@ pub fn draw<'a, Message>(
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
renderer.with_layer(text_bounds, render);
|
if is_selecting {
|
||||||
|
renderer.with_layer(bounds, render);
|
||||||
|
} else {
|
||||||
|
render(renderer);
|
||||||
|
}
|
||||||
|
|
||||||
let trailing_icon_tree = children.get(child_index);
|
let trailing_icon_tree = children.get(child_index);
|
||||||
|
|
||||||
|
|
@ -2630,7 +2723,7 @@ pub struct State {
|
||||||
last_click: Option<mouse::Click>,
|
last_click: Option<mouse::Click>,
|
||||||
cursor: Cursor,
|
cursor: Cursor,
|
||||||
keyboard_modifiers: keyboard::Modifiers,
|
keyboard_modifiers: keyboard::Modifiers,
|
||||||
// TODO: Add stateful horizontal scrolling offset
|
scroll_offset: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
|
@ -2709,6 +2802,7 @@ impl State {
|
||||||
last_click: None,
|
last_click: None,
|
||||||
cursor: Cursor::default(),
|
cursor: Cursor::default(),
|
||||||
keyboard_modifiers: keyboard::Modifiers::default(),
|
keyboard_modifiers: keyboard::Modifiers::default(),
|
||||||
|
scroll_offset: 0.0,
|
||||||
dirty: false,
|
dirty: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2797,13 +2891,11 @@ impl State {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn setting_selection(&mut self, value: &Value, bounds: Rectangle<f32>, target: f32) {
|
pub(super) fn setting_selection(&mut self, value: &Value, bounds: Rectangle<f32>, target: f32) {
|
||||||
let position = if target > 0.0 {
|
let (position, affinity) = find_cursor_position(bounds, value, self, target)
|
||||||
find_cursor_position(bounds, value, self, target)
|
.unwrap_or((0, text::Affinity::Before));
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
self.cursor.move_to(position.unwrap_or(0));
|
self.cursor.set_affinity(affinity);
|
||||||
|
self.cursor.move_to(position);
|
||||||
self.dragging_state = Some(DraggingState::Selection);
|
self.dragging_state = Some(DraggingState::Selection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2867,14 +2959,33 @@ fn measure_cursor_and_scroll_offset(
|
||||||
paragraph: &impl text::Paragraph,
|
paragraph: &impl text::Paragraph,
|
||||||
text_bounds: Rectangle,
|
text_bounds: Rectangle,
|
||||||
cursor_index: usize,
|
cursor_index: usize,
|
||||||
|
value: &Value,
|
||||||
|
affinity: text::Affinity,
|
||||||
|
current_offset: f32,
|
||||||
) -> (f32, f32) {
|
) -> (f32, f32) {
|
||||||
let grapheme_position = paragraph
|
let byte_index = value.byte_index_at_grapheme(cursor_index);
|
||||||
.grapheme_position(0, cursor_index)
|
let position = paragraph
|
||||||
|
.cursor_position(0, byte_index, affinity)
|
||||||
.unwrap_or(Point::ORIGIN);
|
.unwrap_or(Point::ORIGIN);
|
||||||
|
|
||||||
let offset = ((grapheme_position.x + 5.0) - text_bounds.width).max(0.0);
|
// The visible window in paragraph coordinates is:
|
||||||
|
// [current_offset, current_offset + text_bounds.width]
|
||||||
|
// Keep the cursor visible with a 5px margin on each side.
|
||||||
|
let offset = if position.x > current_offset + text_bounds.width - 5.0 {
|
||||||
|
// Cursor past right edge of visible window → scroll left
|
||||||
|
(position.x + 5.0) - text_bounds.width
|
||||||
|
} else if position.x < current_offset + 5.0 {
|
||||||
|
// Cursor past left edge of visible window → scroll right
|
||||||
|
position.x - 5.0
|
||||||
|
} else {
|
||||||
|
// Cursor is within visible window → keep current scroll
|
||||||
|
current_offset
|
||||||
|
};
|
||||||
|
|
||||||
(grapheme_position.x, offset)
|
let max_offset = (paragraph.min_width() - text_bounds.width).max(0.0);
|
||||||
|
let offset = offset.clamp(0.0, max_offset);
|
||||||
|
|
||||||
|
(position.x, offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Computes the position of the text cursor at the given X coordinate of
|
/// Computes the position of the text cursor at the given X coordinate of
|
||||||
|
|
@ -2885,23 +2996,23 @@ fn find_cursor_position(
|
||||||
value: &Value,
|
value: &Value,
|
||||||
state: &State,
|
state: &State,
|
||||||
x: f32,
|
x: f32,
|
||||||
) -> Option<usize> {
|
) -> Option<(usize, text::Affinity)> {
|
||||||
let offset = offset(text_bounds, value, state);
|
let value_str = value.to_string();
|
||||||
let value = value.to_string();
|
|
||||||
|
|
||||||
let char_offset = state
|
let hit = state.value.raw().hit_test(Point::new(
|
||||||
.value
|
x + state.scroll_offset,
|
||||||
.raw()
|
text_bounds.height / 2.0,
|
||||||
.hit_test(Point::new(x + offset, text_bounds.height / 2.0))
|
))?;
|
||||||
.map(text::Hit::cursor)?;
|
let char_offset = hit.cursor();
|
||||||
|
let affinity = hit.affinity();
|
||||||
|
|
||||||
Some(
|
let grapheme_count = unicode_segmentation::UnicodeSegmentation::graphemes(
|
||||||
unicode_segmentation::UnicodeSegmentation::graphemes(
|
&value_str[..char_offset.min(value_str.len())],
|
||||||
&value[..char_offset.min(value.len())],
|
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.count(),
|
.count();
|
||||||
)
|
|
||||||
|
Some((grapheme_count, affinity))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(never)]
|
#[inline(never)]
|
||||||
|
|
@ -2928,7 +3039,7 @@ fn replace_paragraph(
|
||||||
content: value.to_string(),
|
content: value.to_string(),
|
||||||
bounds,
|
bounds,
|
||||||
size: text_size,
|
size: text_size,
|
||||||
align_x: text::Alignment::Left,
|
align_x: text::Alignment::Default,
|
||||||
align_y: alignment::Vertical::Top,
|
align_y: alignment::Vertical::Top,
|
||||||
shaping: text::Shaping::Advanced,
|
shaping: text::Shaping::Advanced,
|
||||||
wrapping: text::Wrapping::None,
|
wrapping: text::Wrapping::None,
|
||||||
|
|
@ -2961,11 +3072,48 @@ fn offset(text_bounds: Rectangle, value: &Value, state: &State) -> f32 {
|
||||||
cursor::State::Selection { end, .. } => end,
|
cursor::State::Selection { end, .. } => end,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (_, offset) =
|
let (_, offset) = measure_cursor_and_scroll_offset(
|
||||||
measure_cursor_and_scroll_offset(state.value.raw(), text_bounds, focus_position);
|
state.value.raw(),
|
||||||
|
text_bounds,
|
||||||
|
focus_position,
|
||||||
|
value,
|
||||||
|
state.cursor().affinity(),
|
||||||
|
state.scroll_offset,
|
||||||
|
);
|
||||||
|
|
||||||
offset
|
offset
|
||||||
} else {
|
} else {
|
||||||
|
match effective_alignment(state.value.raw()) {
|
||||||
|
alignment::Horizontal::Right => {
|
||||||
|
(state.value.raw().min_width() - text_bounds.width).max(0.0)
|
||||||
|
}
|
||||||
|
_ => 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(never)]
|
||||||
|
fn alignment_offset(
|
||||||
|
text_bounds_width: f32,
|
||||||
|
text_min_width: f32,
|
||||||
|
alignment: alignment::Horizontal,
|
||||||
|
) -> f32 {
|
||||||
|
if text_min_width > text_bounds_width {
|
||||||
0.0
|
0.0
|
||||||
|
} else {
|
||||||
|
match alignment {
|
||||||
|
alignment::Horizontal::Left => 0.0,
|
||||||
|
alignment::Horizontal::Center => (text_bounds_width - text_min_width) / 2.0,
|
||||||
|
alignment::Horizontal::Right => text_bounds_width - text_min_width,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(never)]
|
||||||
|
fn effective_alignment(paragraph: &impl text::Paragraph) -> alignment::Horizontal {
|
||||||
|
if paragraph.is_rtl(0).unwrap_or(false) {
|
||||||
|
alignment::Horizontal::Right
|
||||||
|
} else {
|
||||||
|
alignment::Horizontal::Left
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -132,11 +132,34 @@ impl Value {
|
||||||
graphemes: std::iter::repeat_n(String::from("•"), self.graphemes.len()).collect(),
|
graphemes: std::iter::repeat_n(String::from("•"), self.graphemes.len()).collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts a grapheme index to a byte index in the underlying string.
|
||||||
|
#[must_use]
|
||||||
|
pub fn byte_index_at_grapheme(&self, grapheme_index: usize) -> usize {
|
||||||
|
self.graphemes[..grapheme_index.min(self.graphemes.len())]
|
||||||
|
.iter()
|
||||||
|
.map(|g| g.len())
|
||||||
|
.sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToString for Value {
|
/// Converts a byte index to a grapheme index.
|
||||||
|
#[must_use]
|
||||||
|
pub fn grapheme_index_at_byte(&self, byte_index: usize) -> usize {
|
||||||
|
let mut bytes = 0;
|
||||||
|
for (i, g) in self.graphemes.iter().enumerate() {
|
||||||
|
if bytes >= byte_index {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
bytes += g.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.graphemes.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Value {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn to_string(&self) -> String {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
self.graphemes.concat()
|
f.write_str(&self.graphemes.concat())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue