Text input (#143)
* update: iced 0.10.0 * wip: text input * wip: text inputs with icons and buttons * wip: improve text input * refactor: text input styling * chore: add scale factor * chore(text_input): add winit example and do some cleanup
This commit is contained in:
parent
fcdefcd8fb
commit
db8e791b87
9 changed files with 3017 additions and 4 deletions
|
|
@ -13,9 +13,10 @@ use cosmic::{
|
|||
iced_widget::text,
|
||||
theme::{self, Theme},
|
||||
widget::{
|
||||
button, cosmic_container, header_bar, nav_bar, nav_bar_toggle,
|
||||
button, cosmic_container, header_bar, icon, inline_input, nav_bar, nav_bar_toggle,
|
||||
rectangle_tracker::{rectangle_tracker_subscription, RectangleTracker, RectangleUpdate},
|
||||
scrollable, segmented_button, segmented_selection, settings, IconSource,
|
||||
scrollable, search_input, secure_input, segmented_button, segmented_selection, settings,
|
||||
text_input, IconSource,
|
||||
},
|
||||
Element, ElementExt,
|
||||
};
|
||||
|
|
@ -127,6 +128,8 @@ pub struct Window {
|
|||
rectangle_tracker: Option<RectangleTracker<u32>>,
|
||||
pub selection: segmented_button::SingleSelectModel,
|
||||
timeline: Timeline,
|
||||
input_value: String,
|
||||
secure_input_visible: bool,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
|
|
@ -183,12 +186,13 @@ pub enum Message {
|
|||
Drag,
|
||||
Minimize,
|
||||
Maximize,
|
||||
InputChanged,
|
||||
Rectangle(RectangleUpdate<u32>),
|
||||
NavBar(segmented_button::Entity),
|
||||
Ignore,
|
||||
Selection(segmented_button::Entity),
|
||||
Tick(Instant),
|
||||
InputChanged(String),
|
||||
ToggleVisible,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
|
|
@ -305,7 +309,6 @@ impl Application for Window {
|
|||
Message::Minimize => return set_mode_window(window::Id(0), window::Mode::Hidden),
|
||||
Message::Maximize => return toggle_maximize(window::Id(0)),
|
||||
Message::RowSelected(row) => println!("Selected row {row}"),
|
||||
Message::InputChanged => {}
|
||||
Message::Rectangle(r) => match r {
|
||||
RectangleUpdate::Rectangle(_) => {}
|
||||
RectangleUpdate::Init(t) => {
|
||||
|
|
@ -315,6 +318,12 @@ impl Application for Window {
|
|||
Message::Ignore => {}
|
||||
Message::Selection(key) => self.selection.activate(key),
|
||||
Message::Tick(now) => self.timeline.now(now),
|
||||
Message::InputChanged(v) => {
|
||||
self.input_value = v;
|
||||
}
|
||||
Message::ToggleVisible => {
|
||||
self.secure_input_visible = !self.secure_input_visible;
|
||||
}
|
||||
}
|
||||
|
||||
Command::none()
|
||||
|
|
@ -476,6 +485,41 @@ impl Application for Window {
|
|||
.padding(16)
|
||||
.style(cosmic::theme::Container::Secondary),
|
||||
))
|
||||
.add(settings::item(
|
||||
"Text Input",
|
||||
text_input("test", &self.input_value)
|
||||
.width(Length::Fill)
|
||||
.on_input(Message::InputChanged),
|
||||
))
|
||||
.add(settings::item(
|
||||
"Text Input",
|
||||
secure_input(
|
||||
"test",
|
||||
&self.input_value,
|
||||
Some(Message::ToggleVisible),
|
||||
!self.secure_input_visible,
|
||||
)
|
||||
.label("Test Secure Input Label")
|
||||
.helper_text("password")
|
||||
.width(Length::Fill)
|
||||
.on_input(Message::InputChanged),
|
||||
))
|
||||
.add(settings::item(
|
||||
"Text Input",
|
||||
search_input(
|
||||
"search for stuff",
|
||||
&self.input_value,
|
||||
Some(Message::InputChanged("".to_string())),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.on_input(Message::InputChanged),
|
||||
))
|
||||
.add(settings::item(
|
||||
"Text Input",
|
||||
inline_input(&self.input_value)
|
||||
.width(Length::Fill)
|
||||
.on_input(Message::InputChanged),
|
||||
))
|
||||
.into(),
|
||||
])
|
||||
.into();
|
||||
|
|
|
|||
|
|
@ -504,6 +504,10 @@ impl State {
|
|||
.size(20)
|
||||
.id(INPUT_ID.clone())
|
||||
.into(),
|
||||
cosmic::widget::text_input("test", &self.entry_value)
|
||||
.width(Length::Fill)
|
||||
.on_input(Message::InputChanged)
|
||||
.into(),
|
||||
])
|
||||
.into()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,10 @@ pub use warning::*;
|
|||
|
||||
pub mod cosmic_container;
|
||||
pub use cosmic_container::*;
|
||||
// #[cfg(feature = "wayland")]
|
||||
pub mod text_input;
|
||||
// #[cfg(feature = "wayland")]
|
||||
pub use text_input::*;
|
||||
|
||||
/// An element to distinguish a boundary between two elements.
|
||||
pub mod divider {
|
||||
|
|
|
|||
173
src/widget/text_input/cursor.rs
Normal file
173
src/widget/text_input/cursor.rs
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
//! Track the cursor of a text input.
|
||||
use super::value::Value;
|
||||
|
||||
/// The cursor of a text input.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct Cursor {
|
||||
state: State,
|
||||
}
|
||||
|
||||
/// The state of a [`Cursor`].
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum State {
|
||||
/// Cursor without a selection
|
||||
Index(usize),
|
||||
|
||||
/// Cursor selecting a range of text
|
||||
Selection {
|
||||
/// The start of the selection
|
||||
start: usize,
|
||||
/// The end of the selection
|
||||
end: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for Cursor {
|
||||
fn default() -> Self {
|
||||
Cursor {
|
||||
state: State::Index(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Cursor {
|
||||
/// Returns the [`State`] of the [`Cursor`].
|
||||
#[must_use]
|
||||
pub fn state(&self, value: &Value) -> State {
|
||||
match self.state {
|
||||
State::Index(index) => State::Index(index.min(value.len())),
|
||||
State::Selection { start, end } => {
|
||||
let start = start.min(value.len());
|
||||
let end = end.min(value.len());
|
||||
|
||||
if start == end {
|
||||
State::Index(start)
|
||||
} else {
|
||||
State::Selection { start, end }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current selection of the [`Cursor`] for the given [`Value`].
|
||||
///
|
||||
/// `start` is guaranteed to be <= than `end`.
|
||||
#[must_use]
|
||||
pub fn selection(&self, value: &Value) -> Option<(usize, usize)> {
|
||||
match self.state(value) {
|
||||
State::Selection { start, end } => Some((start.min(end), start.max(end))),
|
||||
State::Index(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn move_to(&mut self, position: usize) {
|
||||
self.state = State::Index(position);
|
||||
}
|
||||
|
||||
pub(crate) fn move_right(&mut self, value: &Value) {
|
||||
self.move_right_by_amount(value, 1);
|
||||
}
|
||||
|
||||
pub(crate) fn move_right_by_words(&mut self, value: &Value) {
|
||||
self.move_to(value.next_end_of_word(self.right(value)));
|
||||
}
|
||||
|
||||
pub(crate) fn move_right_by_amount(&mut self, value: &Value, amount: usize) {
|
||||
match self.state(value) {
|
||||
State::Index(index) => self.move_to(index.saturating_add(amount).min(value.len())),
|
||||
State::Selection { start, end } => self.move_to(end.max(start)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn move_left(&mut self, value: &Value) {
|
||||
match self.state(value) {
|
||||
State::Index(index) if index > 0 => self.move_to(index - 1),
|
||||
State::Selection { start, end } => self.move_to(start.min(end)),
|
||||
State::Index(_) => self.move_to(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn move_left_by_words(&mut self, value: &Value) {
|
||||
self.move_to(value.previous_start_of_word(self.left(value)));
|
||||
}
|
||||
|
||||
pub(crate) fn select_range(&mut self, start: usize, end: usize) {
|
||||
if start == end {
|
||||
self.state = State::Index(start);
|
||||
} else {
|
||||
self.state = State::Selection { start, end };
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn select_left(&mut self, value: &Value) {
|
||||
match self.state(value) {
|
||||
State::Index(index) if index > 0 => self.select_range(index, index - 1),
|
||||
State::Selection { start, end } if end > 0 => self.select_range(start, end - 1),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn select_right(&mut self, value: &Value) {
|
||||
match self.state(value) {
|
||||
State::Index(index) if index < value.len() => self.select_range(index, index + 1),
|
||||
State::Selection { start, end } if end < value.len() => {
|
||||
self.select_range(start, end + 1);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn select_left_by_words(&mut self, value: &Value) {
|
||||
match self.state(value) {
|
||||
State::Index(index) => self.select_range(index, value.previous_start_of_word(index)),
|
||||
State::Selection { start, end } => {
|
||||
self.select_range(start, value.previous_start_of_word(end));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn select_right_by_words(&mut self, value: &Value) {
|
||||
match self.state(value) {
|
||||
State::Index(index) => self.select_range(index, value.next_end_of_word(index)),
|
||||
State::Selection { start, end } => {
|
||||
self.select_range(start, value.next_end_of_word(end));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn select_all(&mut self, value: &Value) {
|
||||
self.select_range(0, value.len());
|
||||
}
|
||||
|
||||
pub(crate) fn start(&self, value: &Value) -> usize {
|
||||
let start = match self.state {
|
||||
State::Index(index) => index,
|
||||
State::Selection { start, .. } => start,
|
||||
};
|
||||
|
||||
start.min(value.len())
|
||||
}
|
||||
|
||||
pub(crate) fn end(&self, value: &Value) -> usize {
|
||||
let end = match self.state {
|
||||
State::Index(index) => index,
|
||||
State::Selection { end, .. } => end,
|
||||
};
|
||||
|
||||
end.min(value.len())
|
||||
}
|
||||
|
||||
fn left(&self, value: &Value) -> usize {
|
||||
match self.state(value) {
|
||||
State::Index(index) => index,
|
||||
State::Selection { start, end } => start.min(end),
|
||||
}
|
||||
}
|
||||
|
||||
fn right(&self, value: &Value) -> usize {
|
||||
match self.state(value) {
|
||||
State::Index(index) => index,
|
||||
State::Selection { start, end } => start.max(end),
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/widget/text_input/editor.rs
Normal file
65
src/widget/text_input/editor.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
use super::{cursor::Cursor, value::Value};
|
||||
|
||||
pub struct Editor<'a> {
|
||||
value: &'a mut Value,
|
||||
cursor: &'a mut Cursor,
|
||||
}
|
||||
|
||||
impl<'a> Editor<'a> {
|
||||
pub fn new(value: &'a mut Value, cursor: &'a mut Cursor) -> Editor<'a> {
|
||||
Editor { value, cursor }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn contents(&self) -> String {
|
||||
self.value.to_string()
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, character: char) {
|
||||
if let Some((left, right)) = self.cursor.selection(self.value) {
|
||||
self.cursor.move_left(self.value);
|
||||
self.value.remove_many(left, right);
|
||||
}
|
||||
|
||||
self.value.insert(self.cursor.end(self.value), character);
|
||||
self.cursor.move_right(self.value);
|
||||
}
|
||||
|
||||
pub fn paste(&mut self, content: Value) {
|
||||
let length = content.len();
|
||||
if let Some((left, right)) = self.cursor.selection(self.value) {
|
||||
self.cursor.move_left(self.value);
|
||||
self.value.remove_many(left, right);
|
||||
}
|
||||
|
||||
self.value.insert_many(self.cursor.end(self.value), content);
|
||||
|
||||
self.cursor.move_right_by_amount(self.value, length);
|
||||
}
|
||||
|
||||
pub fn backspace(&mut self) {
|
||||
if let Some((start, end)) = self.cursor.selection(self.value) {
|
||||
self.cursor.move_left(self.value);
|
||||
self.value.remove_many(start, end);
|
||||
} else {
|
||||
let start = self.cursor.start(self.value);
|
||||
|
||||
if start > 0 {
|
||||
self.cursor.move_left(self.value);
|
||||
self.value.remove(start - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(&mut self) {
|
||||
if self.cursor.selection(self.value).is_some() {
|
||||
self.backspace();
|
||||
} else {
|
||||
let end = self.cursor.end(self.value);
|
||||
|
||||
if end < self.value.len() {
|
||||
self.value.remove(end);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2319
src/widget/text_input/input.rs
Normal file
2319
src/widget/text_input/input.rs
Normal file
File diff suppressed because it is too large
Load diff
10
src/widget/text_input/mod.rs
Normal file
10
src/widget/text_input/mod.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
//! A text input widget from iced widgets plus some added details.
|
||||
|
||||
pub mod cursor;
|
||||
pub mod editor;
|
||||
mod input;
|
||||
mod style;
|
||||
pub mod value;
|
||||
|
||||
pub use input::*;
|
||||
pub use style::{Appearance as TextInputAppearance, StyleSheet as TextInputStyleSheet};
|
||||
263
src/widget/text_input/style.rs
Normal file
263
src/widget/text_input/style.rs
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
//! Change the appearance of a text input.
|
||||
use iced_core::{Background, BorderRadius, Color};
|
||||
|
||||
/// The appearance of a text input.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Appearance {
|
||||
/// The [`Background`] of the text input.
|
||||
pub background: Background,
|
||||
/// The border radius of the text input.
|
||||
pub border_radius: BorderRadius,
|
||||
/// The border offset
|
||||
pub border_offset: Option<f32>,
|
||||
/// The border width of the text input.
|
||||
pub border_width: f32,
|
||||
/// The border [`Color`] of the text input.
|
||||
pub border_color: Color,
|
||||
/// The label [`Color`] of the text input.
|
||||
pub label_color: Color,
|
||||
/// The text [`Color`] of the text input.
|
||||
pub selected_text_color: Color,
|
||||
/// The text [`Color`] of the text input.
|
||||
pub text_color: Color,
|
||||
/// The selected fill [`Color`] of the text input.
|
||||
pub selected_fill: Color,
|
||||
}
|
||||
|
||||
/// A set of rules that dictate the style of a text input.
|
||||
pub trait StyleSheet {
|
||||
/// The supported style of the [`StyleSheet`].
|
||||
type Style: Default;
|
||||
|
||||
/// Produces the style of an active text input.
|
||||
fn active(&self, style: &Self::Style) -> Appearance;
|
||||
|
||||
/// Produces the style of an errored text input.
|
||||
fn error(&self, style: &Self::Style) -> Appearance;
|
||||
|
||||
/// Produces the style of a focused text input.
|
||||
fn focused(&self, style: &Self::Style) -> Appearance;
|
||||
|
||||
/// Produces the [`Color`] of the placeholder of a text input.
|
||||
fn placeholder_color(&self, style: &Self::Style) -> Color;
|
||||
|
||||
/// Produces the style of an hovered text input.
|
||||
fn hovered(&self, style: &Self::Style) -> Appearance {
|
||||
self.focused(style)
|
||||
}
|
||||
|
||||
/// Produces the style of a disabled text input.
|
||||
fn disabled(&self, style: &Self::Style) -> Appearance;
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default)]
|
||||
pub enum TextInput {
|
||||
#[default]
|
||||
Default,
|
||||
ExpandableSearch,
|
||||
Search,
|
||||
Inline,
|
||||
}
|
||||
|
||||
impl StyleSheet for crate::Theme {
|
||||
type Style = TextInput;
|
||||
|
||||
fn active(&self, style: &Self::Style) -> Appearance {
|
||||
let palette = self.cosmic();
|
||||
let mut bg = palette.palette.neutral_7;
|
||||
bg.alpha = 0.25;
|
||||
let corner = palette.corner_radii;
|
||||
let label_color = palette.palette.neutral_9;
|
||||
match style {
|
||||
TextInput::Default => Appearance {
|
||||
background: Color::from(bg).into(),
|
||||
border_radius: corner.radius_s.into(),
|
||||
border_width: 1.0,
|
||||
border_offset: None,
|
||||
border_color: self.current_container().component.divider.into(),
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
TextInput::ExpandableSearch => Appearance {
|
||||
background: Color::TRANSPARENT.into(),
|
||||
border_radius: corner.radius_xl.into(),
|
||||
border_width: 0.0,
|
||||
border_offset: None,
|
||||
border_color: Color::TRANSPARENT,
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
TextInput::Search => Appearance {
|
||||
background: Color::from(bg).into(),
|
||||
border_radius: corner.radius_xl.into(),
|
||||
border_width: 0.0,
|
||||
border_offset: None,
|
||||
border_color: Color::TRANSPARENT,
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
TextInput::Inline => Appearance {
|
||||
background: Color::TRANSPARENT.into(),
|
||||
border_radius: corner.radius_0.into(),
|
||||
border_width: 0.0,
|
||||
border_offset: None,
|
||||
border_color: Color::TRANSPARENT,
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn error(&self, style: &Self::Style) -> Appearance {
|
||||
let palette = self.cosmic();
|
||||
let mut bg = palette.palette.neutral_7;
|
||||
bg.alpha = 0.25;
|
||||
let corner = palette.corner_radii;
|
||||
let label_color = palette.palette.neutral_9;
|
||||
|
||||
match style {
|
||||
TextInput::Default => Appearance {
|
||||
background: Color::from(bg).into(),
|
||||
border_radius: corner.radius_s.into(),
|
||||
border_width: 1.0,
|
||||
border_offset: Some(2.0),
|
||||
border_color: Color::from(palette.destructive_color()),
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
TextInput::Search | TextInput::ExpandableSearch => Appearance {
|
||||
background: Color::from(bg).into(),
|
||||
border_radius: corner.radius_xl.into(),
|
||||
border_width: 0.0,
|
||||
border_offset: None,
|
||||
border_color: Color::TRANSPARENT,
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
TextInput::Inline => Appearance {
|
||||
background: Color::TRANSPARENT.into(),
|
||||
border_radius: corner.radius_0.into(),
|
||||
border_width: 0.0,
|
||||
border_offset: None,
|
||||
border_color: Color::TRANSPARENT,
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self, style: &Self::Style) -> Appearance {
|
||||
let palette = self.cosmic();
|
||||
let mut bg = palette.palette.neutral_7;
|
||||
bg.alpha = 0.25;
|
||||
let corner = palette.corner_radii;
|
||||
let label_color = palette.palette.neutral_9;
|
||||
|
||||
match style {
|
||||
TextInput::Default => Appearance {
|
||||
background: Color::from(bg).into(),
|
||||
border_radius: corner.radius_s.into(),
|
||||
border_width: 1.0,
|
||||
border_offset: None,
|
||||
border_color: palette.accent.base.into(),
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
TextInput::Search | TextInput::ExpandableSearch => Appearance {
|
||||
background: Color::from(bg).into(),
|
||||
border_radius: corner.radius_xl.into(),
|
||||
border_offset: None,
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
TextInput::Inline => Appearance {
|
||||
background: Color::from(self.current_container().component.hover).into(),
|
||||
border_radius: corner.radius_0.into(),
|
||||
border_width: 0.0,
|
||||
border_offset: None,
|
||||
border_color: Color::TRANSPARENT,
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn focused(&self, style: &Self::Style) -> Appearance {
|
||||
let palette = self.cosmic();
|
||||
let mut bg = palette.palette.neutral_7;
|
||||
bg.alpha = 0.25;
|
||||
let corner = palette.corner_radii;
|
||||
let label_color = palette.palette.neutral_9;
|
||||
|
||||
match style {
|
||||
TextInput::Default => Appearance {
|
||||
background: Color::from(bg).into(),
|
||||
border_radius: corner.radius_s.into(),
|
||||
border_width: 1.0,
|
||||
border_offset: Some(2.0),
|
||||
border_color: palette.accent.base.into(),
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
TextInput::Search | TextInput::ExpandableSearch => Appearance {
|
||||
background: Color::from(bg).into(),
|
||||
border_radius: corner.radius_xl.into(),
|
||||
border_width: 0.0,
|
||||
border_offset: Some(2.0),
|
||||
border_color: Color::TRANSPARENT,
|
||||
text_color: self.current_container().on.into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
TextInput::Inline => Appearance {
|
||||
background: Color::from(palette.accent.base).into(),
|
||||
border_radius: corner.radius_0.into(),
|
||||
border_width: 0.0,
|
||||
border_offset: None,
|
||||
border_color: Color::TRANSPARENT,
|
||||
// TODO use regular text color here after text rendering handles multiple colors
|
||||
// in this case, for selected and unselected text
|
||||
text_color: palette.on_accent_color().into(),
|
||||
selected_text_color: palette.on_accent_color().into(),
|
||||
selected_fill: palette.accent_color().into(),
|
||||
label_color: label_color.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn placeholder_color(&self, _style: &Self::Style) -> Color {
|
||||
let palette = self.cosmic();
|
||||
let mut neutral_9 = palette.palette.neutral_9;
|
||||
neutral_9.alpha = 0.7;
|
||||
neutral_9.into()
|
||||
}
|
||||
|
||||
fn disabled(&self, style: &Self::Style) -> Appearance {
|
||||
self.active(style)
|
||||
}
|
||||
}
|
||||
131
src/widget/text_input/value.rs
Normal file
131
src/widget/text_input/value.rs
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
/// The value of a [`TextInput`].
|
||||
///
|
||||
/// [`TextInput`]: crate::widget::TextInput
|
||||
// TODO: Reduce allocations, cache results (?)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Value {
|
||||
graphemes: Vec<String>,
|
||||
}
|
||||
|
||||
impl Value {
|
||||
/// Creates a new [`Value`] from a string slice.
|
||||
pub fn new(string: &str) -> Self {
|
||||
let graphemes = UnicodeSegmentation::graphemes(string, true)
|
||||
.map(String::from)
|
||||
.collect();
|
||||
|
||||
Self { graphemes }
|
||||
}
|
||||
|
||||
/// Returns whether the [`Value`] is empty or not.
|
||||
///
|
||||
/// A [`Value`] is empty when it contains no graphemes.
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Returns the total amount of graphemes in the [`Value`].
|
||||
#[must_use]
|
||||
pub fn len(&self) -> usize {
|
||||
self.graphemes.len()
|
||||
}
|
||||
|
||||
/// Returns the position of the previous start of a word from the given
|
||||
/// grapheme `index`.
|
||||
#[must_use]
|
||||
pub fn previous_start_of_word(&self, index: usize) -> usize {
|
||||
let previous_string = &self.graphemes[..index.min(self.graphemes.len())].concat();
|
||||
|
||||
UnicodeSegmentation::split_word_bound_indices(previous_string as &str)
|
||||
.filter(|(_, word)| !word.trim_start().is_empty())
|
||||
.next_back()
|
||||
.map_or(0, |(i, previous_word)| {
|
||||
index
|
||||
- UnicodeSegmentation::graphemes(previous_word, true).count()
|
||||
- UnicodeSegmentation::graphemes(
|
||||
&previous_string[i + previous_word.len()..] as &str,
|
||||
true,
|
||||
)
|
||||
.count()
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the position of the next end of a word from the given grapheme
|
||||
/// `index`.
|
||||
#[must_use]
|
||||
pub fn next_end_of_word(&self, index: usize) -> usize {
|
||||
let next_string = &self.graphemes[index..].concat();
|
||||
|
||||
UnicodeSegmentation::split_word_bound_indices(next_string as &str)
|
||||
.find(|(_, word)| !word.trim_start().is_empty())
|
||||
.map_or(self.len(), |(i, next_word)| {
|
||||
index
|
||||
+ UnicodeSegmentation::graphemes(next_word, true).count()
|
||||
+ UnicodeSegmentation::graphemes(&next_string[..i] as &str, true).count()
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a new [`Value`] containing the graphemes from `start` until the
|
||||
/// given `end`.
|
||||
#[must_use]
|
||||
pub fn select(&self, start: usize, end: usize) -> Self {
|
||||
let graphemes = self.graphemes[start.min(self.len())..end.min(self.len())].to_vec();
|
||||
|
||||
Self { graphemes }
|
||||
}
|
||||
|
||||
/// Returns a new [`Value`] containing the graphemes until the given
|
||||
/// `index`.
|
||||
#[must_use]
|
||||
pub fn until(&self, index: usize) -> Self {
|
||||
let graphemes = self.graphemes[..index.min(self.len())].to_vec();
|
||||
|
||||
Self { graphemes }
|
||||
}
|
||||
|
||||
/// Inserts a new `char` at the given grapheme `index`.
|
||||
pub fn insert(&mut self, index: usize, c: char) {
|
||||
self.graphemes.insert(index, c.to_string());
|
||||
|
||||
self.graphemes = UnicodeSegmentation::graphemes(&self.to_string() as &str, true)
|
||||
.map(String::from)
|
||||
.collect();
|
||||
}
|
||||
|
||||
/// Inserts a bunch of graphemes at the given grapheme `index`.
|
||||
pub fn insert_many(&mut self, index: usize, mut value: Value) {
|
||||
let _ = self
|
||||
.graphemes
|
||||
.splice(index..index, value.graphemes.drain(..));
|
||||
}
|
||||
|
||||
/// Removes the grapheme at the given `index`.
|
||||
pub fn remove(&mut self, index: usize) {
|
||||
let _ = self.graphemes.remove(index);
|
||||
}
|
||||
|
||||
/// Removes the graphemes from `start` to `end`.
|
||||
pub fn remove_many(&mut self, start: usize, end: usize) {
|
||||
let _ = self.graphemes.splice(start..end, std::iter::empty());
|
||||
}
|
||||
|
||||
/// Returns a new [`Value`] with all its graphemes replaced with the
|
||||
/// dot ('•') character.
|
||||
#[must_use]
|
||||
pub fn secure(&self) -> Self {
|
||||
Self {
|
||||
graphemes: std::iter::repeat(String::from("•"))
|
||||
.take(self.graphemes.len())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Value {
|
||||
fn to_string(&self) -> String {
|
||||
self.graphemes.concat()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue