use std::cell::{Cell, RefCell};
use std::rc::Rc;
use crate::shortcut::StandardEdit;
use crate::signal::Signal;
use crate::text_input_document::{TextInputDocument, TextInputEditor};
use crate::widget::EventContext;
use crate::widget_id::WidgetId;
pub trait TextSurface {
fn can_undo(&self) -> bool;
fn can_redo(&self) -> bool;
fn undo(&self);
fn redo(&self);
fn history_frozen(&self) -> bool {
false
}
fn has_selection(&self) -> bool;
fn is_read_only(&self) -> bool;
fn allows_copy(&self) -> bool;
fn accepts(&self, command: StandardEdit) -> bool {
match command {
StandardEdit::Cut => !self.is_read_only() && self.allows_copy(),
StandardEdit::Copy => self.allows_copy(),
StandardEdit::Paste | StandardEdit::PastePlain => !self.is_read_only(),
StandardEdit::Undo | StandardEdit::Redo => !self.history_frozen(),
StandardEdit::SelectAll => true,
}
}
fn cut(&self, ctx: &EventContext<'_>);
fn copy(&self, ctx: &EventContext<'_>);
fn paste(&self, ctx: &EventContext<'_>);
fn paste_plain(&self, ctx: &EventContext<'_>);
fn select_all(&self);
fn input_document(&self) -> Option<&dyn TextInputDocument> {
None
}
fn input_editor(&self) -> Option<&dyn TextInputEditor> {
None
}
}
#[derive(Clone)]
pub struct TextSurfaces {
entries: Rc<RefCell<Vec<(WidgetId, Rc<dyn TextSurface>)>>>,
focused: Signal<Option<WidgetId>>,
empty_history_passes_on: Rc<Cell<bool>>,
}
impl std::fmt::Debug for TextSurfaces {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TextSurfaces")
.field("registered", &self.entries.borrow().len())
.field("focused_is_text", &self.focused_is_text_surface())
.field("empty_history_passes_on", &self.empty_history_passes_on())
.finish()
}
}
impl TextSurfaces {
pub(crate) fn new(focused: Signal<Option<WidgetId>>) -> Self {
Self {
entries: Rc::new(RefCell::new(Vec::new())),
focused,
empty_history_passes_on: Rc::new(Cell::new(false)),
}
}
pub(crate) fn insert(&self, owner: WidgetId, surface: Rc<dyn TextSurface>) {
let mut entries = self.entries.borrow_mut();
entries.retain(|(id, _)| *id != owner);
entries.push((owner, surface));
}
pub(crate) fn remove(&self, owner: WidgetId) {
self.entries.borrow_mut().retain(|(id, _)| *id != owner);
}
pub fn focused(&self) -> Option<Rc<dyn TextSurface>> {
let focused = self.focused.get()?;
self.entries
.borrow()
.iter()
.find(|(id, _)| *id == focused)
.map(|(_, s)| Rc::clone(s))
}
pub fn surface(&self, owner: WidgetId) -> Option<Rc<dyn TextSurface>> {
self.entries
.borrow()
.iter()
.find(|(id, _)| *id == owner)
.map(|(_, s)| Rc::clone(s))
}
pub fn focused_is_text_surface(&self) -> bool {
self.focused().is_some()
}
pub fn with_focused_input_document<R>(
&self,
f: impl FnOnce(&dyn TextInputDocument) -> R,
) -> Option<R> {
let surface = self.focused()?;
let document = surface.input_document()?;
Some(f(document))
}
pub fn with_focused_input_editor<R>(
&self,
f: impl FnOnce(&dyn TextInputEditor) -> R,
) -> Option<R> {
let surface = self.focused()?;
let editor = surface.input_editor()?;
Some(f(editor))
}
pub fn is_read_only(&self, owner: WidgetId) -> bool {
self.entries
.borrow()
.iter()
.find(|(id, _)| *id == owner)
.map(|(_, s)| s.is_read_only())
.unwrap_or(false)
}
pub fn set_empty_history_passes_on(&self, on: bool) {
self.empty_history_passes_on.set(on);
}
pub fn empty_history_passes_on(&self) -> bool {
self.empty_history_passes_on.get()
}
pub fn focus_signal(&self) -> Signal<Option<WidgetId>> {
self.focused.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Plain {
read_only: bool,
allows_copy: bool,
frozen: bool,
}
impl TextSurface for Plain {
fn can_undo(&self) -> bool {
true
}
fn can_redo(&self) -> bool {
true
}
fn undo(&self) {}
fn redo(&self) {}
fn history_frozen(&self) -> bool {
self.frozen
}
fn has_selection(&self) -> bool {
true
}
fn is_read_only(&self) -> bool {
self.read_only
}
fn allows_copy(&self) -> bool {
self.allows_copy
}
fn cut(&self, _: &EventContext<'_>) {}
fn copy(&self, _: &EventContext<'_>) {}
fn paste(&self, _: &EventContext<'_>) {}
fn paste_plain(&self, _: &EventContext<'_>) {}
fn select_all(&self) {}
}
struct Described {
text: &'static str,
}
impl crate::text_input_document::TextInputDocument for Described {
fn char_count(&self) -> Option<usize> {
Some(self.text.chars().count())
}
fn text_in(&self, range: std::ops::Range<usize>) -> Option<String> {
Some(
self.text
.chars()
.skip(range.start)
.take(range.len())
.collect(),
)
}
fn selection(&self) -> Option<std::ops::Range<usize>> {
Some(1..2)
}
fn caret(&self) -> Option<usize> {
Some(2)
}
fn composition_range(&self) -> Option<std::ops::Range<usize>> {
None
}
fn first_line_rect(
&self,
_: std::ops::Range<usize>,
) -> Option<crate::text_input_document::FirstLineRect> {
None
}
fn offset_at_point(&self, _: teksilo_canvas::Point) -> Option<usize> {
None
}
fn visible_text_rect(&self) -> Option<teksilo_canvas::Rect> {
None
}
fn is_editable(&self) -> bool {
true
}
fn is_secure(&self) -> bool {
false
}
fn writing_tools_allowed(&self) -> bool {
true
}
fn utf16_index(&self) -> Option<Rc<crate::text_input_document::Utf16Index>> {
Some(Rc::new(crate::text_input_document::Utf16Index::new(
self.text,
)))
}
}
impl TextSurface for Described {
fn can_undo(&self) -> bool {
false
}
fn can_redo(&self) -> bool {
false
}
fn undo(&self) {}
fn redo(&self) {}
fn has_selection(&self) -> bool {
true
}
fn is_read_only(&self) -> bool {
false
}
fn allows_copy(&self) -> bool {
true
}
fn cut(&self, _: &EventContext<'_>) {}
fn copy(&self, _: &EventContext<'_>) {}
fn paste(&self, _: &EventContext<'_>) {}
fn paste_plain(&self, _: &EventContext<'_>) {}
fn select_all(&self) {}
fn input_document(&self) -> Option<&dyn crate::text_input_document::TextInputDocument> {
Some(self)
}
}
#[test]
fn the_focused_input_document_is_the_focused_widgets_own() {
let focused = Signal::new(None);
let surfaces = TextSurfaces::new(focused.clone());
let described = WidgetId::from(slotmap::KeyData::from_ffi(1 << 32 | 1));
let plain = WidgetId::from(slotmap::KeyData::from_ffi(1 << 32 | 2));
surfaces.insert(
described,
Rc::new(Described {
text: "a\u{1F600}b",
}),
);
surfaces.insert(
plain,
Rc::new(Plain {
read_only: false,
allows_copy: true,
frozen: false,
}),
);
assert_eq!(surfaces.with_focused_input_document(|d| d.caret()), None);
focused.set(Some(described));
assert_eq!(
surfaces.with_focused_input_document(|d| d.selection()),
Some(Some(1..2))
);
assert_eq!(
surfaces.with_focused_input_document(|d| d.char_range_to_utf16(1..2)),
Some(Some(1..3)),
"the emoji is two UTF-16 units"
);
focused.set(Some(plain));
assert_eq!(
surfaces.with_focused_input_document(|d| d.caret()),
None,
"a surface that describes no document answers nothing"
);
let left = surfaces.surface(described).expect("registered");
assert_eq!(
left.input_document().and_then(|d| d.selection()),
Some(1..2)
);
surfaces.remove(described);
assert!(
surfaces.surface(described).is_none(),
"gone with its widget"
);
}
#[test]
fn the_default_accepts_follows_the_older_questions() {
let editable = Plain {
read_only: false,
allows_copy: true,
frozen: false,
};
for command in [
StandardEdit::Cut,
StandardEdit::Copy,
StandardEdit::Paste,
StandardEdit::PastePlain,
StandardEdit::Undo,
StandardEdit::Redo,
StandardEdit::SelectAll,
] {
assert!(editable.accepts(command), "{command:?}");
}
let read_only = Plain {
read_only: true,
allows_copy: true,
frozen: true,
};
assert!(!read_only.accepts(StandardEdit::Cut));
assert!(!read_only.accepts(StandardEdit::Paste));
assert!(!read_only.accepts(StandardEdit::PastePlain));
assert!(!read_only.accepts(StandardEdit::Undo));
assert!(read_only.accepts(StandardEdit::Copy));
assert!(read_only.accepts(StandardEdit::SelectAll));
let secret = Plain {
read_only: false,
allows_copy: false,
frozen: false,
};
assert!(!secret.accepts(StandardEdit::Copy));
assert!(!secret.accepts(StandardEdit::Cut), "a cut is a copy too");
assert!(secret.accepts(StandardEdit::Paste));
}
}