Skip to content

Commit

Permalink
Implement clipboard support and select motions in textboxes
Browse files Browse the repository at this point in the history
Closes #21.
  • Loading branch information
LPGhatguy committed Jan 25, 2025
1 parent 300fb6d commit 4d44508
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 23 deletions.
11 changes: 7 additions & 4 deletions crates/yakui-widgets/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ default-fonts = []
[dependencies]
yakui-core = { path = "../yakui-core", version = "0.3.0" }

cosmic-text = { version = "0.12.0", default-features = false, features = [
"std",
"swash",
] }
arboard = "3.4.1"
log = "0.4.25"
sys-locale = "0.3.1"
thunderdome = "0.6.0"

[dependencies.cosmic-text]
version = "0.12.0"
default-features = false
features = ["std", "swash"]

[dev-dependencies]
yakui = { path = "../yakui" }
yakui-test = { path = "../yakui-test" }
Expand Down
67 changes: 67 additions & 0 deletions crates/yakui-widgets/src/clipboard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use std::cell::RefCell;
use std::rc::Rc;

use arboard::Clipboard;

#[derive(Clone)]
pub struct ClipboardHolder {
inner: Option<Rc<RefCell<Clipboard>>>,
}

impl ClipboardHolder {
pub fn new() -> Self {
let inner = match Clipboard::new() {
Ok(c) => Some(Rc::new(RefCell::new(c))),
Err(err) => {
log::error!("Failed to open clipboard: {err:?}");
None
}
};

Self { inner }
}

pub fn copy(&self, text: &str) {
self.operate(|clipboard| {
clipboard.set_text(text)?;
Ok(())
});
}

pub fn paste(&self) -> Option<String> {
self.operate(|clipboard| clipboard.get_text())
}

pub fn dispose(&mut self) {
// Clipboard must be explicitly dropped when the program terminates, so
// we take it here to drop it.
let _ = self.inner.take();
}

fn operate<T>(
&self,
callback: impl FnOnce(&mut Clipboard) -> Result<T, arboard::Error>,
) -> Option<T> {
let inner = self.inner.as_ref().map(|inner| inner.borrow_mut());

match inner {
Some(mut clipboard) => match callback(&mut *clipboard) {
Ok(v) => Some(v),
Err(err) => {
log::error!("Failed to operate on clipboard: {err:?}");
None
}
},
None => {
log::error!("Failed to operate on clipboard: clipboard failed to open");
None
}
}
}
}

impl Default for ClipboardHolder {
fn default() -> Self {
Self::new()
}
}
1 change: 1 addition & 0 deletions crates/yakui-widgets/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mod ignore_debug;

pub mod util;

pub mod clipboard;
pub mod colors;
pub mod font;
pub mod shapes;
Expand Down
155 changes: 136 additions & 19 deletions crates/yakui-widgets/src/widgets/textbox.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
use std::cell::{Cell, RefCell};
use std::mem;

use cosmic_text::Edit;
use cosmic_text::{Edit, Selection};
use yakui_core::event::{EventInterest, EventResponse, WidgetEvent};
use yakui_core::geometry::{Color, Constraints, Rect, Vec2};
use yakui_core::input::{KeyCode, Modifiers, MouseButton};
use yakui_core::paint::PaintRect;
use yakui_core::widget::{EventContext, LayoutContext, PaintContext, Widget};
use yakui_core::Response;

use crate::clipboard::ClipboardHolder;
use crate::font::Fonts;
use crate::shapes::{self, RoundedRectangle};
use crate::style::{TextAlignment, TextStyle};
Expand Down Expand Up @@ -471,9 +472,28 @@ impl Widget for TextBoxWidget {
let fonts = ctx.dom.get_global_or_init(Fonts::default);
fonts.with_system(|font_system| {
if let Some(editor) = self.cosmic_editor.get_mut() {
enum SelectMove {
Deselect,
Left,
Right,
}

let mut select_move = None;
let original_bounds = editor.selection_bounds().unwrap_or_else(|| {
let cursor = editor.cursor();
(cursor, cursor)
});
let res;

match key {
KeyCode::ArrowLeft => {
if *down {
if modifiers.shift() {
select_move = Some(SelectMove::Left);
} else {
select_move = Some(SelectMove::Deselect);
}

if modifiers.ctrl() {
editor.action(
font_system,
Expand All @@ -488,11 +508,18 @@ impl Widget for TextBoxWidget {
);
}
}
EventResponse::Sink

res = EventResponse::Sink;
}

KeyCode::ArrowRight => {
if *down {
if modifiers.shift() {
select_move = Some(SelectMove::Right);
} else {
select_move = Some(SelectMove::Deselect);
}

if modifiers.ctrl() {
editor.action(
font_system,
Expand All @@ -507,83 +534,128 @@ impl Widget for TextBoxWidget {
);
}
}
EventResponse::Sink

res = EventResponse::Sink;
}

KeyCode::ArrowUp => {
if *down {
if modifiers.shift() {
select_move = Some(SelectMove::Left);
} else {
select_move = Some(SelectMove::Deselect);
}

editor.action(
font_system,
cosmic_text::Action::Motion(cosmic_text::Motion::Up),
);
}
EventResponse::Sink

res = EventResponse::Sink;
}

KeyCode::ArrowDown => {
if *down {
if modifiers.shift() {
select_move = Some(SelectMove::Right);
} else {
select_move = Some(SelectMove::Deselect);
}

editor.action(
font_system,
cosmic_text::Action::Motion(cosmic_text::Motion::Down),
);
}
EventResponse::Sink

res = EventResponse::Sink;
}

KeyCode::PageUp => {
if *down {
if modifiers.shift() {
select_move = Some(SelectMove::Left);
} else {
select_move = Some(SelectMove::Deselect);
}

editor.action(
font_system,
cosmic_text::Action::Motion(cosmic_text::Motion::PageUp),
);
}
EventResponse::Sink

res = EventResponse::Sink;
}

KeyCode::PageDown => {
if *down {
if modifiers.shift() {
select_move = Some(SelectMove::Right);
} else {
select_move = Some(SelectMove::Deselect);
}

editor.action(
font_system,
cosmic_text::Action::Motion(cosmic_text::Motion::PageDown),
);
}
EventResponse::Sink

res = EventResponse::Sink;
}

KeyCode::Backspace => {
if *down {
editor.action(font_system, cosmic_text::Action::Backspace);
self.text_changed_by_cosmic.set(true);
}
EventResponse::Sink

res = EventResponse::Sink;
}

KeyCode::Delete => {
if *down {
editor.action(font_system, cosmic_text::Action::Delete);
self.text_changed_by_cosmic.set(true);
}
EventResponse::Sink

res = EventResponse::Sink;
}

KeyCode::Home => {
if *down {
if modifiers.shift() {
select_move = Some(SelectMove::Left);
} else {
select_move = Some(SelectMove::Deselect);
}

editor.action(
font_system,
cosmic_text::Action::Motion(cosmic_text::Motion::Home),
);
}
EventResponse::Sink

res = EventResponse::Sink;
}

KeyCode::End => {
if *down {
if modifiers.shift() {
select_move = Some(SelectMove::Right);
} else {
select_move = Some(SelectMove::Deselect);
}

editor.action(
font_system,
cosmic_text::Action::Motion(cosmic_text::Motion::End),
);
}
EventResponse::Sink

res = EventResponse::Sink;
}

KeyCode::Enter | KeyCode::NumpadEnter => {
Expand All @@ -601,7 +673,8 @@ impl Widget for TextBoxWidget {
self.text_changed_by_cosmic.set(true);
}
}
EventResponse::Sink

res = EventResponse::Sink;
}

KeyCode::Escape => {
Expand All @@ -611,7 +684,7 @@ impl Widget for TextBoxWidget {
ctx.input.set_selection(None);
}
}
EventResponse::Sink
res = EventResponse::Sink;
}

KeyCode::KeyA if *down && main_modifier(modifiers) => {
Expand All @@ -621,21 +694,65 @@ impl Widget for TextBoxWidget {
editor.set_cursor(end);
}

EventResponse::Sink
res = EventResponse::Sink;
}

KeyCode::KeyX if *down && main_modifier(modifiers) => {
let clipboard =
ctx.dom.get_global_or_init(ClipboardHolder::default);

if let Some(text) = editor.copy_selection() {
clipboard.copy(&text);
}
editor.delete_selection();
self.text_changed_by_cosmic.set(true);

res = EventResponse::Sink;
}

KeyCode::KeyC if *down && main_modifier(modifiers) => {
println!("TODO: Copy!");
EventResponse::Sink
let clipboard =
ctx.dom.get_global_or_init(ClipboardHolder::default);

if let Some(text) = editor.copy_selection() {
clipboard.copy(&text);
}

res = EventResponse::Sink;
}

KeyCode::KeyV if *down && main_modifier(modifiers) => {
println!("TODO: Paste!");
EventResponse::Sink
let clipboard =
ctx.dom.get_global_or_init(ClipboardHolder::default);

if let Some(text) = clipboard.paste() {
editor.insert_string(&text, None);
self.text_changed_by_cosmic.set(true);
}

res = EventResponse::Sink;
}

_ => EventResponse::Sink,
_ => res = EventResponse::Sink,
}

match select_move {
Some(SelectMove::Deselect) => {
editor.set_selection(Selection::None);
}

Some(SelectMove::Left) => {
editor.set_selection(Selection::Normal(original_bounds.1));
}

Some(SelectMove::Right) => {
editor.set_selection(Selection::Normal(original_bounds.0));
}

None => {}
}

res
} else {
EventResponse::Bubble
}
Expand Down

0 comments on commit 4d44508

Please sign in to comment.