Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Text input #143

Merged
merged 7 commits into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 48 additions & 4 deletions examples/cosmic-sctk/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) => {
Expand All @@ -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()
Expand Down Expand Up @@ -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();
Expand Down
4 changes: 4 additions & 0 deletions examples/cosmic/src/window/demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
4 changes: 4 additions & 0 deletions src/widget/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
173 changes: 173 additions & 0 deletions src/widget/text_input/cursor.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 65 additions & 0 deletions src/widget/text_input/editor.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
Loading
Loading