use std::borrow::{Borrow, BorrowMut, Cow};
use std::cmp::Ordering;
use std::fmt::{self, Debug, Display, Formatter, Write};
use std::hash::Hash;
use std::ops::{Deref, DerefMut};
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use figures::units::{Lp, Px, UPx};
use figures::{
Abs, FloatConversion, IntoSigned, IntoUnsigned, Point, Rect, Round, ScreenScale, Size, Zero,
};
use intentional::Cast;
use kludgine::app::winit::event::{ElementState, Ime};
use kludgine::app::winit::keyboard::{Key, NamedKey};
use kludgine::app::winit::window::{CursorIcon, ImePurpose};
use kludgine::shapes::{Shape, StrokeOptions};
use kludgine::text::{MeasuredText, Text, TextOrigin};
use kludgine::{CanRenderTo, Color, DrawableExt};
use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation};
use zeroize::Zeroizing;
use crate::context::{EventContext, GraphicsContext, LayoutContext};
use crate::styles::components::{HighlightColor, IntrinsicPadding, OutlineColor, TextColor};
use crate::utils::ModifiersExt;
use crate::value::{Destination, Dynamic, Generation, IntoDynamic, IntoValue, Source, Value};
use crate::widget::{Callback, EventHandling, Widget, HANDLED, IGNORED};
use crate::window::KeyEvent;
use crate::{ConstraintLimit, Lazy};
const CURSOR_BLINK_DURATION: Duration = Duration::from_millis(500);
#[must_use]
pub struct Input<Storage = String> {
pub value: Dynamic<Storage>,
pub placeholder: Value<String>,
mask_symbol: Value<CowString>,
mask: CowString,
on_key: Option<Callback<KeyEvent, EventHandling>>,
cache: Option<CachedLayout>,
selection: SelectionState,
blink_state: BlinkState,
needs_to_select_all: bool,
mouse_buttons_down: usize,
line_navigation_x_target: Option<Px>,
window_focused: bool,
}
#[derive(Eq, PartialEq, Clone, Copy)]
struct CacheKey {
generation: Generation,
mask_generation: Option<Generation>,
placeholder_generation: Option<Generation>,
width: Option<Px>,
color: Color,
mask_bytes: usize,
cursor: Cursor,
selection: Option<Cursor>,
}
struct CachedLayout {
bytes: usize,
measured: MeasuredText<Px>,
placeholder: MeasuredText<Px>,
key: CacheKey,
}
#[derive(Clone, Copy, Eq, PartialEq, Debug, Default)]
pub struct SelectionState {
pub cursor: Cursor,
pub start: Option<Cursor>,
}
#[derive(Clone, Copy, Eq, PartialEq, Debug, Ord, PartialOrd, Default)]
pub struct Cursor {
pub offset: usize,
pub affinity: Affinity,
}
#[derive(Clone, Copy, Eq, PartialEq, Debug, Ord, PartialOrd, Default)]
pub enum Affinity {
#[default]
Before,
After,
}
impl<Storage> Input<Storage>
where
Storage: InputStorage,
{
pub fn new(initial_value: impl IntoDynamic<Storage>) -> Self {
Self {
value: initial_value.into_dynamic(),
mask: CowString::default(),
mask_symbol: Storage::MASKED
.then(|| CowString::from('\u{2022}'))
.unwrap_or_default()
.into_value(),
placeholder: Value::default(),
cache: None,
blink_state: BlinkState::default(),
selection: SelectionState::default(),
on_key: None,
mouse_buttons_down: 0,
needs_to_select_all: false,
line_navigation_x_target: None,
window_focused: false,
}
}
pub fn placeholder(mut self, placeholder: impl IntoValue<String>) -> Self {
self.placeholder = placeholder.into_value();
self
}
pub fn mask_symbol(mut self, symbol: impl IntoValue<CowString>) -> Self {
self.mask_symbol = symbol.into_value();
self
}
pub fn on_key<F>(mut self, on_key: F) -> Self
where
F: FnMut(KeyEvent) -> EventHandling + Send + 'static,
{
self.on_key = Some(Callback::new(on_key));
self
}
fn select_all(&mut self) {
self.value.map_ref(|value| {
let text = value.as_str();
self.selection.start = Some(Cursor::default());
self.selection.cursor.offset = text.len();
self.selection.cursor.affinity = Affinity::After;
});
}
fn forward_delete(&mut self, context: &mut EventContext<'_>) {
if !context.enabled() {
return;
}
let (cursor, selection) = self.selected_range();
if let Some(selection) = selection {
self.replace_range(cursor, selection, "");
} else {
let mut value = self.value.lock();
if let Some(length) = value.as_str()[cursor.offset..]
.graphemes(true)
.next()
.map(str::len)
{
value
.as_string_mut()
.replace_range(cursor.offset..cursor.offset + length, "");
}
}
}
fn replace_range(&mut self, start: Cursor, end: Cursor, new_text: &str) {
self.value.map_mut(|mut value| {
let value = value.as_string_mut();
let start = start.offset.min(value.len().saturating_sub(1));
let end = end.offset.min(value.len());
value.replace_range(start..end, new_text);
self.selection.cursor.offset = start + new_text.len();
self.selection.start = None;
});
}
fn delete(&mut self, context: &mut EventContext<'_>) {
if !context.enabled() {
return;
}
let (cursor, selection) = self.selected_range();
if let Some(selection) = selection {
self.replace_range(cursor, selection, "");
} else if cursor.offset > 0 {
let mut value = self.value.lock();
let length = value.as_str().len();
if length == 0 || cursor.offset == 0 {
return;
}
if let Ok(Some(offset)) = GraphemeCursor::new(cursor.offset, value.as_str().len(), true)
.prev_boundary(value.as_str(), 0)
{
value
.as_string_mut()
.replace_range(offset..cursor.offset, "");
self.selection.cursor.offset -= cursor.offset - offset;
}
}
}
fn move_cursor(
&mut self,
direction: Affinity,
mode: CursorNavigationMode,
context: &mut EventContext<'_>,
) {
if !matches!(mode, CursorNavigationMode::Line) {
self.line_navigation_x_target = None;
}
self.selection.cursor.affinity = Affinity::Before;
match mode {
CursorNavigationMode::Grapheme => self.move_cursor_by_grapheme(direction),
CursorNavigationMode::Word => self.move_cursor_by_word(direction),
CursorNavigationMode::Line => self.move_cursor_by_line(direction, context),
CursorNavigationMode::LineExtent => self.move_cursor_by_line_extent(direction, context),
}
}
fn move_cursor_by_grapheme(&mut self, affinity: Affinity) {
let value = self.value.lock();
let length = value.as_str().len();
match affinity {
Affinity::Before => {
if let Some((_, grapheme)) =
value
.as_str()
.grapheme_indices(true)
.find(|(index, grapheme)| {
index + grapheme.len() == self.selection.cursor.offset
})
{
self.selection.cursor.offset -= grapheme.len();
} else {
self.selection.cursor.offset = 0;
}
}
Affinity::After => {
if self.selection.cursor.offset < length {
if let Some(grapheme) = value.as_str()[self.selection.cursor.offset..]
.graphemes(true)
.next()
{
self.selection.cursor.offset += grapheme.len();
} else {
self.selection.cursor.offset = length;
}
}
}
}
}
fn move_cursor_by_word(&mut self, affinity: Affinity) {
let value = self.value.lock();
let length = value.as_str().len();
match affinity {
Affinity::Before => {
let mut words = value.as_str().unicode_word_indices().peekable();
while let Some((index, _)) = words.next() {
let next_starts_after_selection = words
.peek()
.map_or(true, |(index, _)| *index >= self.selection.cursor.offset);
if next_starts_after_selection {
self.selection.cursor.offset = index;
return;
}
}
self.selection.cursor.offset = 0;
}
Affinity::After => {
if self.selection.cursor.offset < length {
if let Some((index, word)) = value.as_str()[self.selection.cursor.offset..]
.unicode_word_indices()
.next()
{
self.selection.cursor.offset += index + word.len();
} else {
self.selection.cursor.offset = length;
}
}
}
}
}
fn move_cursor_by_line_extent(&mut self, affinity: Affinity, context: &mut EventContext<'_>) {
let Some(cache) = self.cache.as_ref() else {
return;
};
let (mut position, _) = self.point_from_cursor(cache, self.selection.cursor, cache.bytes);
position.y += context
.get(&IntrinsicPadding)
.into_px(context.kludgine.scale())
.round();
match affinity {
Affinity::Before => position.x = Px::ZERO,
Affinity::After => {
position.x = context.last_layout().map_or(Px::MAX, |r| r.size.width);
}
};
self.selection.cursor = self.cursor_from_point(position, context);
}
fn move_cursor_by_line(&mut self, affinity: Affinity, context: &mut EventContext<'_>) {
let Some(cache) = self.cache.as_ref() else {
return;
};
let (mut position, _) = self.point_from_cursor(cache, self.selection.cursor, cache.bytes);
position += Point::squared(
context
.get(&IntrinsicPadding)
.into_px(context.kludgine.scale())
.round(),
);
if let Some(target_x) = self.line_navigation_x_target {
position.x = target_x;
} else {
self.line_navigation_x_target = Some(position.x);
}
match affinity {
Affinity::Before => position.y -= cache.measured.line_height,
Affinity::After => {
position.y += cache.measured.line_height;
}
};
self.selection.cursor = self.cursor_from_point(position, context);
}
fn constrain_selection(&mut self) {
let length = self.value.map_ref(|s| s.as_str().len());
self.selection.cursor.offset = self.selection.cursor.offset.min(length);
if let Some(start) = &mut self.selection.start {
start.offset = start.offset.min(length);
}
}
fn selected_range(&mut self) -> (Cursor, Option<Cursor>) {
self.constrain_selection();
match self.selection.start {
Some(start) => match start.offset.cmp(&self.selection.cursor.offset) {
Ordering::Less => (start, Some(self.selection.cursor)),
Ordering::Equal => {
if self.mouse_buttons_down == 0 {
self.selection.start = None;
}
(self.selection.cursor, None)
}
Ordering::Greater => (self.selection.cursor, Some(start)),
},
None => (self.selection.cursor, None),
}
}
fn map_selected_text<R>(&mut self, map: impl FnOnce(&str) -> R) -> Option<R> {
let (cursor, Some(end)) = self.selected_range() else {
return None;
};
Some(
self.value
.map_ref(|value| map(&value.as_str()[cursor.offset..end.offset])),
)
}
fn is_masked(&self) -> bool {
self.mask_symbol.map(|mask| !mask.is_empty())
}
fn copy_selection_to_clipboard(&mut self, context: &mut EventContext<'_>) {
if self.is_masked() {
return;
}
self.map_selected_text(|text| {
if let Some(mut clipboard) = context.cushy().clipboard_guard() {
match clipboard.set_text(text) {
Ok(()) => {}
Err(err) => tracing::error!("error copying to clipboard: {err}"),
}
}
});
}
fn replace_selection(&mut self, new_text: &str, context: &mut EventContext<'_>) {
if !context.enabled() {
return;
}
let selected_range = self.selected_range();
match selected_range {
(start, Some(end)) => {
self.replace_range(start, end, new_text);
}
(cursor, None) => {
let mut value = self.value.lock();
if cursor.offset < value.as_str().len() {
value.as_string_mut().insert_str(cursor.offset, new_text);
self.selection.cursor.offset += new_text.len();
} else {
value.as_string_mut().push_str(new_text);
self.selection.cursor.offset += new_text.len();
}
}
};
}
fn paste_from_clipboard(&mut self, context: &mut EventContext<'_>) -> bool {
if !context.enabled() {
return false;
}
match context
.cushy()
.clipboard_guard()
.map(|mut clipboard| clipboard.get_text())
{
Some(Ok(text)) => {
self.replace_selection(&text, context);
true
}
None | Some(Err(arboard::Error::ConversionFailure)) => false,
Some(Err(err)) => {
tracing::error!("error retrieving clipboard contents: {err}");
false
}
}
}
fn handle_key(&mut self, input: KeyEvent, context: &mut EventContext<'_>) -> EventHandling {
match (input.state, input.logical_key, input.text.as_deref()) {
(ElementState::Pressed, Key::Named(key @ (NamedKey::Backspace| NamedKey::Delete)), _) => {
match key {
NamedKey::Backspace => self.delete(context),
NamedKey::Delete => self.forward_delete(context),
_ => unreachable!("previously matched"),
}
HANDLED
}
(ElementState::Pressed, Key::Named(key @ (NamedKey::ArrowLeft | NamedKey::ArrowDown | NamedKey::ArrowUp | NamedKey::ArrowRight | NamedKey::Home | NamedKey::End)), _) => {
let modifiers = context.modifiers();
let affinity = if matches!(key, NamedKey::ArrowLeft | NamedKey::ArrowUp | NamedKey::Home) {
Affinity::Before
} else {
Affinity::After
};
match (self.selection.start, modifiers.state().shift_key()) {
(None, true) => {
self.selection.start = Some(self.selection.cursor);
}
(Some(start), false) => {
self.selection.cursor = if affinity == Affinity::Before {
start.min(self.selection.cursor)
} else {
start.max(self.selection.cursor)
};
self.selection.start = None;
}
_ => {}
};
match key {
#[cfg(any(target_os = "ios", target_os = "macos"))]
NamedKey::ArrowLeft | NamedKey::ArrowRight if modifiers.primary() => self.move_cursor(affinity, CursorNavigationMode::LineExtent, context),
#[cfg(not(any(target_os = "ios", target_os = "macos")))]
NamedKey::Home | NamedKey::End => self.move_cursor(affinity, CursorNavigationMode::LineExtent, context),
NamedKey::ArrowLeft | NamedKey::ArrowRight if modifiers.word_select() => self.move_cursor(affinity, CursorNavigationMode::Word, context),
NamedKey::ArrowLeft | NamedKey::ArrowRight => self.move_cursor(affinity, CursorNavigationMode::Grapheme, context),
NamedKey::ArrowDown | NamedKey::ArrowUp => self.move_cursor(affinity, CursorNavigationMode::Line, context),
_ => tracing::warn!("unhandled key: {key:?}"),
}
HANDLED
}
(state, _, Some("a")) if context.modifiers().primary() => {
if state.is_pressed() {
self.select_all();
}
HANDLED
}
(state, _, Some("c")) if context.modifiers().primary() => {
if state.is_pressed() {
self.copy_selection_to_clipboard(context);
}
HANDLED
}
(state, _, Some("v")) if context.modifiers().primary() => {
if state.is_pressed() {
self.paste_from_clipboard(context);
}
HANDLED
}
(state, _, Some(text))
if !context.modifiers().primary()
&& text != "\t" && text != "\r" && text != "\u{1b}" =>
{
if state.is_pressed() {
self.replace_selection(text, context);
}
HANDLED
}
(_, _, _) => IGNORED,
}
}
fn layout_text(&mut self, width: Option<Px>, context: &mut GraphicsContext<'_, '_, '_, '_>) {
context.invalidate_when_changed(&self.value);
let mut key = {
let (cursor, selection) = self.selected_range();
CacheKey {
generation: self.value.generation(),
mask_generation: self.mask_symbol.generation(),
placeholder_generation: self.placeholder.generation(),
width,
color: context.get(&TextColor),
mask_bytes: self
.mask_symbol
.map(|sym| sym.graphemes(true).next().map_or(0, str::len)),
cursor,
selection,
}
};
match &mut self.cache {
Some(cache)
if cache.measured.can_render_to(&context.gfx)
&& cache.placeholder.can_render_to(&context.gfx)
&& cache.key == key => {}
_ => {
let (bytes, measured, placeholder, ) = self.value.map_ref(|storage| {
let mut text = storage.as_str();
let mut bytes = text.len();
self.mask_symbol.map(|mask_symbol| {
if let Some(first_grapheme) = mask_symbol.graphemes(true).next() {
if mask_symbol != first_grapheme {
static WARNING: OnceLock<()> = OnceLock::new();
WARNING.get_or_init(|| tracing::warn!("Mask symbol {mask_symbol} as more than one grapheme. Only the first grapheme will be used."));
}
key.mask_bytes = first_grapheme.len();
let char_count = text.graphemes(true).count();
bytes = key.mask_bytes * char_count;
self.mask.truncate(bytes);
while self.mask.len() < bytes {
self.mask.push_str(first_grapheme);
}
text = &self.mask;
} else {
key.mask_bytes = 0;
}
});
context.apply_current_font_settings();
let mut text = Text::new(text, key.color);
if let Some(width) = width {
text = text.wrap_at(width);
}
let placeholder_color = context.theme().surface.on_color_variant;
let placeholder = self.placeholder.map(|placeholder| context.gfx.measure_text(Text::new(placeholder, placeholder_color)));
(bytes, context.gfx.measure_text(text), placeholder)
});
self.cache = Some(CachedLayout {
bytes,
measured,
placeholder,
key,
});
}
}
}
fn cache_info(&self) -> CacheInfo<'_> {
let cache = self
.cache
.as_ref()
.expect("always called after layout_text");
let masked = cache.key.mask_bytes > 0;
let mut cursor = cache.key.cursor;
let mut selection = cache.key.selection;
if masked {
self.value.map_ref(|value| {
let value = value.as_str();
assert!(cache.key.cursor.offset <= value.len());
cursor.offset =
value[..cache.key.cursor.offset].graphemes(true).count() * cache.key.mask_bytes;
if let Some(selection) = &mut selection {
assert!(selection.offset <= value.len());
selection.offset =
value[..selection.offset].graphemes(true).count() * cache.key.mask_bytes;
}
});
}
CacheInfo {
cache,
masked,
cursor,
selection,
}
}
#[allow(clippy::too_many_lines)] fn point_from_cursor(
&self,
cache: &CachedLayout,
cursor: Cursor,
total_bytes: usize,
) -> (Point<Px>, Px) {
if cache.measured.glyphs.is_empty()
|| (cursor.offset == 0 && cursor.affinity == Affinity::Before)
{
return (Point::default(), Px::ZERO);
}
let mut closest_before_index = 0;
let mut closest_after_index = usize::MAX;
let mut bottom_right_index = 0;
let mut bottom_right_line = 0;
let mut bottom_right_rect = Rect::default();
let mut unrendered_offset = 0;
for (index, glyph) in cache.measured.glyphs.iter().enumerate() {
unrendered_offset = unrendered_offset.max(glyph.info.end);
let rect = glyph.rect();
if bottom_right_rect.size.width == 0
|| glyph.info.line > bottom_right_line
|| (glyph.info.line == bottom_right_line
&& rect.origin.x > bottom_right_rect.origin.x)
{
bottom_right_line = glyph.info.line;
bottom_right_index = index;
bottom_right_rect = rect;
}
match (
glyph.info.start.cmp(&cursor.offset),
cursor.offset.cmp(&glyph.info.end),
) {
(Ordering::Less | Ordering::Equal, Ordering::Less) => {
let mut grapheme_offset = Px::ZERO;
if glyph.info.start < cursor.offset {
let clustered_bytes = glyph.info.end - glyph.info.start;
if clustered_bytes > 1 {
let clustered_graphemes = if cache.key.mask_bytes > 0 {
self.mask[glyph.info.start..glyph.info.end]
.graphemes(true)
.count()
} else {
self.value.map_ref(|value| {
value.as_str()[glyph.info.start..glyph.info.end]
.graphemes(true)
.count()
})
};
if clustered_graphemes > 1 {
let cursor_offset = cursor.offset - glyph.info.start;
grapheme_offset = rect.size.width * cursor_offset.cast::<f32>()
/ clustered_graphemes.cast::<f32>();
}
}
}
return (
Point::new(
rect.origin.x + grapheme_offset,
cache.measured.line_height.saturating_mul(Px::new(
i32::try_from(glyph.info.line).unwrap_or(i32::MAX),
)),
),
rect.size.width,
);
}
(Ordering::Less, _) => {
closest_before_index = closest_before_index.max(index);
}
(_, Ordering::Less) => {
closest_after_index = closest_after_index.min(index);
}
_ => {}
}
}
if closest_after_index == usize::MAX {
let bottom_right = &cache.measured.glyphs[bottom_right_index];
let bottom_y = cache.measured.line_height.saturating_mul(Px::new(
i32::try_from(bottom_right.info.line).unwrap_or(i32::MAX),
));
let mut bottom_right_cursor = Point::new(
bottom_right_rect.origin.x + bottom_right_rect.size.width,
bottom_y,
);
let bytes_after_glyph = total_bytes - unrendered_offset;
if !(bottom_right.info.end == cursor.offset || bytes_after_glyph == 0) {
let space_past_glyph = bottom_right.info.line_width - bottom_right_cursor.x;
let space_per_byte =
space_past_glyph.into_float() / bytes_after_glyph.cast::<f32>();
let cursor_position = space_per_byte
* (cursor.offset.saturating_sub(unrendered_offset)).cast::<f32>();
bottom_right_cursor.x += Px::from(cursor_position);
}
(bottom_right_cursor, Px::ZERO)
} else {
let before = &cache.measured.glyphs[closest_before_index];
let after = &cache.measured.glyphs[closest_after_index];
let before_rect = before.rect();
let after_rect = after.rect();
let before_y = cache
.measured
.line_height
.saturating_mul(Px::new(i32::try_from(before.info.line).unwrap_or(i32::MAX)));
if before.info.line == after.info.line {
let before_right = before_rect.origin.x + before_rect.size.width;
let space_between = after_rect.origin.x - before_right;
let bytes_between = after.info.start - before.info.end;
let space_per_byte = space_between.into_float() / bytes_between.cast::<f32>();
let cursor_position =
space_per_byte * (cursor.offset - before.info.end).cast::<f32>();
(
Point::new(before_right + Px::from(cursor_position), before_y),
Px::from(space_per_byte),
)
} else {
match cursor.affinity {
Affinity::Before => {
let mut origin = before_rect.origin;
origin.x += before_rect.size.width;
(origin, before_y)
}
Affinity::After => (
Point::new(Px::ZERO, before_y + cache.measured.line_height),
Px::ZERO,
),
}
}
}
}
fn cursor_from_point(&mut self, location: Point<Px>, context: &mut EventContext<'_>) -> Cursor {
let mut cursor = self.cached_cursor_from_point(location, context);
if let Some(symbol) = self.mask.graphemes(true).next() {
let grapheme_offset = cursor.offset / symbol.len();
cursor.offset = self.value.map_ref(|value| {
value
.as_str()
.graphemes(true)
.take(grapheme_offset)
.map(str::len)
.sum::<usize>()
});
}
cursor
}
fn cached_cursor_from_point(
&mut self,
location: Point<Px>,
context: &mut EventContext<'_>,
) -> Cursor {
let Some(cache) = &self.cache else {
return Cursor::default();
};
let padding = context
.get(&IntrinsicPadding)
.into_px(context.kludgine.scale())
.round();
let mut location = location - padding;
if location.y < 0 {
return Cursor::default();
}
if location.x < 0 {
location.x = Px::ZERO;
}
let mut closest: Option<(Cursor, i32, usize, Point<Px>)> = None;
let mut current_line = usize::MAX;
let mut current_line_y = Px::ZERO;
for (index, glyph) in cache.measured.glyphs.iter().enumerate() {
if current_line != glyph.info.line {
current_line = glyph.info.line;
current_line_y = cache
.measured
.line_height
.saturating_mul(Px::new(i32::try_from(current_line).unwrap_or(i32::MAX)));
}
let mut rect = glyph.rect();
if !glyph.visible() {
rect.size.height = cache.measured.line_height;
}
let relative = location - Point::new(rect.origin.x, current_line_y);
if relative.x >= 0
&& relative.y >= 0
&& relative.x <= rect.size.width
&& relative.y <= cache.measured.line_height
{
return if relative.x > rect.size.width / 2 {
if glyph.info.end < cache.bytes {
Cursor {
offset: glyph.info.end,
affinity: Affinity::Before,
}
} else {
Cursor {
offset: glyph.info.start,
affinity: Affinity::After,
}
}
} else {
Cursor {
offset: glyph.info.start,
affinity: Affinity::Before,
}
};
}
let line_height = cache.measured.line_height.get();
if relative.y < 0 || relative.y >= line_height {
continue;
}
let xy = relative
.x
.get()
.saturating_mul(
((relative.y.get() + line_height - 1) / line_height * line_height)
.saturating_pow(2),
)
.saturating_abs();
let cursor = Cursor {
offset: if relative.x <= rect.size.width / 3 {
glyph.info.start
} else {
glyph.info.end
},
affinity: Affinity::Before,
};
match closest {
Some((_, closest_xy, ..)) if xy < closest_xy => {
closest = Some((cursor, xy, index, relative));
}
None => closest = Some((cursor, xy, index, relative)),
_ => {}
}
}
if let Some((closest, _, index, relative)) = closest {
if relative.x.abs() < cache.measured.line_height && index < cache.measured.glyphs.len()
{
return closest;
}
}
Cursor {
offset: cache.bytes,
affinity: Affinity::After,
}
}
}
struct CacheInfo<'a> {
cache: &'a CachedLayout,
masked: bool,
cursor: Cursor,
selection: Option<Cursor>,
}
#[derive(Debug, Clone, Copy)]
enum CursorNavigationMode {
Grapheme,
Word,
LineExtent,
Line,
}
impl<Storage> Debug for Input<Storage>
where
Storage: Debug,
{
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("Input")
.field("text", &self.value)
.field("mask_symbol", &self.mask_symbol)
.field("placeholder", &self.placeholder)
.finish_non_exhaustive()
}
}
impl<Storage> Widget for Input<Storage>
where
Storage: InputStorage + Debug,
{
fn hit_test(&mut self, _location: Point<Px>, _context: &mut EventContext<'_>) -> bool {
true
}
fn accept_focus(&mut self, _context: &mut EventContext<'_>) -> bool {
true
}
fn mouse_down(
&mut self,
location: Point<Px>,
_device_id: crate::window::DeviceId,
_button: kludgine::app::winit::event::MouseButton,
context: &mut EventContext<'_>,
) -> EventHandling {
self.mouse_buttons_down += 1;
context.focus();
self.needs_to_select_all = false;
self.selection.cursor = self.cursor_from_point(location, context);
self.selection.start = Some(self.selection.cursor);
context.set_needs_redraw();
HANDLED
}
fn hover(
&mut self,
_location: Point<Px>,
_context: &mut EventContext<'_>,
) -> Option<CursorIcon> {
Some(CursorIcon::Text)
}
fn mouse_drag(
&mut self,
location: Point<Px>,
_device_id: crate::window::DeviceId,
_button: kludgine::app::winit::event::MouseButton,
context: &mut EventContext<'_>,
) {
let cursor_location = self.cursor_from_point(location, context);
if self.selection.cursor != cursor_location {
self.selection.cursor = cursor_location;
context.set_needs_redraw();
}
self.blink_state.force_on();
}
fn mouse_up(
&mut self,
_location: Option<Point<Px>>,
_device_id: crate::window::DeviceId,
_button: kludgine::app::winit::event::MouseButton,
_context: &mut EventContext<'_>,
) {
self.mouse_buttons_down -= 1;
}
#[allow(clippy::too_many_lines)]
fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_>) {
if self.needs_to_select_all {
self.needs_to_select_all = false;
if self.selection.start.is_none() {
self.select_all();
}
}
self.blink_state.update(context.elapsed());
let window_focused = context.window().focused().get_tracking_redraw(context);
if window_focused != self.window_focused {
if window_focused {
self.blink_state.force_on();
}
self.window_focused = window_focused;
}
let cursor_state = self.blink_state;
let size = context.gfx.size();
let padding = context
.get(&IntrinsicPadding)
.into_px(context.gfx.scale())
.round();
let padding = Point::squared(padding);
self.layout_text(Some(size.width.into_signed()), context);
let info = self.cache_info();
let focused = context.focused(false);
let highlight = if focused && window_focused {
context.draw_focus_ring();
context.get(&HighlightColor)
} else {
let outline_color = context.get(&OutlineColor);
context.stroke_outline::<Lp>(outline_color, StrokeOptions::default());
outline_color
};
if focused {
context.set_ime_allowed(true);
context.set_ime_location(context.gfx.region());
context.set_ime_purpose(if info.masked {
ImePurpose::Password
} else {
ImePurpose::Normal
});
}
if let Some(selection) = info.selection {
let (start, end) = if selection < info.cursor {
(selection, info.cursor)
} else {
(info.cursor, selection)
};
let (start_position, _) = self.point_from_cursor(info.cache, start, info.cache.bytes);
let (end_position, end_width) =
self.point_from_cursor(info.cache, end, info.cache.bytes);
if start_position.y == end_position.y {
let width = end_position.x - start_position.x;
context.gfx.draw_shape(
Shape::filled_rect(
Rect::new(
start_position,
Size::new(width, info.cache.measured.line_height),
),
highlight,
)
.translate_by(padding),
);
} else {
let width = size.width.into_signed() - start_position.x;
context.gfx.draw_shape(
Shape::filled_rect(
Rect::new(
start_position,
Size::new(width, info.cache.measured.line_height),
),
highlight,
)
.translate_by(padding),
);
let bottom_of_first_line = start_position.y + info.cache.measured.line_height;
let distance_between = end_position.y - bottom_of_first_line;
if distance_between > 0 {
context.gfx.draw_shape(
Shape::filled_rect(
Rect::new(
Point::new(Px::ZERO, bottom_of_first_line),
Size::new(size.width.into_signed(), distance_between),
),
highlight,
)
.translate_by(padding),
);
}
context.gfx.draw_shape(
Shape::filled_rect(
Rect::new(
Point::new(Px::ZERO, end_position.y),
Size::new(end_position.x + end_width, info.cache.measured.line_height),
),
highlight,
)
.translate_by(padding),
);
}
} else if focused && window_focused && context.enabled() {
let (location, _) = self.point_from_cursor(info.cache, info.cursor, info.cache.bytes);
if cursor_state.visible {
let cursor_width = Lp::points(2).into_px(context.gfx.scale());
context.gfx.draw_shape(
Shape::filled_rect(
Rect::new(
Point::new(location.x - cursor_width / 2, location.y),
Size::new(cursor_width, info.cache.measured.line_height),
),
highlight,
)
.translate_by(padding),
);
}
context.redraw_in(cursor_state.remaining_until_blink);
}
let text = if info.cache.bytes > 0 {
&info.cache.measured
} else {
&info.cache.placeholder
};
context
.gfx
.draw_measured_text(text.translate_by(padding), TextOrigin::TopLeft);
}
fn layout(
&mut self,
available_space: Size<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_>,
) -> Size<UPx> {
let padding = context
.get(&IntrinsicPadding)
.into_upx(context.gfx.scale())
.round();
let width = available_space.width.max().saturating_sub(padding * 2);
self.layout_text(Some(width.into_signed()), &mut context.graphics);
let info = self.cache_info();
info.cache
.measured
.size
.max(info.cache.placeholder.size)
.into_unsigned()
+ Size::squared(padding * 2)
}
fn keyboard_input(
&mut self,
_device_id: crate::window::DeviceId,
input: KeyEvent,
_is_synthetic: bool,
context: &mut EventContext<'_>,
) -> EventHandling {
if let Some(on_key) = &mut self.on_key {
on_key.invoke(input.clone())?;
}
let handled = self.handle_key(input, context);
if handled.is_break() {
context.set_needs_redraw();
}
self.blink_state.force_on();
handled
}
fn ime(&mut self, ime: Ime, context: &mut EventContext<'_>) -> EventHandling {
match ime {
Ime::Enabled | Ime::Disabled => {}
Ime::Preedit(text, cursor) => {
tracing::warn!("TODO: preview IME input {text}, cursor: {cursor:?}");
}
Ime::Commit(text) => {
self.replace_selection(&text, context);
context.set_needs_redraw();
}
}
HANDLED
}
fn focus(&mut self, context: &mut EventContext<'_>) {
if self.mouse_buttons_down == 0 {
self.needs_to_select_all = true;
}
context.set_ime_allowed(true);
context.set_ime_purpose(if self.is_masked() {
ImePurpose::Password
} else {
ImePurpose::Normal
});
context.set_needs_redraw();
}
fn blur(&mut self, context: &mut EventContext<'_>) {
context.set_ime_allowed(false);
context.set_needs_redraw();
}
}
#[derive(Clone, Copy)]
struct BlinkState {
visible: bool,
remaining_until_blink: Duration,
}
impl Default for BlinkState {
fn default() -> Self {
Self {
visible: true,
remaining_until_blink: CURSOR_BLINK_DURATION,
}
}
}
impl BlinkState {
pub fn update(&mut self, elapsed: Duration) {
let total_cycles = elapsed.as_nanos() / CURSOR_BLINK_DURATION.as_nanos();
let remaining = Duration::from_nanos(
u64::try_from(elapsed.as_nanos() % CURSOR_BLINK_DURATION.as_nanos())
.expect("remainder fits in u64"),
);
if total_cycles & 1 == 1 {
self.visible = !self.visible;
}
if let Some(remaining) = self.remaining_until_blink.checked_sub(remaining) {
self.remaining_until_blink = remaining;
} else {
self.visible = !self.visible;
self.remaining_until_blink =
CURSOR_BLINK_DURATION - (remaining - self.remaining_until_blink);
}
}
pub fn force_on(&mut self) {
self.visible = true;
self.remaining_until_blink = CURSOR_BLINK_DURATION;
}
}
pub trait InputStorage: Send + 'static {
const MASKED: bool;
fn as_str(&self) -> &str;
fn as_string_mut(&mut self) -> &mut String;
}
impl InputStorage for String {
const MASKED: bool = false;
fn as_str(&self) -> &str {
self.borrow()
}
fn as_string_mut(&mut self) -> &mut String {
self.borrow_mut()
}
}
impl InputStorage for Cow<'static, str> {
const MASKED: bool = false;
fn as_str(&self) -> &str {
self.borrow()
}
fn as_string_mut(&mut self) -> &mut String {
self.to_mut()
}
}
pub trait InputValue<Storage>: IntoDynamic<Storage> + Sized
where
Storage: InputStorage,
{
fn into_input(self) -> Input<Storage> {
Input::new(self.into_dynamic())
}
fn to_input(&self) -> Input<Storage>
where
Self: Clone,
{
self.clone().into_input()
}
}
impl<T> InputValue<String> for T where T: IntoDynamic<String> {}
impl<T> InputValue<Cow<'static, str>> for T where T: IntoDynamic<Cow<'static, str>> {}
#[derive(Eq, Clone, Hash, Ord)]
pub struct CowString(Arc<String>);
impl CowString {
pub fn new(str: impl Into<String>) -> Self {
Self(Arc::new(str.into()))
}
}
impl Debug for CowString {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Debug::fmt(self.as_str(), f)
}
}
impl Display for CowString {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(self.as_str(), f)
}
}
impl<T> PartialOrd<T> for CowString
where
T: PartialOrd<str> + ?Sized,
{
fn partial_cmp(&self, other: &T) -> Option<Ordering> {
other.partial_cmp(self.as_str()).map(Ordering::reverse)
}
}
impl From<CowString> for String {
fn from(s: CowString) -> Self {
match Arc::try_unwrap(s.0) {
Ok(s) => s,
Err(arc) => (*arc).clone(),
}
}
}
#[derive(Eq, Clone)]
pub struct MaskedString(Arc<Zeroizing<String>>);
impl MaskedString {
pub fn new(str: impl Into<String>) -> Self {
Self(Arc::new(Zeroizing::new(str.into())))
}
}
impl Debug for MaskedString {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if f.alternate() {
f.write_str("MaskedString(")?;
for _ in 0..self.as_str().len() {
f.write_char('*')?;
}
f.write_char(')')
} else {
f.debug_struct("MaskedString").finish_non_exhaustive()
}
}
}
macro_rules! impl_cow_string {
($type:ident, $masked:literal) => {
impl Deref for $type {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for $type {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut *Arc::make_mut(&mut self.0)
}
}
impl Default for $type {
fn default() -> Self {
static EMPTY: Lazy<$type> = Lazy::new(|| $type(Arc::default()));
EMPTY.clone()
}
}
impl From<char> for $type {
fn from(s: char) -> Self {
Self::new(s)
}
}
impl IntoValue<$type> for char {
fn into_value(self) -> Value<$type> {
Value::Constant(<$type>::from(self))
}
}
impl From<String> for $type {
fn from(s: String) -> Self {
Self::new(s)
}
}
impl IntoValue<$type> for String {
fn into_value(self) -> Value<$type> {
Value::Constant(<$type>::from(self))
}
}
impl<'a> From<&'a str> for $type {
fn from(s: &'a str) -> Self {
Self::new(s)
}
}
impl IntoValue<$type> for &str {
fn into_value(self) -> Value<$type> {
Value::Constant(<$type>::from(self))
}
}
impl IntoValue<$type> for Dynamic<String> {
fn into_value(self) -> Value<$type> {
Value::Dynamic(self.map_each_to())
}
}
impl IntoValue<$type> for Dynamic<&'static str> {
fn into_value(self) -> Value<$type> {
Value::Dynamic(self.map_each(|s| <$type>::from(*s)))
}
}
impl<'a> From<&'a String> for $type {
fn from(s: &'a String) -> Self {
Self::new(s.as_str())
}
}
impl<T> PartialEq<T> for $type
where
T: PartialEq<str> + ?Sized,
{
fn eq(&self, other: &T) -> bool {
other == self.as_str()
}
}
impl InputStorage for $type {
const MASKED: bool = $masked;
fn as_str(&self) -> &str {
&**self
}
fn as_string_mut(&mut self) -> &mut String {
&mut *Arc::make_mut(&mut self.0)
}
}
impl<T> InputValue<$type> for T where T: IntoDynamic<$type> {}
#[cfg(feature = "serde")]
impl serde::Serialize for $type {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for $type {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
String::deserialize(deserializer).map(Self::from)
}
}
};
}
impl_cow_string!(CowString, false);
impl_cow_string!(MaskedString, true);