Draft Editor API and TextEditor widget
This commit is contained in:
parent
346af3f8b0
commit
6448429103
25 changed files with 1384 additions and 92 deletions
|
|
@ -66,6 +66,13 @@ impl<T: Damage> Damage for Primitive<T> {
|
|||
|
||||
bounds.expand(1.5)
|
||||
}
|
||||
Self::Editor {
|
||||
editor, position, ..
|
||||
} => {
|
||||
let bounds = Rectangle::new(*position, editor.bounds);
|
||||
|
||||
bounds.expand(1.5)
|
||||
}
|
||||
Self::Quad { bounds, .. }
|
||||
| Self::Image { bounds, .. }
|
||||
| Self::Svg { bounds, .. } => bounds.expand(1.0),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use crate::core::image;
|
|||
use crate::core::svg;
|
||||
use crate::core::text;
|
||||
use crate::core::{Background, Color, Font, Pixels, Point, Rectangle, Vector};
|
||||
use crate::text::editor;
|
||||
use crate::text::paragraph;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
|
@ -41,6 +42,15 @@ pub enum Primitive<T> {
|
|||
/// The color of the paragraph.
|
||||
color: Color,
|
||||
},
|
||||
/// An editor primitive
|
||||
Editor {
|
||||
/// The [`editor::Weak`] reference.
|
||||
editor: editor::Weak,
|
||||
/// The position of the paragraph.
|
||||
position: Point,
|
||||
/// The color of the paragraph.
|
||||
color: Color,
|
||||
},
|
||||
/// A quad primitive
|
||||
Quad {
|
||||
/// The bounds of the quad
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ where
|
|||
{
|
||||
type Font = Font;
|
||||
type Paragraph = text::Paragraph;
|
||||
type Editor = text::Editor;
|
||||
|
||||
const ICON_FONT: Font = Font::with_name("Iced-Icons");
|
||||
const CHECKMARK_ICON: char = '\u{f00c}';
|
||||
|
|
@ -171,6 +172,19 @@ where
|
|||
});
|
||||
}
|
||||
|
||||
fn fill_editor(
|
||||
&mut self,
|
||||
editor: &Self::Editor,
|
||||
position: Point,
|
||||
color: Color,
|
||||
) {
|
||||
self.primitives.push(Primitive::Editor {
|
||||
editor: editor.downgrade(),
|
||||
position,
|
||||
color,
|
||||
});
|
||||
}
|
||||
|
||||
fn fill_text(
|
||||
&mut self,
|
||||
text: Text<'_, Self::Font>,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
pub mod cache;
|
||||
pub mod editor;
|
||||
pub mod paragraph;
|
||||
|
||||
pub use cache::Cache;
|
||||
pub use editor::Editor;
|
||||
pub use paragraph::Paragraph;
|
||||
|
||||
pub use cosmic_text;
|
||||
|
|
|
|||
327
graphics/src/text/editor.rs
Normal file
327
graphics/src/text/editor.rs
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
use crate::core::text::editor::{self, Action, Cursor};
|
||||
use crate::core::text::LineHeight;
|
||||
use crate::core::{Font, Pixels, Point, Size};
|
||||
use crate::text;
|
||||
|
||||
use cosmic_text::Edit;
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::{self, Arc};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Editor(Option<Arc<Internal>>);
|
||||
|
||||
struct Internal {
|
||||
editor: cosmic_text::Editor,
|
||||
font: Font,
|
||||
bounds: Size,
|
||||
min_bounds: Size,
|
||||
version: text::Version,
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn buffer(&self) -> &cosmic_text::Buffer {
|
||||
&self.internal().editor.buffer()
|
||||
}
|
||||
|
||||
pub fn downgrade(&self) -> Weak {
|
||||
let editor = self.internal();
|
||||
|
||||
Weak {
|
||||
raw: Arc::downgrade(editor),
|
||||
bounds: editor.bounds,
|
||||
}
|
||||
}
|
||||
|
||||
fn internal(&self) -> &Arc<Internal> {
|
||||
self.0
|
||||
.as_ref()
|
||||
.expect("editor should always be initialized")
|
||||
}
|
||||
}
|
||||
|
||||
impl editor::Editor for Editor {
|
||||
type Font = Font;
|
||||
|
||||
fn with_text(text: &str) -> Self {
|
||||
let mut buffer = cosmic_text::Buffer::new_empty(cosmic_text::Metrics {
|
||||
font_size: 1.0,
|
||||
line_height: 1.0,
|
||||
});
|
||||
|
||||
buffer.set_text(
|
||||
text::font_system()
|
||||
.write()
|
||||
.expect("Write font system")
|
||||
.raw(),
|
||||
text,
|
||||
cosmic_text::Attrs::new(),
|
||||
cosmic_text::Shaping::Advanced,
|
||||
);
|
||||
|
||||
Editor(Some(Arc::new(Internal {
|
||||
editor: cosmic_text::Editor::new(buffer),
|
||||
..Default::default()
|
||||
})))
|
||||
}
|
||||
|
||||
fn cursor(&self) -> editor::Cursor {
|
||||
let internal = self.internal();
|
||||
|
||||
match internal.editor.select_opt() {
|
||||
Some(selection) => {
|
||||
// TODO
|
||||
Cursor::Selection(vec![])
|
||||
}
|
||||
None => {
|
||||
let cursor = internal.editor.cursor();
|
||||
let buffer = internal.editor.buffer();
|
||||
|
||||
let lines_before_cursor: usize = buffer
|
||||
.lines
|
||||
.iter()
|
||||
.take(cursor.line)
|
||||
.map(|line| {
|
||||
line.layout_opt()
|
||||
.as_ref()
|
||||
.expect("Line layout should be cached")
|
||||
.len()
|
||||
})
|
||||
.sum();
|
||||
|
||||
let line = buffer
|
||||
.lines
|
||||
.get(cursor.line)
|
||||
.expect("Cursor line should be present");
|
||||
|
||||
let layout = line
|
||||
.layout_opt()
|
||||
.as_ref()
|
||||
.expect("Line layout should be cached");
|
||||
|
||||
let mut lines = layout.iter().enumerate();
|
||||
|
||||
let (subline, offset) = lines
|
||||
.find_map(|(i, line)| {
|
||||
let start = line
|
||||
.glyphs
|
||||
.first()
|
||||
.map(|glyph| glyph.start)
|
||||
.unwrap_or(0);
|
||||
let end = line
|
||||
.glyphs
|
||||
.last()
|
||||
.map(|glyph| glyph.end)
|
||||
.unwrap_or(0);
|
||||
|
||||
let is_cursor_after_start = start <= cursor.index;
|
||||
|
||||
let is_cursor_before_end = match cursor.affinity {
|
||||
cosmic_text::Affinity::Before => {
|
||||
cursor.index <= end
|
||||
}
|
||||
cosmic_text::Affinity::After => cursor.index < end,
|
||||
};
|
||||
|
||||
if is_cursor_after_start && is_cursor_before_end {
|
||||
let offset = line
|
||||
.glyphs
|
||||
.iter()
|
||||
.take_while(|glyph| cursor.index > glyph.start)
|
||||
.map(|glyph| glyph.w)
|
||||
.sum();
|
||||
|
||||
Some((i, offset))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or((0, 0.0));
|
||||
|
||||
let line_height = buffer.metrics().line_height;
|
||||
|
||||
let scroll_offset = buffer.scroll() as f32 * line_height;
|
||||
|
||||
Cursor::Caret(Point::new(
|
||||
offset,
|
||||
(lines_before_cursor + subline) as f32 * line_height
|
||||
- scroll_offset,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn perform(&mut self, action: Action) {
|
||||
let mut font_system =
|
||||
text::font_system().write().expect("Write font system");
|
||||
|
||||
let editor =
|
||||
self.0.take().expect("Editor should always be initialized");
|
||||
|
||||
// TODO: Handle multiple strong references somehow
|
||||
let mut internal = Arc::try_unwrap(editor)
|
||||
.expect("Editor cannot have multiple strong references");
|
||||
|
||||
let editor = &mut internal.editor;
|
||||
|
||||
let mut act = |action| editor.action(font_system.raw(), action);
|
||||
|
||||
match action {
|
||||
Action::MoveLeft => act(cosmic_text::Action::Left),
|
||||
Action::MoveRight => act(cosmic_text::Action::Right),
|
||||
Action::MoveUp => act(cosmic_text::Action::Up),
|
||||
Action::MoveDown => act(cosmic_text::Action::Down),
|
||||
Action::Insert(c) => act(cosmic_text::Action::Insert(c)),
|
||||
Action::Backspace => act(cosmic_text::Action::Backspace),
|
||||
Action::Delete => act(cosmic_text::Action::Delete),
|
||||
Action::Click(position) => act(cosmic_text::Action::Click {
|
||||
x: position.x as i32,
|
||||
y: position.y as i32,
|
||||
}),
|
||||
Action::Drag(position) => act(cosmic_text::Action::Drag {
|
||||
x: position.x as i32,
|
||||
y: position.y as i32,
|
||||
}),
|
||||
_ => todo!(),
|
||||
}
|
||||
|
||||
editor.shape_as_needed(font_system.raw());
|
||||
|
||||
self.0 = Some(Arc::new(internal));
|
||||
}
|
||||
|
||||
fn bounds(&self) -> Size {
|
||||
self.internal().bounds
|
||||
}
|
||||
|
||||
fn min_bounds(&self) -> Size {
|
||||
self.internal().min_bounds
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
new_bounds: Size,
|
||||
new_font: Font,
|
||||
new_size: Pixels,
|
||||
new_line_height: LineHeight,
|
||||
) {
|
||||
let editor =
|
||||
self.0.take().expect("editor should always be initialized");
|
||||
|
||||
let mut internal = Arc::try_unwrap(editor)
|
||||
.expect("Editor cannot have multiple strong references");
|
||||
|
||||
let mut font_system =
|
||||
text::font_system().write().expect("Write font system");
|
||||
|
||||
let mut changed = false;
|
||||
|
||||
if new_font != internal.font {
|
||||
for line in internal.editor.buffer_mut().lines.iter_mut() {
|
||||
let _ = line.set_attrs_list(cosmic_text::AttrsList::new(
|
||||
text::to_attributes(new_font),
|
||||
));
|
||||
}
|
||||
|
||||
changed = true;
|
||||
}
|
||||
|
||||
let metrics = internal.editor.buffer().metrics();
|
||||
let new_line_height = new_line_height.to_absolute(new_size);
|
||||
|
||||
if new_size.0 != metrics.font_size
|
||||
|| new_line_height.0 != metrics.line_height
|
||||
{
|
||||
internal.editor.buffer_mut().set_metrics(
|
||||
font_system.raw(),
|
||||
cosmic_text::Metrics::new(new_size.0, new_line_height.0),
|
||||
);
|
||||
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if new_bounds != internal.bounds {
|
||||
internal.editor.buffer_mut().set_size(
|
||||
font_system.raw(),
|
||||
new_bounds.width,
|
||||
new_bounds.height,
|
||||
);
|
||||
|
||||
internal.bounds = new_bounds;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if changed {
|
||||
internal.min_bounds = text::measure(&internal.editor.buffer());
|
||||
}
|
||||
|
||||
self.0 = Some(Arc::new(internal));
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Editor {
|
||||
fn default() -> Self {
|
||||
Self(Some(Arc::new(Internal::default())))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Internal {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.font == other.font
|
||||
&& self.bounds == other.bounds
|
||||
&& self.min_bounds == other.min_bounds
|
||||
&& self.editor.buffer().metrics() == other.editor.buffer().metrics()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Internal {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
editor: cosmic_text::Editor::new(cosmic_text::Buffer::new_empty(
|
||||
cosmic_text::Metrics {
|
||||
font_size: 1.0,
|
||||
line_height: 1.0,
|
||||
},
|
||||
)),
|
||||
font: Font::default(),
|
||||
bounds: Size::ZERO,
|
||||
min_bounds: Size::ZERO,
|
||||
version: text::Version::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Internal {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Internal")
|
||||
.field("font", &self.font)
|
||||
.field("bounds", &self.bounds)
|
||||
.field("min_bounds", &self.min_bounds)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Weak {
|
||||
raw: sync::Weak<Internal>,
|
||||
pub bounds: Size,
|
||||
}
|
||||
|
||||
impl Weak {
|
||||
pub fn upgrade(&self) -> Option<Editor> {
|
||||
self.raw.upgrade().map(Some).map(Editor)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Weak {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self.raw.upgrade(), other.raw.upgrade()) {
|
||||
(Some(p1), Some(p2)) => p1 == p2,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue