Decouple editing from buffer
This commit is contained in:
parent
26c83be35a
commit
92cad6fe13
11 changed files with 901 additions and 858 deletions
|
|
@ -26,9 +26,10 @@ use cosmic::{
|
||||||
use cosmic_text::{
|
use cosmic_text::{
|
||||||
Attrs,
|
Attrs,
|
||||||
AttrsList,
|
AttrsList,
|
||||||
|
Buffer,
|
||||||
|
Editor,
|
||||||
FontSystem,
|
FontSystem,
|
||||||
TextBuffer,
|
Metrics,
|
||||||
TextMetrics,
|
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
|
|
@ -47,13 +48,13 @@ lazy_static::lazy_static! {
|
||||||
static ref FONT_SYSTEM: FontSystem<'static> = FontSystem::new();
|
static ref FONT_SYSTEM: FontSystem<'static> = FontSystem::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
static FONT_SIZES: &'static [TextMetrics] = &[
|
static FONT_SIZES: &'static [Metrics] = &[
|
||||||
TextMetrics::new(10, 14), // Caption
|
Metrics::new(10, 14), // Caption
|
||||||
TextMetrics::new(14, 20), // Body
|
Metrics::new(14, 20), // Body
|
||||||
TextMetrics::new(20, 28), // Title 4
|
Metrics::new(20, 28), // Title 4
|
||||||
TextMetrics::new(24, 32), // Title 3
|
Metrics::new(24, 32), // Title 3
|
||||||
TextMetrics::new(28, 36), // Title 2
|
Metrics::new(28, 36), // Title 2
|
||||||
TextMetrics::new(32, 44), // Title 1
|
Metrics::new(32, 44), // Title 1
|
||||||
];
|
];
|
||||||
|
|
||||||
fn main() -> cosmic::iced::Result {
|
fn main() -> cosmic::iced::Result {
|
||||||
|
|
@ -68,7 +69,7 @@ pub struct Window {
|
||||||
theme: Theme,
|
theme: Theme,
|
||||||
path_opt: Option<PathBuf>,
|
path_opt: Option<PathBuf>,
|
||||||
attrs: Attrs<'static>,
|
attrs: Attrs<'static>,
|
||||||
buffer: Mutex<TextBuffer<'static>>,
|
editor: Mutex<Editor<'static>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
|
@ -79,22 +80,22 @@ pub enum Message {
|
||||||
Bold(bool),
|
Bold(bool),
|
||||||
Italic(bool),
|
Italic(bool),
|
||||||
Monospaced(bool),
|
Monospaced(bool),
|
||||||
MetricsChanged(TextMetrics),
|
MetricsChanged(Metrics),
|
||||||
ThemeChanged(&'static str),
|
ThemeChanged(&'static str),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Window {
|
impl Window {
|
||||||
pub fn open(&mut self, path: PathBuf) {
|
pub fn open(&mut self, path: PathBuf) {
|
||||||
let mut buffer = self.buffer.lock().unwrap();
|
let mut editor = self.editor.lock().unwrap();
|
||||||
match fs::read_to_string(&path) {
|
match fs::read_to_string(&path) {
|
||||||
Ok(text) => {
|
Ok(text) => {
|
||||||
log::info!("opened '{}'", path.display());
|
log::info!("opened '{}'", path.display());
|
||||||
buffer.set_text(&text, self.attrs);
|
editor.buffer.set_text(&text, self.attrs);
|
||||||
self.path_opt = Some(path);
|
self.path_opt = Some(path);
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("failed to open '{}': {}", path.display(), err);
|
log::error!("failed to open '{}': {}", path.display(), err);
|
||||||
buffer.set_text("", self.attrs);
|
editor.buffer.set_text("", self.attrs);
|
||||||
self.path_opt = None;
|
self.path_opt = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -112,17 +113,17 @@ impl Application for Window {
|
||||||
.monospaced(true)
|
.monospaced(true)
|
||||||
.family(cosmic_text::Family::Monospace);
|
.family(cosmic_text::Family::Monospace);
|
||||||
|
|
||||||
let mut buffer = TextBuffer::new(
|
let mut editor = Editor::new(Buffer::new(
|
||||||
&FONT_SYSTEM,
|
&FONT_SYSTEM,
|
||||||
FONT_SIZES[1 /* Body */],
|
FONT_SIZES[1 /* Body */],
|
||||||
);
|
));
|
||||||
update_attrs(&mut buffer, attrs);
|
update_attrs(&mut editor, attrs);
|
||||||
|
|
||||||
let mut window = Window {
|
let mut window = Window {
|
||||||
theme: Theme::Dark,
|
theme: Theme::Dark,
|
||||||
path_opt: None,
|
path_opt: None,
|
||||||
attrs,
|
attrs,
|
||||||
buffer: Mutex::new(buffer),
|
editor: Mutex::new(editor),
|
||||||
};
|
};
|
||||||
if let Some(arg) = env::args().nth(1) {
|
if let Some(arg) = env::args().nth(1) {
|
||||||
window.open(PathBuf::from(arg));
|
window.open(PathBuf::from(arg));
|
||||||
|
|
@ -151,9 +152,9 @@ impl Application for Window {
|
||||||
},
|
},
|
||||||
Message::Save => {
|
Message::Save => {
|
||||||
if let Some(path) = &self.path_opt {
|
if let Some(path) = &self.path_opt {
|
||||||
let buffer = self.buffer.lock().unwrap();
|
let editor = self.editor.lock().unwrap();
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
for line in buffer.lines.iter() {
|
for line in editor.buffer.lines.iter() {
|
||||||
text.push_str(line.text());
|
text.push_str(line.text());
|
||||||
text.push('\n');
|
text.push('\n');
|
||||||
}
|
}
|
||||||
|
|
@ -174,8 +175,8 @@ impl Application for Window {
|
||||||
cosmic_text::Weight::NORMAL
|
cosmic_text::Weight::NORMAL
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut buffer = self.buffer.lock().unwrap();
|
let mut editor = self.editor.lock().unwrap();
|
||||||
update_attrs(&mut buffer, self.attrs);
|
update_attrs(&mut editor, self.attrs);
|
||||||
},
|
},
|
||||||
Message::Italic(italic) => {
|
Message::Italic(italic) => {
|
||||||
self.attrs = self.attrs.style(if italic {
|
self.attrs = self.attrs.style(if italic {
|
||||||
|
|
@ -184,8 +185,8 @@ impl Application for Window {
|
||||||
cosmic_text::Style::Normal
|
cosmic_text::Style::Normal
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut buffer = self.buffer.lock().unwrap();
|
let mut editor = self.editor.lock().unwrap();
|
||||||
update_attrs(&mut buffer, self.attrs);
|
update_attrs(&mut editor, self.attrs);
|
||||||
},
|
},
|
||||||
Message::Monospaced(monospaced) => {
|
Message::Monospaced(monospaced) => {
|
||||||
self.attrs = self.attrs
|
self.attrs = self.attrs
|
||||||
|
|
@ -196,12 +197,12 @@ impl Application for Window {
|
||||||
})
|
})
|
||||||
.monospaced(monospaced);
|
.monospaced(monospaced);
|
||||||
|
|
||||||
let mut buffer = self.buffer.lock().unwrap();
|
let mut editor = self.editor.lock().unwrap();
|
||||||
update_attrs(&mut buffer, self.attrs);
|
update_attrs(&mut editor, self.attrs);
|
||||||
},
|
},
|
||||||
Message::MetricsChanged(metrics) => {
|
Message::MetricsChanged(metrics) => {
|
||||||
let mut buffer = self.buffer.lock().unwrap();
|
let mut editor = self.editor.lock().unwrap();
|
||||||
buffer.set_metrics(metrics);
|
editor.buffer.set_metrics(metrics);
|
||||||
},
|
},
|
||||||
Message::ThemeChanged(theme) => {
|
Message::ThemeChanged(theme) => {
|
||||||
self.theme = match theme {
|
self.theme = match theme {
|
||||||
|
|
@ -214,8 +215,8 @@ impl Application for Window {
|
||||||
let as_u8 = |component: f32| (component * 255.0) as u8;
|
let as_u8 = |component: f32| (component * 255.0) as u8;
|
||||||
self.attrs = self.attrs.color(cosmic_text::Color::rgba(as_u8(r), as_u8(g), as_u8(b), as_u8(a)));
|
self.attrs = self.attrs.color(cosmic_text::Color::rgba(as_u8(r), as_u8(g), as_u8(b), as_u8(a)));
|
||||||
|
|
||||||
let mut buffer = self.buffer.lock().unwrap();
|
let mut editor = self.editor.lock().unwrap();
|
||||||
update_attrs(&mut buffer, self.attrs);
|
update_attrs(&mut editor, self.attrs);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -234,10 +235,10 @@ impl Application for Window {
|
||||||
);
|
);
|
||||||
|
|
||||||
let font_size_picker = {
|
let font_size_picker = {
|
||||||
let buffer = self.buffer.lock().unwrap();
|
let editor = self.editor.lock().unwrap();
|
||||||
pick_list(
|
pick_list(
|
||||||
FONT_SIZES,
|
FONT_SIZES,
|
||||||
Some(buffer.metrics()),
|
Some(editor.buffer.metrics()),
|
||||||
Message::MetricsChanged
|
Message::MetricsChanged
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
@ -261,7 +262,7 @@ impl Application for Window {
|
||||||
.align_items(Alignment::Center)
|
.align_items(Alignment::Center)
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
,
|
,
|
||||||
text_box(&self.buffer)
|
text_box(&self.editor)
|
||||||
]
|
]
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
.padding(16)
|
.padding(16)
|
||||||
|
|
@ -272,8 +273,8 @@ impl Application for Window {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_attrs<'a>(buffer: &mut TextBuffer<'a>, attrs: Attrs<'a>) {
|
fn update_attrs<'a>(editor: &mut Editor<'a>, attrs: Attrs<'a>) {
|
||||||
buffer.lines.iter_mut().for_each(|line| {
|
editor.buffer.lines.iter_mut().for_each(|line| {
|
||||||
line.set_attrs_list(AttrsList::new(attrs));
|
line.set_attrs_list(AttrsList::new(attrs));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,10 @@ use cosmic::iced_native::{
|
||||||
widget::{self, tree, Widget},
|
widget::{self, tree, Widget},
|
||||||
};
|
};
|
||||||
use cosmic_text::{
|
use cosmic_text::{
|
||||||
|
Action,
|
||||||
|
Buffer,
|
||||||
|
Editor,
|
||||||
SwashCache,
|
SwashCache,
|
||||||
TextAction,
|
|
||||||
TextBuffer,
|
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
cmp,
|
cmp,
|
||||||
|
|
@ -47,19 +48,19 @@ impl StyleSheet for Theme {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TextBox<'a> {
|
pub struct TextBox<'a> {
|
||||||
buffer: &'a Mutex<TextBuffer<'static>>,
|
editor: &'a Mutex<Editor<'static>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TextBox<'a> {
|
impl<'a> TextBox<'a> {
|
||||||
pub fn new(buffer: &'a Mutex<TextBuffer<'static>>) -> Self {
|
pub fn new(editor: &'a Mutex<Editor<'static>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
buffer,
|
editor,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text_box<'a>(buffer: &'a Mutex<TextBuffer<'static>>) -> TextBox<'a> {
|
pub fn text_box<'a>(editor: &'a Mutex<Editor<'static>>) -> TextBox<'a> {
|
||||||
TextBox::new(buffer)
|
TextBox::new(editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message, Renderer> Widget<Message, Renderer> for TextBox<'a>
|
impl<'a, Message, Renderer> Widget<Message, Renderer> for TextBox<'a>
|
||||||
|
|
@ -141,28 +142,22 @@ where
|
||||||
|
|
||||||
let mut pixels_opt = state.pixels_opt.lock().unwrap();
|
let mut pixels_opt = state.pixels_opt.lock().unwrap();
|
||||||
|
|
||||||
let mut buffer = self.buffer.lock().unwrap();
|
let mut editor = self.editor.lock().unwrap();
|
||||||
|
|
||||||
let layout_w = layout.bounds().width as i32;
|
let layout_w = layout.bounds().width as i32;
|
||||||
let layout_h = layout.bounds().height as i32;
|
let layout_h = layout.bounds().height as i32;
|
||||||
buffer.set_size(layout_w, layout_h);
|
editor.buffer.set_size(layout_w, layout_h);
|
||||||
|
|
||||||
if buffer.cursor_moved {
|
|
||||||
buffer.shape_until_cursor();
|
|
||||||
buffer.cursor_moved = false;
|
|
||||||
} else {
|
|
||||||
buffer.shape_until_scroll();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
editor.shape_as_needed();
|
||||||
//TODO: redraw on color change
|
//TODO: redraw on color change
|
||||||
if buffer.redraw || pixels_opt.is_none() {
|
if editor.buffer.redraw || pixels_opt.is_none() {
|
||||||
// Redraw buffer to image
|
// Redraw buffer to image
|
||||||
|
|
||||||
let instant = Instant::now();
|
let instant = Instant::now();
|
||||||
|
|
||||||
let mut pixels = vec![0; layout_w as usize * layout_h as usize * 4];
|
let mut pixels = vec![0; layout_w as usize * layout_h as usize * 4];
|
||||||
|
|
||||||
buffer.draw(&mut state.cache.lock().unwrap(), text_color, |start_x, start_y, w, h, color| {
|
editor.draw(&mut state.cache.lock().unwrap(), text_color, |start_x, start_y, w, h, color| {
|
||||||
let alpha = (color.0 >> 24) & 0xFF;
|
let alpha = (color.0 >> 24) & 0xFF;
|
||||||
if alpha == 0 {
|
if alpha == 0 {
|
||||||
// Do not draw if alpha is zero
|
// Do not draw if alpha is zero
|
||||||
|
|
@ -212,7 +207,7 @@ where
|
||||||
|
|
||||||
*pixels_opt = Some((layout_w as u32, layout_h as u32, pixels));
|
*pixels_opt = Some((layout_w as u32, layout_h as u32, pixels));
|
||||||
|
|
||||||
buffer.redraw = false;
|
editor.buffer.redraw = false;
|
||||||
|
|
||||||
let duration = instant.elapsed();
|
let duration = instant.elapsed();
|
||||||
log::debug!("redraw: {:?}", duration);
|
log::debug!("redraw: {:?}", duration);
|
||||||
|
|
@ -235,64 +230,64 @@ where
|
||||||
_shell: &mut Shell<'_, Message>,
|
_shell: &mut Shell<'_, Message>,
|
||||||
) -> Status {
|
) -> Status {
|
||||||
let state = tree.state.downcast_mut::<State>();
|
let state = tree.state.downcast_mut::<State>();
|
||||||
let mut buffer = self.buffer.lock().unwrap();
|
let mut editor = self.editor.lock().unwrap();
|
||||||
|
|
||||||
let mut status = Status::Ignored;
|
let mut status = Status::Ignored;
|
||||||
match event {
|
match event {
|
||||||
Event::Keyboard(KeyEvent::KeyPressed { key_code, modifiers }) => match key_code {
|
Event::Keyboard(KeyEvent::KeyPressed { key_code, modifiers }) => match key_code {
|
||||||
KeyCode::Left => {
|
KeyCode::Left => {
|
||||||
buffer.action(TextAction::Left);
|
editor.action(Action::Left);
|
||||||
status = Status::Captured;
|
status = Status::Captured;
|
||||||
},
|
},
|
||||||
KeyCode::Right => {
|
KeyCode::Right => {
|
||||||
buffer.action(TextAction::Right);
|
editor.action(Action::Right);
|
||||||
status = Status::Captured;
|
status = Status::Captured;
|
||||||
},
|
},
|
||||||
KeyCode::Up => {
|
KeyCode::Up => {
|
||||||
buffer.action(TextAction::Up);
|
editor.action(Action::Up);
|
||||||
status = Status::Captured;
|
status = Status::Captured;
|
||||||
},
|
},
|
||||||
KeyCode::Down => {
|
KeyCode::Down => {
|
||||||
buffer.action(TextAction::Down);
|
editor.action(Action::Down);
|
||||||
status = Status::Captured;
|
status = Status::Captured;
|
||||||
},
|
},
|
||||||
KeyCode::Home => {
|
KeyCode::Home => {
|
||||||
buffer.action(TextAction::Home);
|
editor.action(Action::Home);
|
||||||
status = Status::Captured;
|
status = Status::Captured;
|
||||||
},
|
},
|
||||||
KeyCode::End => {
|
KeyCode::End => {
|
||||||
buffer.action(TextAction::End);
|
editor.action(Action::End);
|
||||||
status = Status::Captured;
|
status = Status::Captured;
|
||||||
},
|
},
|
||||||
KeyCode::PageUp => {
|
KeyCode::PageUp => {
|
||||||
buffer.action(TextAction::PageUp);
|
editor.action(Action::PageUp);
|
||||||
status = Status::Captured;
|
status = Status::Captured;
|
||||||
},
|
},
|
||||||
KeyCode::PageDown => {
|
KeyCode::PageDown => {
|
||||||
buffer.action(TextAction::PageDown);
|
editor.action(Action::PageDown);
|
||||||
status = Status::Captured;
|
status = Status::Captured;
|
||||||
},
|
},
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
buffer.action(TextAction::Enter);
|
editor.action(Action::Enter);
|
||||||
status = Status::Captured;
|
status = Status::Captured;
|
||||||
},
|
},
|
||||||
KeyCode::Backspace => {
|
KeyCode::Backspace => {
|
||||||
buffer.action(TextAction::Backspace);
|
editor.action(Action::Backspace);
|
||||||
status = Status::Captured;
|
status = Status::Captured;
|
||||||
},
|
},
|
||||||
KeyCode::Delete => {
|
KeyCode::Delete => {
|
||||||
buffer.action(TextAction::Delete);
|
editor.action(Action::Delete);
|
||||||
status = Status::Captured;
|
status = Status::Captured;
|
||||||
},
|
},
|
||||||
_ => ()
|
_ => ()
|
||||||
},
|
},
|
||||||
Event::Keyboard(KeyEvent::CharacterReceived(character)) => {
|
Event::Keyboard(KeyEvent::CharacterReceived(character)) => {
|
||||||
buffer.action(TextAction::Insert(character));
|
editor.action(Action::Insert(character));
|
||||||
status = Status::Captured;
|
status = Status::Captured;
|
||||||
},
|
},
|
||||||
Event::Mouse(MouseEvent::ButtonPressed(Button::Left)) => {
|
Event::Mouse(MouseEvent::ButtonPressed(Button::Left)) => {
|
||||||
if layout.bounds().contains(cursor_position) {
|
if layout.bounds().contains(cursor_position) {
|
||||||
buffer.action(TextAction::Click {
|
editor.action(Action::Click {
|
||||||
x: (cursor_position.x - layout.bounds().x) as i32,
|
x: (cursor_position.x - layout.bounds().x) as i32,
|
||||||
y: (cursor_position.y - layout.bounds().y) as i32,
|
y: (cursor_position.y - layout.bounds().y) as i32,
|
||||||
});
|
});
|
||||||
|
|
@ -306,7 +301,7 @@ where
|
||||||
},
|
},
|
||||||
Event::Mouse(MouseEvent::CursorMoved { .. }) => {
|
Event::Mouse(MouseEvent::CursorMoved { .. }) => {
|
||||||
if state.is_dragging {
|
if state.is_dragging {
|
||||||
buffer.action(TextAction::Drag {
|
editor.action(Action::Drag {
|
||||||
x: (cursor_position.x - layout.bounds().x) as i32,
|
x: (cursor_position.x - layout.bounds().x) as i32,
|
||||||
y: (cursor_position.y - layout.bounds().y) as i32,
|
y: (cursor_position.y - layout.bounds().y) as i32,
|
||||||
});
|
});
|
||||||
|
|
@ -315,7 +310,7 @@ where
|
||||||
},
|
},
|
||||||
Event::Mouse(MouseEvent::WheelScrolled { delta }) => match delta {
|
Event::Mouse(MouseEvent::WheelScrolled { delta }) => match delta {
|
||||||
ScrollDelta::Lines { x, y } => {
|
ScrollDelta::Lines { x, y } => {
|
||||||
buffer.action(TextAction::Scroll {
|
editor.action(Action::Scroll {
|
||||||
lines: (-y * 6.0) as i32,
|
lines: (-y * 6.0) as i32,
|
||||||
});
|
});
|
||||||
status = Status::Captured;
|
status = Status::Captured;
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ use cosmic_text::{
|
||||||
Attrs,
|
Attrs,
|
||||||
AttrsList,
|
AttrsList,
|
||||||
SwashCache,
|
SwashCache,
|
||||||
TextBufferLine,
|
BufferLine,
|
||||||
TextMetrics,
|
Metrics,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
cmp,
|
cmp,
|
||||||
|
|
@ -44,8 +44,8 @@ impl StyleSheet for Theme {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Text {
|
pub struct Text {
|
||||||
line: TextBufferLine<'static>,
|
line: BufferLine<'static>,
|
||||||
metrics: TextMetrics,
|
metrics: Metrics,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Text {
|
impl Text {
|
||||||
|
|
@ -53,7 +53,7 @@ impl Text {
|
||||||
let instant = Instant::now();
|
let instant = Instant::now();
|
||||||
|
|
||||||
//TODO: make it possible to set attrs
|
//TODO: make it possible to set attrs
|
||||||
let mut line = TextBufferLine::new(
|
let mut line = BufferLine::new(
|
||||||
string,
|
string,
|
||||||
AttrsList::new(Attrs::new())
|
AttrsList::new(Attrs::new())
|
||||||
);
|
);
|
||||||
|
|
@ -63,7 +63,7 @@ impl Text {
|
||||||
|
|
||||||
let text = Self {
|
let text = Self {
|
||||||
line,
|
line,
|
||||||
metrics: TextMetrics::new(14, 20),
|
metrics: Metrics::new(14, 20),
|
||||||
};
|
};
|
||||||
|
|
||||||
log::debug!("Text::new in {:?}", instant.elapsed());
|
log::debug!("Text::new in {:?}", instant.elapsed());
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,15 @@
|
||||||
use cosmic_text::{
|
use cosmic_text::{
|
||||||
Attrs,
|
Attrs,
|
||||||
AttrsList,
|
AttrsList,
|
||||||
|
Buffer,
|
||||||
Color,
|
Color,
|
||||||
|
Editor,
|
||||||
Family,
|
Family,
|
||||||
FontSystem,
|
FontSystem,
|
||||||
|
Metrics,
|
||||||
Style,
|
Style,
|
||||||
SwashCache,
|
SwashCache,
|
||||||
TextAction,
|
Action,
|
||||||
TextBuffer,
|
|
||||||
TextMetrics,
|
|
||||||
Weight
|
Weight
|
||||||
};
|
};
|
||||||
use orbclient::{EventOption, Renderer, Window, WindowFlag};
|
use orbclient::{EventOption, Renderer, Window, WindowFlag};
|
||||||
|
|
@ -67,23 +68,23 @@ fn main() {
|
||||||
let font_system = FontSystem::new();
|
let font_system = FontSystem::new();
|
||||||
|
|
||||||
let font_sizes = [
|
let font_sizes = [
|
||||||
TextMetrics::new(10, 14).scale(display_scale), // Caption
|
Metrics::new(10, 14).scale(display_scale), // Caption
|
||||||
TextMetrics::new(14, 20).scale(display_scale), // Body
|
Metrics::new(14, 20).scale(display_scale), // Body
|
||||||
TextMetrics::new(20, 28).scale(display_scale), // Title 4
|
Metrics::new(20, 28).scale(display_scale), // Title 4
|
||||||
TextMetrics::new(24, 32).scale(display_scale), // Title 3
|
Metrics::new(24, 32).scale(display_scale), // Title 3
|
||||||
TextMetrics::new(28, 36).scale(display_scale), // Title 2
|
Metrics::new(28, 36).scale(display_scale), // Title 2
|
||||||
TextMetrics::new(32, 44).scale(display_scale), // Title 1
|
Metrics::new(32, 44).scale(display_scale), // Title 1
|
||||||
];
|
];
|
||||||
let font_size_default = 1; // Body
|
let font_size_default = 1; // Body
|
||||||
let mut font_size_i = font_size_default;
|
let mut font_size_i = font_size_default;
|
||||||
|
|
||||||
let line_x = 8 * display_scale;
|
let line_x = 8 * display_scale;
|
||||||
let mut buffer = TextBuffer::new(
|
let mut editor = Editor::new(Buffer::new(
|
||||||
&font_system,
|
&font_system,
|
||||||
font_sizes[font_size_i]
|
font_sizes[font_size_i]
|
||||||
);
|
));
|
||||||
|
|
||||||
buffer.set_size(
|
editor.buffer.set_size(
|
||||||
window.width() as i32 - line_x * 2,
|
window.width() as i32 - line_x * 2,
|
||||||
window.height() as i32
|
window.height() as i32
|
||||||
);
|
);
|
||||||
|
|
@ -91,7 +92,7 @@ fn main() {
|
||||||
let attrs = Attrs::new()
|
let attrs = Attrs::new()
|
||||||
.monospaced(true)
|
.monospaced(true)
|
||||||
.family(Family::Monospace);
|
.family(Family::Monospace);
|
||||||
buffer.set_text(&text, attrs);
|
editor.buffer.set_text(&text, attrs);
|
||||||
|
|
||||||
let mut bg_color = orbclient::Color::rgb(0x00, 0x00, 0x00);
|
let mut bg_color = orbclient::Color::rgb(0x00, 0x00, 0x00);
|
||||||
let mut font_color = Color::rgb(0xFF, 0xFF, 0xFF);
|
let mut font_color = Color::rgb(0xFF, 0xFF, 0xFF);
|
||||||
|
|
@ -149,8 +150,8 @@ fn main() {
|
||||||
if rehighlight {
|
if rehighlight {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
|
|
||||||
for line_i in 0..buffer.lines.len() {
|
for line_i in 0..editor.buffer.lines.len() {
|
||||||
let line = &mut buffer.lines[line_i];
|
let line = &mut editor.buffer.lines[line_i];
|
||||||
if ! line.is_reset() && line_i < syntax_cache.len() {
|
if ! line.is_reset() && line_i < syntax_cache.len() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -209,8 +210,8 @@ fn main() {
|
||||||
if line_i < syntax_cache.len() {
|
if line_i < syntax_cache.len() {
|
||||||
if syntax_cache[line_i] != cache_item {
|
if syntax_cache[line_i] != cache_item {
|
||||||
syntax_cache[line_i] = cache_item;
|
syntax_cache[line_i] = cache_item;
|
||||||
if line_i + 1 < buffer.lines.len() {
|
if line_i + 1 < editor.buffer.lines.len() {
|
||||||
buffer.lines[line_i + 1].reset();
|
editor.buffer.lines[line_i + 1].reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -218,25 +219,19 @@ fn main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer.redraw = true;
|
editor.buffer.redraw = true;
|
||||||
rehighlight = false;
|
rehighlight = false;
|
||||||
|
|
||||||
log::info!("Syntax highlighted in {:?}", now.elapsed());
|
log::info!("Syntax highlighted in {:?}", now.elapsed());
|
||||||
}
|
}
|
||||||
|
|
||||||
if buffer.cursor_moved {
|
editor.shape_as_needed();
|
||||||
buffer.shape_until_cursor();
|
if editor.buffer.redraw {
|
||||||
buffer.cursor_moved = false;
|
|
||||||
} else {
|
|
||||||
buffer.shape_until_scroll();
|
|
||||||
}
|
|
||||||
|
|
||||||
if buffer.redraw {
|
|
||||||
let instant = Instant::now();
|
let instant = Instant::now();
|
||||||
|
|
||||||
window.set(bg_color);
|
window.set(bg_color);
|
||||||
|
|
||||||
buffer.draw(&mut swash_cache, font_color, |x, y, w, h, color| {
|
editor.draw(&mut swash_cache, font_color, |x, y, w, h, color| {
|
||||||
window.rect(line_x + x, y, w, h, orbclient::Color { data: color.0 })
|
window.rect(line_x + x, y, w, h, orbclient::Color { data: color.0 })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -244,7 +239,7 @@ fn main() {
|
||||||
{
|
{
|
||||||
let mut start_line_opt = None;
|
let mut start_line_opt = None;
|
||||||
let mut end_line = 0;
|
let mut end_line = 0;
|
||||||
for run in buffer.layout_runs() {
|
for run in editor.buffer.layout_runs() {
|
||||||
end_line = run.line_i;
|
end_line = run.line_i;
|
||||||
if start_line_opt == None {
|
if start_line_opt == None {
|
||||||
start_line_opt = Some(end_line);
|
start_line_opt = Some(end_line);
|
||||||
|
|
@ -252,7 +247,7 @@ fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let start_line = start_line_opt.unwrap_or(end_line);
|
let start_line = start_line_opt.unwrap_or(end_line);
|
||||||
let lines = buffer.lines.len();
|
let lines = editor.buffer.lines.len();
|
||||||
let start_y = (start_line * window.height() as usize) / lines;
|
let start_y = (start_line * window.height() as usize) / lines;
|
||||||
let end_y = (end_line * window.height() as usize) / lines;
|
let end_y = (end_line * window.height() as usize) / lines;
|
||||||
if end_y > start_y {
|
if end_y > start_y {
|
||||||
|
|
@ -268,7 +263,7 @@ fn main() {
|
||||||
|
|
||||||
window.sync();
|
window.sync();
|
||||||
|
|
||||||
buffer.redraw = false;
|
editor.buffer.redraw = false;
|
||||||
|
|
||||||
let duration = instant.elapsed();
|
let duration = instant.elapsed();
|
||||||
log::debug!("redraw: {:?}", duration);
|
log::debug!("redraw: {:?}", duration);
|
||||||
|
|
@ -282,62 +277,62 @@ fn main() {
|
||||||
match event.to_option() {
|
match event.to_option() {
|
||||||
EventOption::Key(event) => match event.scancode {
|
EventOption::Key(event) => match event.scancode {
|
||||||
orbclient::K_CTRL => ctrl_pressed = event.pressed,
|
orbclient::K_CTRL => ctrl_pressed = event.pressed,
|
||||||
orbclient::K_LEFT if event.pressed => buffer.action(TextAction::Left),
|
orbclient::K_LEFT if event.pressed => editor.action(Action::Left),
|
||||||
orbclient::K_RIGHT if event.pressed => buffer.action(TextAction::Right),
|
orbclient::K_RIGHT if event.pressed => editor.action(Action::Right),
|
||||||
orbclient::K_UP if event.pressed => buffer.action(TextAction::Up),
|
orbclient::K_UP if event.pressed => editor.action(Action::Up),
|
||||||
orbclient::K_DOWN if event.pressed => buffer.action(TextAction::Down),
|
orbclient::K_DOWN if event.pressed => editor.action(Action::Down),
|
||||||
orbclient::K_HOME if event.pressed => buffer.action(TextAction::Home),
|
orbclient::K_HOME if event.pressed => editor.action(Action::Home),
|
||||||
orbclient::K_END if event.pressed => buffer.action(TextAction::End),
|
orbclient::K_END if event.pressed => editor.action(Action::End),
|
||||||
orbclient::K_PGUP if event.pressed => buffer.action(TextAction::PageUp),
|
orbclient::K_PGUP if event.pressed => editor.action(Action::PageUp),
|
||||||
orbclient::K_PGDN if event.pressed => buffer.action(TextAction::PageDown),
|
orbclient::K_PGDN if event.pressed => editor.action(Action::PageDown),
|
||||||
orbclient::K_ENTER if event.pressed => {
|
orbclient::K_ENTER if event.pressed => {
|
||||||
buffer.action(TextAction::Enter);
|
editor.action(Action::Enter);
|
||||||
rehighlight = true;
|
rehighlight = true;
|
||||||
},
|
},
|
||||||
orbclient::K_BKSP if event.pressed => {
|
orbclient::K_BKSP if event.pressed => {
|
||||||
buffer.action(TextAction::Backspace);
|
editor.action(Action::Backspace);
|
||||||
rehighlight = true;
|
rehighlight = true;
|
||||||
},
|
},
|
||||||
orbclient::K_DEL if event.pressed => {
|
orbclient::K_DEL if event.pressed => {
|
||||||
buffer.action(TextAction::Delete);
|
editor.action(Action::Delete);
|
||||||
rehighlight = true;
|
rehighlight = true;
|
||||||
},
|
},
|
||||||
orbclient::K_0 if event.pressed && ctrl_pressed => {
|
orbclient::K_0 if event.pressed && ctrl_pressed => {
|
||||||
font_size_i = font_size_default;
|
font_size_i = font_size_default;
|
||||||
buffer.set_metrics(font_sizes[font_size_i]);
|
editor.buffer.set_metrics(font_sizes[font_size_i]);
|
||||||
}
|
}
|
||||||
orbclient::K_MINUS if event.pressed && ctrl_pressed => {
|
orbclient::K_MINUS if event.pressed && ctrl_pressed => {
|
||||||
if font_size_i > 0 {
|
if font_size_i > 0 {
|
||||||
font_size_i -= 1;
|
font_size_i -= 1;
|
||||||
buffer.set_metrics(font_sizes[font_size_i]);
|
editor.buffer.set_metrics(font_sizes[font_size_i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
orbclient::K_EQUALS if event.pressed && ctrl_pressed => {
|
orbclient::K_EQUALS if event.pressed && ctrl_pressed => {
|
||||||
if font_size_i + 1 < font_sizes.len() {
|
if font_size_i + 1 < font_sizes.len() {
|
||||||
font_size_i += 1;
|
font_size_i += 1;
|
||||||
buffer.set_metrics(font_sizes[font_size_i]);
|
editor.buffer.set_metrics(font_sizes[font_size_i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
},
|
},
|
||||||
EventOption::TextInput(event) if !ctrl_pressed => {
|
EventOption::TextInput(event) if !ctrl_pressed => {
|
||||||
buffer.action(TextAction::Insert(event.character));
|
editor.action(Action::Insert(event.character));
|
||||||
rehighlight = true;
|
rehighlight = true;
|
||||||
}
|
}
|
||||||
EventOption::Mouse(event) => {
|
EventOption::Mouse(event) => {
|
||||||
mouse_x = event.x;
|
mouse_x = event.x;
|
||||||
mouse_y = event.y;
|
mouse_y = event.y;
|
||||||
if mouse_left {
|
if mouse_left {
|
||||||
buffer.action(TextAction::Drag {
|
editor.action(Action::Drag {
|
||||||
x: mouse_x - line_x,
|
x: mouse_x - line_x,
|
||||||
y: mouse_y,
|
y: mouse_y,
|
||||||
});
|
});
|
||||||
|
|
||||||
if mouse_y <= 5 {
|
if mouse_y <= 5 {
|
||||||
buffer.action(TextAction::Scroll { lines: -3 });
|
editor.action(Action::Scroll { lines: -3 });
|
||||||
window_async = true;
|
window_async = true;
|
||||||
} else if mouse_y + 5 >= window.height() as i32 {
|
} else if mouse_y + 5 >= window.height() as i32 {
|
||||||
buffer.action(TextAction::Scroll { lines: 3 });
|
editor.action(Action::Scroll { lines: 3 });
|
||||||
window_async = true;
|
window_async = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -348,7 +343,7 @@ fn main() {
|
||||||
if event.left != mouse_left {
|
if event.left != mouse_left {
|
||||||
mouse_left = event.left;
|
mouse_left = event.left;
|
||||||
if mouse_left {
|
if mouse_left {
|
||||||
buffer.action(TextAction::Click {
|
editor.action(Action::Click {
|
||||||
x: mouse_x - line_x,
|
x: mouse_x - line_x,
|
||||||
y: mouse_y,
|
y: mouse_y,
|
||||||
});
|
});
|
||||||
|
|
@ -357,11 +352,10 @@ fn main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EventOption::Resize(event) => {
|
EventOption::Resize(event) => {
|
||||||
buffer.set_size(event.width as i32 - line_x * 2, event.height as i32);
|
editor.buffer.set_size(event.width as i32 - line_x * 2, event.height as i32);
|
||||||
buffer.redraw = true;
|
|
||||||
}
|
}
|
||||||
EventOption::Scroll(event) => {
|
EventOption::Scroll(event) => {
|
||||||
buffer.action(TextAction::Scroll {
|
editor.action(Action::Scroll {
|
||||||
lines: -event.y * 3,
|
lines: -event.y * 3,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -371,16 +365,16 @@ fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if mouse_left && force_drag {
|
if mouse_left && force_drag {
|
||||||
buffer.action(TextAction::Drag {
|
editor.action(Action::Drag {
|
||||||
x: mouse_x - line_x,
|
x: mouse_x - line_x,
|
||||||
y: mouse_y,
|
y: mouse_y,
|
||||||
});
|
});
|
||||||
|
|
||||||
if mouse_y <= 5 {
|
if mouse_y <= 5 {
|
||||||
buffer.action(TextAction::Scroll { lines: -3 });
|
editor.action(Action::Scroll { lines: -3 });
|
||||||
window_async = true;
|
window_async = true;
|
||||||
} else if mouse_y + 5 >= window.height() as i32 {
|
} else if mouse_y + 5 >= window.height() as i32 {
|
||||||
buffer.action(TextAction::Scroll { lines: 3 });
|
editor.action(Action::Scroll { lines: 3 });
|
||||||
window_async = true;
|
window_async = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,27 @@
|
||||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
use cosmic_text::{Color, FontSystem, SwashCache, TextAction, TextBuffer, TextMetrics};
|
use cosmic_text::{Action, Buffer, Color, Editor, FontSystem, Metrics, SwashCache};
|
||||||
use orbclient::{EventOption, Renderer, Window, WindowFlag};
|
use orbclient::{EventOption, Renderer, Window, WindowFlag};
|
||||||
use std::{env, fs, process, thread, time::{Duration, Instant}};
|
use std::{env, fs, process, thread, time::{Duration, Instant}};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
fn redraw(window: &mut Window, buffer: &mut TextBuffer<'_>, swash_cache: &mut SwashCache) {
|
fn redraw(window: &mut Window, editor: &mut Editor<'_>, swash_cache: &mut SwashCache) {
|
||||||
let bg_color = orbclient::Color::rgb(0x34, 0x34, 0x34);
|
let bg_color = orbclient::Color::rgb(0x34, 0x34, 0x34);
|
||||||
let font_color = Color::rgb(0xFF, 0xFF, 0xFF);
|
let font_color = Color::rgb(0xFF, 0xFF, 0xFF);
|
||||||
|
|
||||||
if buffer.cursor_moved {
|
editor.shape_as_needed();
|
||||||
buffer.shape_until_cursor();
|
if editor.buffer.redraw {
|
||||||
buffer.cursor_moved = false;
|
|
||||||
} else {
|
|
||||||
buffer.shape_until_scroll();
|
|
||||||
}
|
|
||||||
|
|
||||||
if buffer.redraw {
|
|
||||||
let instant = Instant::now();
|
let instant = Instant::now();
|
||||||
|
|
||||||
window.set(bg_color);
|
window.set(bg_color);
|
||||||
|
|
||||||
buffer.draw(swash_cache, font_color, |x, y, w, h, color| {
|
editor.draw(swash_cache, font_color, |x, y, w, h, color| {
|
||||||
window.rect(x, y, w, h, orbclient::Color { data: color.0 });
|
window.rect(x, y, w, h, orbclient::Color { data: color.0 });
|
||||||
});
|
});
|
||||||
|
|
||||||
window.sync();
|
window.sync();
|
||||||
|
|
||||||
buffer.redraw = false;
|
editor.buffer.redraw = false;
|
||||||
|
|
||||||
let duration = instant.elapsed();
|
let duration = instant.elapsed();
|
||||||
log::debug!("redraw: {:?}", duration);
|
log::debug!("redraw: {:?}", duration);
|
||||||
|
|
@ -51,16 +45,16 @@ fn main() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let font_sizes = [
|
let font_sizes = [
|
||||||
TextMetrics::new(10, 14).scale(display_scale), // Caption
|
Metrics::new(10, 14).scale(display_scale), // Caption
|
||||||
TextMetrics::new(14, 20).scale(display_scale), // Body
|
Metrics::new(14, 20).scale(display_scale), // Body
|
||||||
TextMetrics::new(20, 28).scale(display_scale), // Title 4
|
Metrics::new(20, 28).scale(display_scale), // Title 4
|
||||||
TextMetrics::new(24, 32).scale(display_scale), // Title 3
|
Metrics::new(24, 32).scale(display_scale), // Title 3
|
||||||
TextMetrics::new(28, 36).scale(display_scale), // Title 2
|
Metrics::new(28, 36).scale(display_scale), // Title 2
|
||||||
TextMetrics::new(32, 44).scale(display_scale), // Title 1
|
Metrics::new(32, 44).scale(display_scale), // Title 1
|
||||||
];
|
];
|
||||||
let font_size_default = 1; // Body
|
let font_size_default = 1; // Body
|
||||||
|
|
||||||
let mut buffer = TextBuffer::new(
|
let mut buffer = Buffer::new(
|
||||||
&font_system,
|
&font_system,
|
||||||
font_sizes[font_size_default]
|
font_sizes[font_size_default]
|
||||||
);
|
);
|
||||||
|
|
@ -69,6 +63,8 @@ fn main() {
|
||||||
window.height() as i32
|
window.height() as i32
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let mut editor = Editor::new(buffer);
|
||||||
|
|
||||||
let mut swash_cache = SwashCache::new(&font_system);
|
let mut swash_cache = SwashCache::new(&font_system);
|
||||||
|
|
||||||
let text = if let Some(arg) = env::args().nth(1) {
|
let text = if let Some(arg) = env::args().nth(1) {
|
||||||
|
|
@ -93,49 +89,49 @@ fn main() {
|
||||||
|
|
||||||
// Test backspace of character
|
// Test backspace of character
|
||||||
{
|
{
|
||||||
let cursor = buffer.cursor();
|
let cursor = editor.cursor();
|
||||||
buffer.action(TextAction::Insert(c));
|
editor.action(Action::Insert(c));
|
||||||
buffer.action(TextAction::Backspace);
|
editor.action(Action::Backspace);
|
||||||
assert_eq!(cursor, buffer.cursor());
|
assert_eq!(cursor, editor.cursor());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, normal insert of character
|
// Finally, normal insert of character
|
||||||
buffer.action(TextAction::Insert(c));
|
editor.action(Action::Insert(c));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test delete of EGC
|
// Test delete of EGC
|
||||||
{
|
{
|
||||||
let cursor = buffer.cursor();
|
let cursor = editor.cursor();
|
||||||
buffer.action(TextAction::Previous);
|
editor.action(Action::Previous);
|
||||||
buffer.action(TextAction::Delete);
|
editor.action(Action::Delete);
|
||||||
for c in grapheme.chars() {
|
for c in grapheme.chars() {
|
||||||
buffer.action(TextAction::Insert(c));
|
editor.action(Action::Insert(c));
|
||||||
}
|
}
|
||||||
assert_eq!(cursor, buffer.cursor());
|
assert_eq!(cursor, editor.cursor());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test backspace of newline
|
// Test backspace of newline
|
||||||
{
|
{
|
||||||
let cursor = buffer.cursor();
|
let cursor = editor.cursor();
|
||||||
buffer.action(TextAction::Enter);
|
editor.action(Action::Enter);
|
||||||
buffer.action(TextAction::Backspace);
|
editor.action(Action::Backspace);
|
||||||
assert_eq!(cursor, buffer.cursor());
|
assert_eq!(cursor, editor.cursor());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test delete of newline
|
// Test delete of newline
|
||||||
{
|
{
|
||||||
let cursor = buffer.cursor();
|
let cursor = editor.cursor();
|
||||||
buffer.action(TextAction::Enter);
|
editor.action(Action::Enter);
|
||||||
buffer.action(TextAction::Previous);
|
editor.action(Action::Previous);
|
||||||
buffer.action(TextAction::Delete);
|
editor.action(Action::Delete);
|
||||||
assert_eq!(cursor, buffer.cursor());
|
assert_eq!(cursor, editor.cursor());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, normal enter
|
// Finally, normal enter
|
||||||
buffer.action(TextAction::Enter);
|
editor.action(Action::Enter);
|
||||||
|
|
||||||
redraw(&mut window, &mut buffer, &mut swash_cache);
|
redraw(&mut window, &mut editor, &mut swash_cache);
|
||||||
|
|
||||||
for event in window.events() {
|
for event in window.events() {
|
||||||
match event.to_option() {
|
match event.to_option() {
|
||||||
|
|
@ -150,7 +146,7 @@ fn main() {
|
||||||
|
|
||||||
let mut wrong = 0;
|
let mut wrong = 0;
|
||||||
for (line_i, line) in text.lines().enumerate() {
|
for (line_i, line) in text.lines().enumerate() {
|
||||||
let buffer_line = &buffer.lines[line_i];
|
let buffer_line = &editor.buffer.lines[line_i];
|
||||||
if buffer_line.text() != line {
|
if buffer_line.text() != line {
|
||||||
log::error!("line {}: {:?} != {:?}", line_i, buffer_line.text(), line);
|
log::error!("line {}: {:?} != {:?}", line_i, buffer_line.text(), line);
|
||||||
wrong += 1;
|
wrong += 1;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,20 @@
|
||||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
use cosmic_text::{Attrs, AttrsList, Color, Family, FontSystem, Style, SwashCache,
|
use cosmic_text::{
|
||||||
TextAction, TextBuffer, TextBufferLine, TextMetrics, Weight};
|
Action,
|
||||||
|
Attrs,
|
||||||
|
AttrsList,
|
||||||
|
Buffer,
|
||||||
|
BufferLine,
|
||||||
|
Color,
|
||||||
|
Editor,
|
||||||
|
Family,
|
||||||
|
FontSystem,
|
||||||
|
Metrics,
|
||||||
|
Style,
|
||||||
|
SwashCache,
|
||||||
|
Weight,
|
||||||
|
};
|
||||||
use orbclient::{EventOption, Renderer, Window, WindowFlag};
|
use orbclient::{EventOption, Renderer, Window, WindowFlag};
|
||||||
use std::{process, thread, time::{Duration, Instant}};
|
use std::{process, thread, time::{Duration, Instant}};
|
||||||
|
|
||||||
|
|
@ -31,12 +44,12 @@ fn main() {
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut buffer = TextBuffer::new(
|
let mut editor = Editor::new(Buffer::new(
|
||||||
&font_system,
|
&font_system,
|
||||||
TextMetrics::new(32, 44).scale(display_scale)
|
Metrics::new(32, 44).scale(display_scale)
|
||||||
);
|
));
|
||||||
|
|
||||||
buffer.set_size(
|
editor.buffer.set_size(
|
||||||
window.width() as i32,
|
window.width() as i32,
|
||||||
window.height() as i32
|
window.height() as i32
|
||||||
);
|
);
|
||||||
|
|
@ -46,7 +59,7 @@ fn main() {
|
||||||
let mono_attrs = attrs.monospaced(true).family(Family::Monospace);
|
let mono_attrs = attrs.monospaced(true).family(Family::Monospace);
|
||||||
let comic_attrs = attrs.family(Family::Name("Comic Neue"));
|
let comic_attrs = attrs.family(Family::Name("Comic Neue"));
|
||||||
|
|
||||||
buffer.lines.clear();
|
editor.buffer.lines.clear();
|
||||||
|
|
||||||
let lines: &[&[(&str, Attrs)]] = &[
|
let lines: &[&[(&str, Attrs)]] = &[
|
||||||
&[
|
&[
|
||||||
|
|
@ -118,7 +131,7 @@ fn main() {
|
||||||
let end = line_text.len();
|
let end = line_text.len();
|
||||||
attrs_list.add_span(start..end, attrs);
|
attrs_list.add_span(start..end, attrs);
|
||||||
}
|
}
|
||||||
buffer.lines.push(TextBufferLine::new(line_text, attrs_list));
|
editor.buffer.lines.push(BufferLine::new(line_text, attrs_list));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut swash_cache = SwashCache::new(&font_system);
|
let mut swash_cache = SwashCache::new(&font_system);
|
||||||
|
|
@ -131,25 +144,19 @@ fn main() {
|
||||||
let bg_color = orbclient::Color::rgb(0x34, 0x34, 0x34);
|
let bg_color = orbclient::Color::rgb(0x34, 0x34, 0x34);
|
||||||
let font_color = Color::rgb(0xFF, 0xFF, 0xFF);
|
let font_color = Color::rgb(0xFF, 0xFF, 0xFF);
|
||||||
|
|
||||||
if buffer.cursor_moved {
|
editor.shape_as_needed();
|
||||||
buffer.shape_until_cursor();
|
if editor.buffer.redraw {
|
||||||
buffer.cursor_moved = false;
|
|
||||||
} else {
|
|
||||||
buffer.shape_until_scroll();
|
|
||||||
}
|
|
||||||
|
|
||||||
if buffer.redraw {
|
|
||||||
let instant = Instant::now();
|
let instant = Instant::now();
|
||||||
|
|
||||||
window.set(bg_color);
|
window.set(bg_color);
|
||||||
|
|
||||||
buffer.draw(&mut swash_cache, font_color, |x, y, w, h, color| {
|
editor.draw(&mut swash_cache, font_color, |x, y, w, h, color| {
|
||||||
window.rect(x, y, w, h, orbclient::Color { data: color.0 });
|
window.rect(x, y, w, h, orbclient::Color { data: color.0 });
|
||||||
});
|
});
|
||||||
|
|
||||||
window.sync();
|
window.sync();
|
||||||
|
|
||||||
buffer.redraw = false;
|
editor.buffer.redraw = false;
|
||||||
|
|
||||||
let duration = instant.elapsed();
|
let duration = instant.elapsed();
|
||||||
log::debug!("redraw: {:?}", duration);
|
log::debug!("redraw: {:?}", duration);
|
||||||
|
|
@ -158,36 +165,35 @@ fn main() {
|
||||||
for event in window.events() {
|
for event in window.events() {
|
||||||
match event.to_option() {
|
match event.to_option() {
|
||||||
EventOption::Key(event) => match event.scancode {
|
EventOption::Key(event) => match event.scancode {
|
||||||
orbclient::K_LEFT if event.pressed => buffer.action(TextAction::Left),
|
orbclient::K_LEFT if event.pressed => editor.action(Action::Left),
|
||||||
orbclient::K_RIGHT if event.pressed => buffer.action(TextAction::Right),
|
orbclient::K_RIGHT if event.pressed => editor.action(Action::Right),
|
||||||
orbclient::K_UP if event.pressed => buffer.action(TextAction::Up),
|
orbclient::K_UP if event.pressed => editor.action(Action::Up),
|
||||||
orbclient::K_DOWN if event.pressed => buffer.action(TextAction::Down),
|
orbclient::K_DOWN if event.pressed => editor.action(Action::Down),
|
||||||
orbclient::K_HOME if event.pressed => buffer.action(TextAction::Home),
|
orbclient::K_HOME if event.pressed => editor.action(Action::Home),
|
||||||
orbclient::K_END if event.pressed => buffer.action(TextAction::End),
|
orbclient::K_END if event.pressed => editor.action(Action::End),
|
||||||
orbclient::K_PGUP if event.pressed => buffer.action(TextAction::PageUp),
|
orbclient::K_PGUP if event.pressed => editor.action(Action::PageUp),
|
||||||
orbclient::K_PGDN if event.pressed => buffer.action(TextAction::PageDown),
|
orbclient::K_PGDN if event.pressed => editor.action(Action::PageDown),
|
||||||
orbclient::K_ENTER if event.pressed => buffer.action(TextAction::Enter),
|
orbclient::K_ENTER if event.pressed => editor.action(Action::Enter),
|
||||||
orbclient::K_BKSP if event.pressed => buffer.action(TextAction::Backspace),
|
orbclient::K_BKSP if event.pressed => editor.action(Action::Backspace),
|
||||||
orbclient::K_DEL if event.pressed => buffer.action(TextAction::Delete),
|
orbclient::K_DEL if event.pressed => editor.action(Action::Delete),
|
||||||
_ => (),
|
_ => (),
|
||||||
},
|
},
|
||||||
EventOption::TextInput(event) => buffer.action(TextAction::Insert(event.character)),
|
EventOption::TextInput(event) => editor.action(Action::Insert(event.character)),
|
||||||
EventOption::Mouse(mouse) => {
|
EventOption::Mouse(mouse) => {
|
||||||
mouse_x = mouse.x;
|
mouse_x = mouse.x;
|
||||||
mouse_y = mouse.y;
|
mouse_y = mouse.y;
|
||||||
if mouse_left {
|
if mouse_left {
|
||||||
buffer.action(TextAction::Drag { x: mouse_x, y: mouse_y });
|
editor.action(Action::Drag { x: mouse_x, y: mouse_y });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
EventOption::Button(button) => {
|
EventOption::Button(button) => {
|
||||||
mouse_left = button.left;
|
mouse_left = button.left;
|
||||||
if mouse_left {
|
if mouse_left {
|
||||||
buffer.action(TextAction::Click { x: mouse_x, y: mouse_y });
|
editor.action(Action::Click { x: mouse_x, y: mouse_y });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
EventOption::Resize(resize) => {
|
EventOption::Resize(resize) => {
|
||||||
buffer.set_size(resize.width as i32, resize.height as i32);
|
editor.buffer.set_size(resize.width as i32, resize.height as i32);
|
||||||
buffer.redraw = true;
|
|
||||||
},
|
},
|
||||||
EventOption::Quit(_) => process::exit(0),
|
EventOption::Quit(_) => process::exit(0),
|
||||||
_ => (),
|
_ => (),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use cosmic_text::{Attrs, Color, FontSystem, SwashCache, TextBuffer, TextMetrics};
|
use cosmic_text::{Attrs, Color, FontSystem, SwashCache, Buffer, Metrics};
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
use termion::{
|
use termion::{
|
||||||
color,
|
color,
|
||||||
|
|
@ -13,24 +13,24 @@ fn main() {
|
||||||
let mut swash_cache = SwashCache::new(&font_system);
|
let mut swash_cache = SwashCache::new(&font_system);
|
||||||
|
|
||||||
// Text metrics indicate the font size and line height of a buffer
|
// Text metrics indicate the font size and line height of a buffer
|
||||||
let metrics = TextMetrics::new(14, 20);
|
let metrics = Metrics::new(14, 20);
|
||||||
|
|
||||||
// A TextBuffer provides shaping and layout for a UTF-8 string, create one per text widget
|
// A Buffer provides shaping and layout for a UTF-8 string, create one per text widget
|
||||||
let mut text_buffer = TextBuffer::new(&font_system, metrics);
|
let mut buffer = Buffer::new(&font_system, metrics);
|
||||||
|
|
||||||
// Set a size for the text buffer, in pixels
|
// Set a size for the text buffer, in pixels
|
||||||
let width = 80u16;
|
let width = 80u16;
|
||||||
let height = 25u16;
|
let height = 25u16;
|
||||||
text_buffer.set_size(width as i32, height as i32);
|
buffer.set_size(width as i32, height as i32);
|
||||||
|
|
||||||
// Attributes indicate what font to choose
|
// Attributes indicate what font to choose
|
||||||
let attrs = Attrs::new();
|
let attrs = Attrs::new();
|
||||||
|
|
||||||
// Add some text!
|
// Add some text!
|
||||||
text_buffer.set_text(" Hi, Rust! 🦀", attrs);
|
buffer.set_text(" Hi, Rust! 🦀", attrs);
|
||||||
|
|
||||||
// Perform shaping as desired
|
// Perform shaping as desired
|
||||||
text_buffer.shape_until_cursor();
|
buffer.shape_until_cursor();
|
||||||
|
|
||||||
// Default text color (0xFF, 0xFF, 0xFF is white)
|
// Default text color (0xFF, 0xFF, 0xFF is white)
|
||||||
let text_color = Color::rgb(0xFF, 0xFF, 0xFF);
|
let text_color = Color::rgb(0xFF, 0xFF, 0xFF);
|
||||||
|
|
@ -40,7 +40,7 @@ fn main() {
|
||||||
|
|
||||||
// Clear buffer with black background
|
// Clear buffer with black background
|
||||||
for _y in 0..height {
|
for _y in 0..height {
|
||||||
for _x in 0..text_buffer.size().0 {
|
for _x in 0..buffer.size().0 {
|
||||||
print!(
|
print!(
|
||||||
"{} {}",
|
"{} {}",
|
||||||
color::Bg(color::Rgb(0, 0, 0)),
|
color::Bg(color::Rgb(0, 0, 0)),
|
||||||
|
|
@ -56,7 +56,7 @@ fn main() {
|
||||||
// Print the buffer
|
// Print the buffer
|
||||||
let mut last_x = 0;
|
let mut last_x = 0;
|
||||||
let mut last_y = 0;
|
let mut last_y = 0;
|
||||||
text_buffer.draw(&mut swash_cache, text_color, |x, y, w, h, color| {
|
buffer.draw(&mut swash_cache, text_color, |x, y, w, h, color| {
|
||||||
let a = color.a();
|
let a = color.a();
|
||||||
if a == 0 || x < 0 || y < 0 || w != 1 || h != 1 {
|
if a == 0 || x < 0 || y < 0 || w != 1 || h != 1 {
|
||||||
// Ignore alphas of 0, or invalid x, y coordinates, or unimplemented sizes
|
// Ignore alphas of 0, or invalid x, y coordinates, or unimplemented sizes
|
||||||
|
|
|
||||||
715
src/buffer.rs
715
src/buffer.rs
|
|
@ -7,77 +7,38 @@ use std::{
|
||||||
};
|
};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
use crate::{Attrs, AttrsList, Color, FontSystem, LayoutGlyph, TextBufferLine};
|
use crate::{Attrs, AttrsList, BufferLine, Color, FontSystem, LayoutGlyph, LayoutLine, ShapeLine};
|
||||||
|
|
||||||
/// An action to perform on a [TextBuffer]
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
pub enum TextAction {
|
|
||||||
/// Move cursor to previous character ([Self::Left] in LTR, [Self::Right] in RTL)
|
|
||||||
Previous,
|
|
||||||
/// Move cursor to next character ([Self::Right] in LTR, [Self::Left] in RTL)
|
|
||||||
Next,
|
|
||||||
/// Move cursor left
|
|
||||||
Left,
|
|
||||||
/// Move cursor right
|
|
||||||
Right,
|
|
||||||
/// Move cursor up
|
|
||||||
Up,
|
|
||||||
/// Move cursor down
|
|
||||||
Down,
|
|
||||||
/// Move cursor to start of line
|
|
||||||
Home,
|
|
||||||
/// Move cursor to end of line
|
|
||||||
End,
|
|
||||||
/// Scroll up one page
|
|
||||||
PageUp,
|
|
||||||
/// Scroll down one page
|
|
||||||
PageDown,
|
|
||||||
/// Insert character at cursor
|
|
||||||
Insert(char),
|
|
||||||
/// Create new line
|
|
||||||
Enter,
|
|
||||||
/// Delete text behind cursor
|
|
||||||
Backspace,
|
|
||||||
/// Delete text in front of cursor
|
|
||||||
Delete,
|
|
||||||
/// Mouse click at specified position
|
|
||||||
Click { x: i32, y: i32 },
|
|
||||||
/// Mouse drag to specified position
|
|
||||||
Drag { x: i32, y: i32 },
|
|
||||||
/// Scroll specified number of lines
|
|
||||||
Scroll { lines: i32 },
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Current cursor location
|
/// Current cursor location
|
||||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
pub struct TextCursor {
|
pub struct Cursor {
|
||||||
/// Text line the cursor is on
|
/// Text line the cursor is on
|
||||||
pub line: usize,
|
pub line: usize,
|
||||||
/// Index of glyph at cursor (will insert behind this glyph)
|
/// Index of glyph at cursor (will insert behind this glyph)
|
||||||
pub index: usize,
|
pub index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextCursor {
|
impl Cursor {
|
||||||
/// Create a new cursor
|
/// Create a new cursor
|
||||||
pub const fn new(line: usize, index: usize) -> Self {
|
pub const fn new(line: usize, index: usize) -> Self {
|
||||||
Self { line, index }
|
Self { line, index }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TextLayoutCursor {
|
pub struct LayoutCursor {
|
||||||
line: usize,
|
pub line: usize,
|
||||||
layout: usize,
|
pub layout: usize,
|
||||||
glyph: usize,
|
pub glyph: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextLayoutCursor {
|
impl LayoutCursor {
|
||||||
fn new(line: usize, layout: usize, glyph: usize) -> Self {
|
pub fn new(line: usize, layout: usize, glyph: usize) -> Self {
|
||||||
Self { line, layout, glyph }
|
Self { line, layout, glyph }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A line of visible text for rendering
|
/// A line of visible text for rendering
|
||||||
pub struct TextLayoutRun<'a> {
|
pub struct LayoutRun<'a> {
|
||||||
/// The index of the original text line
|
/// The index of the original text line
|
||||||
pub line_i: usize,
|
pub line_i: usize,
|
||||||
/// The original text line
|
/// The original text line
|
||||||
|
|
@ -90,17 +51,17 @@ pub struct TextLayoutRun<'a> {
|
||||||
pub line_y: i32,
|
pub line_y: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An iterator of visible text lines, see [TextLayoutRun]
|
/// An iterator of visible text lines, see [LayoutRun]
|
||||||
pub struct TextLayoutRunIter<'a, 'b> {
|
pub struct LayoutRunIter<'a, 'b> {
|
||||||
buffer: &'b TextBuffer<'a>,
|
buffer: &'b Buffer<'a>,
|
||||||
line_i: usize,
|
line_i: usize,
|
||||||
layout_i: usize,
|
layout_i: usize,
|
||||||
line_y: i32,
|
line_y: i32,
|
||||||
total_layout: i32,
|
total_layout: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b> TextLayoutRunIter<'a, 'b> {
|
impl<'a, 'b> LayoutRunIter<'a, 'b> {
|
||||||
pub fn new(buffer: &'b TextBuffer<'a>) -> Self {
|
pub fn new(buffer: &'b Buffer<'a>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
buffer,
|
buffer,
|
||||||
line_i: 0,
|
line_i: 0,
|
||||||
|
|
@ -111,8 +72,8 @@ impl<'a, 'b> TextLayoutRunIter<'a, 'b> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b> Iterator for TextLayoutRunIter<'a, 'b> {
|
impl<'a, 'b> Iterator for LayoutRunIter<'a, 'b> {
|
||||||
type Item = TextLayoutRun<'b>;
|
type Item = LayoutRun<'b>;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
while let Some(line) = self.buffer.lines.get(self.line_i) {
|
while let Some(line) = self.buffer.lines.get(self.line_i) {
|
||||||
|
|
@ -132,7 +93,7 @@ impl<'a, 'b> Iterator for TextLayoutRunIter<'a, 'b> {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Some(TextLayoutRun {
|
return Some(LayoutRun {
|
||||||
line_i: self.line_i,
|
line_i: self.line_i,
|
||||||
text: line.text(),
|
text: line.text(),
|
||||||
rtl: shape.rtl,
|
rtl: shape.rtl,
|
||||||
|
|
@ -150,14 +111,14 @@ impl<'a, 'b> Iterator for TextLayoutRunIter<'a, 'b> {
|
||||||
|
|
||||||
/// Metrics of text
|
/// Metrics of text
|
||||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
pub struct TextMetrics {
|
pub struct Metrics {
|
||||||
/// Font size in pixels
|
/// Font size in pixels
|
||||||
pub font_size: i32,
|
pub font_size: i32,
|
||||||
/// Line height in pixels
|
/// Line height in pixels
|
||||||
pub line_height: i32,
|
pub line_height: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextMetrics {
|
impl Metrics {
|
||||||
pub const fn new(font_size: i32, line_height: i32) -> Self {
|
pub const fn new(font_size: i32, line_height: i32) -> Self {
|
||||||
Self { font_size, line_height }
|
Self { font_size, line_height }
|
||||||
}
|
}
|
||||||
|
|
@ -170,37 +131,29 @@ impl TextMetrics {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for TextMetrics {
|
impl fmt::Display for Metrics {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "{}px / {}px", self.font_size, self.line_height)
|
write!(f, "{}px / {}px", self.font_size, self.line_height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A buffer of text that is shaped and laid out
|
/// A buffer of text that is shaped and laid out
|
||||||
pub struct TextBuffer<'a> {
|
pub struct Buffer<'a> {
|
||||||
font_system: &'a FontSystem<'a>,
|
font_system: &'a FontSystem<'a>,
|
||||||
/// Lines (or paragraphs) of text in the buffer
|
/// Lines (or paragraphs) of text in the buffer
|
||||||
pub lines: Vec<TextBufferLine<'a>>,
|
pub lines: Vec<BufferLine<'a>>,
|
||||||
metrics: TextMetrics,
|
metrics: Metrics,
|
||||||
width: i32,
|
width: i32,
|
||||||
height: i32,
|
height: i32,
|
||||||
scroll: i32,
|
scroll: i32,
|
||||||
cursor: TextCursor,
|
|
||||||
cursor_x_opt: Option<i32>,
|
|
||||||
select_opt: Option<TextCursor>,
|
|
||||||
/// True if the cursor has been moved. Set to false after processing
|
|
||||||
///
|
|
||||||
/// Usually, if this is true, you should run [Self::shape_until_cursor] before redrawing.
|
|
||||||
/// Otherwise, you should run [Self::shape_until_scroll]
|
|
||||||
pub cursor_moved: bool,
|
|
||||||
/// True if a redraw is requires. Set to false after processing
|
/// True if a redraw is requires. Set to false after processing
|
||||||
pub redraw: bool,
|
pub redraw: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TextBuffer<'a> {
|
impl<'a> Buffer<'a> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
font_system: &'a FontSystem<'a>,
|
font_system: &'a FontSystem<'a>,
|
||||||
metrics: TextMetrics,
|
metrics: Metrics,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut buffer = Self {
|
let mut buffer = Self {
|
||||||
font_system,
|
font_system,
|
||||||
|
|
@ -209,16 +162,32 @@ impl<'a> TextBuffer<'a> {
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
scroll: 0,
|
scroll: 0,
|
||||||
cursor: TextCursor::default(),
|
|
||||||
cursor_x_opt: None,
|
|
||||||
select_opt: None,
|
|
||||||
cursor_moved: false,
|
|
||||||
redraw: false,
|
redraw: false,
|
||||||
};
|
};
|
||||||
buffer.set_text("", Attrs::new());
|
buffer.set_text("", Attrs::new());
|
||||||
buffer
|
buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn relayout(&mut self) {
|
||||||
|
let instant = Instant::now();
|
||||||
|
|
||||||
|
for line in self.lines.iter_mut() {
|
||||||
|
if line.shape_opt().is_some() {
|
||||||
|
line.reset_layout();
|
||||||
|
line.layout(
|
||||||
|
self.font_system,
|
||||||
|
self.metrics.font_size,
|
||||||
|
self.width
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.redraw = true;
|
||||||
|
|
||||||
|
let duration = instant.elapsed();
|
||||||
|
log::debug!("relayout: {:?}", duration);
|
||||||
|
}
|
||||||
|
|
||||||
/// Pre-shape lines in the buffer, up to `lines`, return actual number of layout lines
|
/// Pre-shape lines in the buffer, up to `lines`, return actual number of layout lines
|
||||||
pub fn shape_until(&mut self, lines: i32) -> i32 {
|
pub fn shape_until(&mut self, lines: i32) -> i32 {
|
||||||
let instant = Instant::now();
|
let instant = Instant::now();
|
||||||
|
|
@ -251,13 +220,13 @@ impl<'a> TextBuffer<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shape lines until cursor, also scrolling to include cursor in view
|
/// Shape lines until cursor, also scrolling to include cursor in view
|
||||||
pub fn shape_until_cursor(&mut self) {
|
pub fn shape_until_cursor(&mut self, cursor: Cursor) {
|
||||||
let instant = Instant::now();
|
let instant = Instant::now();
|
||||||
|
|
||||||
let mut reshaped = 0;
|
let mut reshaped = 0;
|
||||||
let mut layout_i = 0;
|
let mut layout_i = 0;
|
||||||
for (line_i, line) in self.lines.iter_mut().enumerate() {
|
for (line_i, line) in self.lines.iter_mut().enumerate() {
|
||||||
if line_i > self.cursor.line {
|
if line_i > cursor.line {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -269,8 +238,8 @@ impl<'a> TextBuffer<'a> {
|
||||||
self.metrics.font_size,
|
self.metrics.font_size,
|
||||||
self.width
|
self.width
|
||||||
);
|
);
|
||||||
if line_i == self.cursor.line {
|
if line_i == cursor.line {
|
||||||
let layout_cursor = self.layout_cursor(&self.cursor);
|
let layout_cursor = self.layout_cursor(&cursor);
|
||||||
layout_i += layout_cursor.layout as i32;
|
layout_i += layout_cursor.layout as i32;
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -284,7 +253,7 @@ impl<'a> TextBuffer<'a> {
|
||||||
self.redraw = true;
|
self.redraw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let lines = self.lines();
|
let lines = self.visible_lines();
|
||||||
if layout_i < self.scroll {
|
if layout_i < self.scroll {
|
||||||
self.scroll = layout_i;
|
self.scroll = layout_i;
|
||||||
} else if layout_i >= self.scroll + lines {
|
} else if layout_i >= self.scroll + lines {
|
||||||
|
|
@ -296,7 +265,7 @@ impl<'a> TextBuffer<'a> {
|
||||||
|
|
||||||
/// Shape lines until scroll
|
/// Shape lines until scroll
|
||||||
pub fn shape_until_scroll(&mut self) {
|
pub fn shape_until_scroll(&mut self) {
|
||||||
let lines = self.lines();
|
let lines = self.visible_lines();
|
||||||
|
|
||||||
let scroll_end = self.scroll + lines;
|
let scroll_end = self.scroll + lines;
|
||||||
let total_layout = self.shape_until(scroll_end);
|
let total_layout = self.shape_until(scroll_end);
|
||||||
|
|
@ -310,34 +279,14 @@ impl<'a> TextBuffer<'a> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn relayout(&mut self) {
|
pub fn layout_cursor(&self, cursor: &Cursor) -> LayoutCursor {
|
||||||
let instant = Instant::now();
|
|
||||||
|
|
||||||
for line in self.lines.iter_mut() {
|
|
||||||
if line.shape_opt().is_some() {
|
|
||||||
line.reset_layout();
|
|
||||||
line.layout(
|
|
||||||
self.font_system,
|
|
||||||
self.metrics.font_size,
|
|
||||||
self.width
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.redraw = true;
|
|
||||||
|
|
||||||
let duration = instant.elapsed();
|
|
||||||
log::debug!("relayout: {:?}", duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layout_cursor(&self, cursor: &TextCursor) -> TextLayoutCursor {
|
|
||||||
let line = &self.lines[cursor.line];
|
let line = &self.lines[cursor.line];
|
||||||
|
|
||||||
let layout = line.layout_opt().as_ref().unwrap(); //TODO: ensure layout is done?
|
let layout = line.layout_opt().as_ref().unwrap(); //TODO: ensure layout is done?
|
||||||
for (layout_i, layout_line) in layout.iter().enumerate() {
|
for (layout_i, layout_line) in layout.iter().enumerate() {
|
||||||
for (glyph_i, glyph) in layout_line.glyphs.iter().enumerate() {
|
for (glyph_i, glyph) in layout_line.glyphs.iter().enumerate() {
|
||||||
if cursor.index == glyph.start {
|
if cursor.index == glyph.start {
|
||||||
return TextLayoutCursor::new(
|
return LayoutCursor::new(
|
||||||
cursor.line,
|
cursor.line,
|
||||||
layout_i,
|
layout_i,
|
||||||
glyph_i
|
glyph_i
|
||||||
|
|
@ -347,7 +296,7 @@ impl<'a> TextBuffer<'a> {
|
||||||
match layout_line.glyphs.last() {
|
match layout_line.glyphs.last() {
|
||||||
Some(glyph) => {
|
Some(glyph) => {
|
||||||
if cursor.index == glyph.end {
|
if cursor.index == glyph.end {
|
||||||
return TextLayoutCursor::new(
|
return LayoutCursor::new(
|
||||||
cursor.line,
|
cursor.line,
|
||||||
layout_i,
|
layout_i,
|
||||||
layout_line.glyphs.len()
|
layout_line.glyphs.len()
|
||||||
|
|
@ -355,7 +304,7 @@ impl<'a> TextBuffer<'a> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
return TextLayoutCursor::new(
|
return LayoutCursor::new(
|
||||||
cursor.line,
|
cursor.line,
|
||||||
layout_i,
|
layout_i,
|
||||||
0
|
0
|
||||||
|
|
@ -366,57 +315,30 @@ impl<'a> TextBuffer<'a> {
|
||||||
|
|
||||||
// Fall back to start of line
|
// Fall back to start of line
|
||||||
//TODO: should this be the end of the line?
|
//TODO: should this be the end of the line?
|
||||||
TextLayoutCursor::new(
|
LayoutCursor::new(
|
||||||
cursor.line,
|
cursor.line,
|
||||||
0,
|
0,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_layout_cursor(&mut self, cursor: TextLayoutCursor) {
|
pub fn line_shape(&mut self, line_i: usize) -> Option<&ShapeLine> {
|
||||||
let line = &mut self.lines[cursor.line];
|
let line = self.lines.get_mut(line_i)?;
|
||||||
let layout = line.layout(
|
Some(line.shape(&self.font_system))
|
||||||
self.font_system,
|
|
||||||
self.metrics.font_size,
|
|
||||||
self.width
|
|
||||||
);
|
|
||||||
|
|
||||||
let layout_line = match layout.get(cursor.layout) {
|
|
||||||
Some(some) => some,
|
|
||||||
None => match layout.last() {
|
|
||||||
Some(some) => some,
|
|
||||||
None => todo!("layout cursor in line with no layouts"),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let new_index = match layout_line.glyphs.get(cursor.glyph) {
|
|
||||||
Some(glyph) => glyph.start,
|
|
||||||
None => match layout_line.glyphs.last() {
|
|
||||||
Some(glyph) => glyph.end,
|
|
||||||
//TODO: is this correct?
|
|
||||||
None => 0,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if self.cursor.line != cursor.line || self.cursor.index != new_index {
|
|
||||||
self.cursor.line = cursor.line;
|
|
||||||
self.cursor.index = new_index;
|
|
||||||
self.redraw = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current cursor position
|
pub fn line_layout(&mut self, line_i: usize) -> Option<&[LayoutLine]> {
|
||||||
pub fn cursor(&self) -> TextCursor {
|
let line = self.lines.get_mut(line_i)?;
|
||||||
self.cursor
|
Some(line.layout(&self.font_system, self.metrics.font_size, self.width))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current [TextMetrics]
|
/// Get the current [Metrics]
|
||||||
pub fn metrics(&self) -> TextMetrics {
|
pub fn metrics(&self) -> Metrics {
|
||||||
self.metrics
|
self.metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the current [TextMetrics]
|
/// Set the current [Metrics]
|
||||||
pub fn set_metrics(&mut self, metrics: TextMetrics) {
|
pub fn set_metrics(&mut self, metrics: Metrics) {
|
||||||
if metrics != self.metrics {
|
if metrics != self.metrics {
|
||||||
self.metrics = metrics;
|
self.metrics = metrics;
|
||||||
self.relayout();
|
self.relayout();
|
||||||
|
|
@ -448,8 +370,15 @@ impl<'a> TextBuffer<'a> {
|
||||||
self.scroll
|
self.scroll
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_scroll(&mut self, scroll: i32) {
|
||||||
|
if scroll != self.scroll {
|
||||||
|
self.scroll = scroll;
|
||||||
|
self.redraw = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the number of lines that can be viewed in the buffer
|
/// Get the number of lines that can be viewed in the buffer
|
||||||
pub fn lines(&self) -> i32 {
|
pub fn visible_lines(&self) -> i32 {
|
||||||
self.height / self.metrics.line_height
|
self.height / self.metrics.line_height
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -457,325 +386,25 @@ impl<'a> TextBuffer<'a> {
|
||||||
pub fn set_text(&mut self, text: &str, attrs: Attrs<'a>) {
|
pub fn set_text(&mut self, text: &str, attrs: Attrs<'a>) {
|
||||||
self.lines.clear();
|
self.lines.clear();
|
||||||
for line in text.lines() {
|
for line in text.lines() {
|
||||||
self.lines.push(TextBufferLine::new(line.to_string(), AttrsList::new(attrs)));
|
self.lines.push(BufferLine::new(line.to_string(), AttrsList::new(attrs)));
|
||||||
}
|
}
|
||||||
// Make sure there is always one line
|
// Make sure there is always one line
|
||||||
if self.lines.is_empty() {
|
if self.lines.is_empty() {
|
||||||
self.lines.push(TextBufferLine::new(String::new(), AttrsList::new(attrs)));
|
self.lines.push(BufferLine::new(String::new(), AttrsList::new(attrs)));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.scroll = 0;
|
self.scroll = 0;
|
||||||
self.cursor = TextCursor::default();
|
|
||||||
self.select_opt = None;
|
|
||||||
|
|
||||||
self.shape_until_scroll();
|
self.shape_until_scroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform a [TextAction] on the buffer
|
/// Get the visible layout runs for rendering and other tasks
|
||||||
pub fn action(&mut self, action: TextAction) {
|
pub fn layout_runs<'b>(&'b self) -> LayoutRunIter<'a, 'b> {
|
||||||
let old_cursor = self.cursor;
|
LayoutRunIter::new(self)
|
||||||
|
|
||||||
match action {
|
|
||||||
TextAction::Previous => {
|
|
||||||
let line = &mut self.lines[self.cursor.line];
|
|
||||||
if self.cursor.index > 0 {
|
|
||||||
// Find previous character index
|
|
||||||
let mut prev_index = 0;
|
|
||||||
for (i, _) in line.text().grapheme_indices(true) {
|
|
||||||
if i < self.cursor.index {
|
|
||||||
prev_index = i;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.cursor.index = prev_index;
|
|
||||||
self.redraw = true;
|
|
||||||
} else if self.cursor.line > 0 {
|
|
||||||
self.cursor.line -= 1;
|
|
||||||
self.cursor.index = self.lines[self.cursor.line].text().len();
|
|
||||||
self.redraw = true;
|
|
||||||
}
|
|
||||||
self.cursor_x_opt = None;
|
|
||||||
},
|
|
||||||
TextAction::Next => {
|
|
||||||
let line = &mut self.lines[self.cursor.line];
|
|
||||||
if self.cursor.index < line.text().len() {
|
|
||||||
for (i, c) in line.text().grapheme_indices(true) {
|
|
||||||
if i == self.cursor.index {
|
|
||||||
self.cursor.index += c.len();
|
|
||||||
self.redraw = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if self.cursor.line + 1 < self.lines.len() {
|
|
||||||
self.cursor.line += 1;
|
|
||||||
self.cursor.index = 0;
|
|
||||||
self.redraw = true;
|
|
||||||
}
|
|
||||||
self.cursor_x_opt = None;
|
|
||||||
},
|
|
||||||
TextAction::Left => {
|
|
||||||
let rtl_opt = self.lines[self.cursor.line].shape_opt().as_ref().map(|shape| shape.rtl);
|
|
||||||
if let Some(rtl) = rtl_opt {
|
|
||||||
if rtl {
|
|
||||||
self.action(TextAction::Next);
|
|
||||||
} else {
|
|
||||||
self.action(TextAction::Previous);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
TextAction::Right => {
|
|
||||||
let rtl_opt = self.lines[self.cursor.line].shape_opt().as_ref().map(|shape| shape.rtl);
|
|
||||||
if let Some(rtl) = rtl_opt {
|
|
||||||
if rtl {
|
|
||||||
self.action(TextAction::Previous);
|
|
||||||
} else {
|
|
||||||
self.action(TextAction::Next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
TextAction::Up => {
|
|
||||||
//TODO: make this preserve X as best as possible!
|
|
||||||
let mut cursor = self.layout_cursor(&self.cursor);
|
|
||||||
|
|
||||||
if self.cursor_x_opt.is_none() {
|
|
||||||
self.cursor_x_opt = Some(
|
|
||||||
cursor.glyph as i32 //TODO: glyph x position
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if cursor.layout > 0 {
|
|
||||||
cursor.layout -= 1;
|
|
||||||
} else if cursor.line > 0 {
|
|
||||||
cursor.line -= 1;
|
|
||||||
cursor.layout = usize::max_value();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(cursor_x) = self.cursor_x_opt {
|
|
||||||
cursor.glyph = cursor_x as usize; //TODO: glyph x position
|
|
||||||
}
|
|
||||||
|
|
||||||
self.set_layout_cursor(cursor);
|
|
||||||
},
|
|
||||||
TextAction::Down => {
|
|
||||||
//TODO: make this preserve X as best as possible!
|
|
||||||
let mut cursor = self.layout_cursor(&self.cursor);
|
|
||||||
|
|
||||||
let layout_len = {
|
|
||||||
let line = &mut self.lines[cursor.line];
|
|
||||||
let layout = line.layout(
|
|
||||||
self.font_system,
|
|
||||||
self.metrics.font_size,
|
|
||||||
self.width
|
|
||||||
);
|
|
||||||
layout.len()
|
|
||||||
};
|
|
||||||
|
|
||||||
if self.cursor_x_opt.is_none() {
|
|
||||||
self.cursor_x_opt = Some(
|
|
||||||
cursor.glyph as i32 //TODO: glyph x position
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if cursor.layout + 1 < layout_len {
|
|
||||||
cursor.layout += 1;
|
|
||||||
} else if cursor.line + 1 < self.lines.len() {
|
|
||||||
cursor.line += 1;
|
|
||||||
cursor.layout = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(cursor_x) = self.cursor_x_opt {
|
|
||||||
cursor.glyph = cursor_x as usize; //TODO: glyph x position
|
|
||||||
}
|
|
||||||
|
|
||||||
self.set_layout_cursor(cursor);
|
|
||||||
},
|
|
||||||
TextAction::Home => {
|
|
||||||
let mut cursor = self.layout_cursor(&self.cursor);
|
|
||||||
cursor.glyph = 0;
|
|
||||||
self.set_layout_cursor(cursor);
|
|
||||||
self.cursor_x_opt = None;
|
|
||||||
},
|
|
||||||
TextAction::End => {
|
|
||||||
let mut cursor = self.layout_cursor(&self.cursor);
|
|
||||||
cursor.glyph = usize::max_value();
|
|
||||||
self.set_layout_cursor(cursor);
|
|
||||||
self.cursor_x_opt = None;
|
|
||||||
}
|
|
||||||
TextAction::PageUp => {
|
|
||||||
//TODO: move cursor
|
|
||||||
self.scroll -= self.lines();
|
|
||||||
self.redraw = true;
|
|
||||||
|
|
||||||
self.shape_until_scroll();
|
|
||||||
},
|
|
||||||
TextAction::PageDown => {
|
|
||||||
//TODO: move cursor
|
|
||||||
self.scroll += self.lines();
|
|
||||||
self.redraw = true;
|
|
||||||
|
|
||||||
self.shape_until_scroll();
|
|
||||||
},
|
|
||||||
TextAction::Insert(character) => {
|
|
||||||
if character.is_control()
|
|
||||||
&& !['\t', '\u{92}'].contains(&character)
|
|
||||||
{
|
|
||||||
// Filter out special chars (except for tab), use TextAction instead
|
|
||||||
log::debug!("Refusing to insert control character {:?}", character);
|
|
||||||
} else {
|
|
||||||
let line = &mut self.lines[self.cursor.line];
|
|
||||||
|
|
||||||
println!("Before");
|
|
||||||
for span in line.attrs_list().spans() {
|
|
||||||
println!("{:?}", span);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect text after insertion as a line
|
|
||||||
let after = line.split_off(self.cursor.index);
|
|
||||||
|
|
||||||
// Append the inserted text
|
|
||||||
line.append(TextBufferLine::new(
|
|
||||||
character.to_string(),
|
|
||||||
AttrsList::new(line.attrs_list().defaults() /*TODO: provide attrs?*/)
|
|
||||||
));
|
|
||||||
|
|
||||||
// Append the text after insertion
|
|
||||||
line.append(after);
|
|
||||||
|
|
||||||
println!("After");
|
|
||||||
for span in line.attrs_list().spans() {
|
|
||||||
println!("{:?}", span);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.cursor.index += character.len_utf8();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
TextAction::Enter => {
|
|
||||||
let new_line = self.lines[self.cursor.line].split_off(self.cursor.index);
|
|
||||||
|
|
||||||
self.cursor.line += 1;
|
|
||||||
self.cursor.index = 0;
|
|
||||||
|
|
||||||
self.lines.insert(self.cursor.line, new_line);
|
|
||||||
},
|
|
||||||
TextAction::Backspace => {
|
|
||||||
if self.cursor.index > 0 {
|
|
||||||
let line = &mut self.lines[self.cursor.line];
|
|
||||||
|
|
||||||
println!("Before");
|
|
||||||
for span in line.attrs_list().spans() {
|
|
||||||
println!("{:?}", span);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get text line after cursor
|
|
||||||
let after = line.split_off(self.cursor.index);
|
|
||||||
|
|
||||||
// Find previous character index
|
|
||||||
let mut prev_index = 0;
|
|
||||||
for (i, _) in line.text().char_indices() {
|
|
||||||
if i < self.cursor.index {
|
|
||||||
prev_index = i;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Move cursor {} to {}", self.cursor.index, prev_index);
|
|
||||||
|
|
||||||
self.cursor.index = prev_index;
|
|
||||||
|
|
||||||
// Remove character
|
|
||||||
line.split_off(self.cursor.index);
|
|
||||||
|
|
||||||
// Add text after cursor
|
|
||||||
line.append(after);
|
|
||||||
|
|
||||||
println!("After");
|
|
||||||
for span in line.attrs_list().spans() {
|
|
||||||
println!("{:?}", span);
|
|
||||||
}
|
|
||||||
} else if self.cursor.line > 0 {
|
|
||||||
let mut line_index = self.cursor.line;
|
|
||||||
let old_line = self.lines.remove(line_index);
|
|
||||||
line_index -= 1;
|
|
||||||
|
|
||||||
let line = &mut self.lines[line_index];
|
|
||||||
|
|
||||||
self.cursor.line = line_index;
|
|
||||||
self.cursor.index = line.text().len();
|
|
||||||
|
|
||||||
line.append(old_line);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
TextAction::Delete => {
|
|
||||||
if self.cursor.index < self.lines[self.cursor.line].text().len() {
|
|
||||||
let line = &mut self.lines[self.cursor.line];
|
|
||||||
|
|
||||||
let range_opt = line
|
|
||||||
.text()
|
|
||||||
.grapheme_indices(true)
|
|
||||||
.take_while(|(i, _)| *i <= self.cursor.index)
|
|
||||||
.last()
|
|
||||||
.map(|(i, c)| {
|
|
||||||
i..(i + c.len())
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(range) = range_opt {
|
|
||||||
self.cursor.index = range.start;
|
|
||||||
|
|
||||||
// Get text after deleted EGC
|
|
||||||
let after = line.split_off(range.end);
|
|
||||||
|
|
||||||
// Delete EGC
|
|
||||||
line.split_off(range.start);
|
|
||||||
|
|
||||||
// Add text after deleted EGC
|
|
||||||
line.append(after);
|
|
||||||
}
|
|
||||||
} else if self.cursor.line + 1 < self.lines.len() {
|
|
||||||
let old_line = self.lines.remove(self.cursor.line + 1);
|
|
||||||
self.lines[self.cursor.line].append(old_line);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
TextAction::Click { x, y } => {
|
|
||||||
self.select_opt = None;
|
|
||||||
|
|
||||||
if let Some(new_cursor) = self.hit(x, y) {
|
|
||||||
if new_cursor != self.cursor {
|
|
||||||
self.cursor = new_cursor;
|
|
||||||
self.redraw = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
TextAction::Drag { x, y } => {
|
|
||||||
if self.select_opt.is_none() {
|
|
||||||
self.select_opt = Some(self.cursor);
|
|
||||||
self.redraw = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(new_cursor) = self.hit(x, y) {
|
|
||||||
if new_cursor != self.cursor {
|
|
||||||
self.cursor = new_cursor;
|
|
||||||
self.redraw = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
TextAction::Scroll { lines } => {
|
|
||||||
self.scroll += lines;
|
|
||||||
self.redraw = true;
|
|
||||||
|
|
||||||
self.shape_until_scroll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if old_cursor != self.cursor {
|
|
||||||
self.cursor_moved = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert x, y position to TextCursor (hit detection)
|
/// Convert x, y position to Cursor (hit detection)
|
||||||
pub fn hit(&self, x: i32, y: i32) -> Option<TextCursor> {
|
pub fn hit(&self, x: i32, y: i32) -> Option<Cursor> {
|
||||||
let instant = Instant::now();
|
let instant = Instant::now();
|
||||||
|
|
||||||
let font_size = self.metrics.font_size;
|
let font_size = self.metrics.font_size;
|
||||||
|
|
@ -826,7 +455,7 @@ impl<'a> TextBuffer<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut new_cursor = TextCursor::new(run.line_i, 0);
|
let mut new_cursor = Cursor::new(run.line_i, 0);
|
||||||
|
|
||||||
match run.glyphs.get(new_cursor_glyph) {
|
match run.glyphs.get(new_cursor_glyph) {
|
||||||
Some(glyph) => {
|
Some(glyph) => {
|
||||||
|
|
@ -839,22 +468,6 @@ impl<'a> TextBuffer<'a> {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if new_cursor != self.cursor {
|
|
||||||
if let Some(glyph) = run.glyphs.get(new_cursor_glyph) {
|
|
||||||
let font_opt = self.font_system.get_font(glyph.cache_key.font_id);
|
|
||||||
let text_glyph = &run.text[glyph.start..glyph.end];
|
|
||||||
log::debug!(
|
|
||||||
"{}, {}: '{}' ('{}'): '{}' ({:?})",
|
|
||||||
self.cursor.line,
|
|
||||||
self.cursor.index,
|
|
||||||
font_opt.as_ref().map_or("?", |font| font.info.family.as_str()),
|
|
||||||
font_opt.as_ref().map_or("?", |font| font.info.post_script_name.as_str()),
|
|
||||||
text_glyph,
|
|
||||||
text_glyph
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
new_cursor_opt = Some(new_cursor);
|
new_cursor_opt = Some(new_cursor);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
@ -867,172 +480,12 @@ impl<'a> TextBuffer<'a> {
|
||||||
new_cursor_opt
|
new_cursor_opt
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the visible layout runs for rendering and other tasks
|
|
||||||
pub fn layout_runs<'b>(&'b self) -> TextLayoutRunIter<'a, 'b> {
|
|
||||||
TextLayoutRunIter::new(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw the buffer
|
/// Draw the buffer
|
||||||
#[cfg(feature = "swash")]
|
#[cfg(feature = "swash")]
|
||||||
pub fn draw<F>(&self, cache: &mut crate::SwashCache, color: Color, mut f: F)
|
pub fn draw<F>(&self, cache: &mut crate::SwashCache, color: Color, mut f: F)
|
||||||
where F: FnMut(i32, i32, u32, u32, Color)
|
where F: FnMut(i32, i32, u32, u32, Color)
|
||||||
{
|
{
|
||||||
let font_size = self.metrics.font_size;
|
|
||||||
let line_height = self.metrics.line_height;
|
|
||||||
|
|
||||||
for run in self.layout_runs() {
|
for run in self.layout_runs() {
|
||||||
let line_i = run.line_i;
|
|
||||||
let line_y = run.line_y;
|
|
||||||
|
|
||||||
let cursor_glyph_opt = |cursor: &TextCursor| -> Option<(usize, f32)> {
|
|
||||||
if cursor.line == line_i {
|
|
||||||
for (glyph_i, glyph) in run.glyphs.iter().enumerate() {
|
|
||||||
if cursor.index == glyph.start {
|
|
||||||
return Some((glyph_i, 0.0));
|
|
||||||
} else if cursor.index > glyph.start && cursor.index < glyph.end {
|
|
||||||
// Guess x offset based on characters
|
|
||||||
let mut before = 0;
|
|
||||||
let mut total = 0;
|
|
||||||
|
|
||||||
let cluster = &run.text[glyph.start..glyph.end];
|
|
||||||
for (i, _) in cluster.grapheme_indices(true) {
|
|
||||||
if glyph.start + i < cursor.index {
|
|
||||||
before += 1;
|
|
||||||
}
|
|
||||||
total += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let offset = glyph.w * (before as f32) / (total as f32);
|
|
||||||
return Some((glyph_i, offset));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
match run.glyphs.last() {
|
|
||||||
Some(glyph) => {
|
|
||||||
if cursor.index == glyph.end {
|
|
||||||
return Some((run.glyphs.len(), 0.0));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
return Some((0, 0.0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Highlight selection (TODO: HIGHLIGHT COLOR!)
|
|
||||||
if let Some(select) = self.select_opt {
|
|
||||||
let (start, end) = match select.line.cmp(&self.cursor.line) {
|
|
||||||
cmp::Ordering::Greater => (self.cursor, select),
|
|
||||||
cmp::Ordering::Less => (select, self.cursor),
|
|
||||||
cmp::Ordering::Equal => {
|
|
||||||
/* select.line == self.cursor.line */
|
|
||||||
if select.index < self.cursor.index {
|
|
||||||
(select, self.cursor)
|
|
||||||
} else {
|
|
||||||
/* select.index >= self.cursor.index */
|
|
||||||
(self.cursor, select)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if line_i >= start.line && line_i <= end.line {
|
|
||||||
let mut range_opt = None;
|
|
||||||
for glyph in run.glyphs.iter() {
|
|
||||||
// Guess x offset based on characters
|
|
||||||
let cluster = &run.text[glyph.start..glyph.end];
|
|
||||||
let total = cluster.grapheme_indices(true).count();
|
|
||||||
let mut c_x = glyph.x;
|
|
||||||
let c_w = glyph.w / total as f32;
|
|
||||||
for (i, c) in cluster.grapheme_indices(true) {
|
|
||||||
let c_start = glyph.start + i;
|
|
||||||
let c_end = glyph.start + i + c.len();
|
|
||||||
if (start.line != line_i || c_end > start.index)
|
|
||||||
&& (end.line != line_i || c_start < end.index) {
|
|
||||||
range_opt = match range_opt.take() {
|
|
||||||
Some((min, max)) => Some((
|
|
||||||
cmp::min(min, c_x as i32),
|
|
||||||
cmp::max(max, (c_x + c_w) as i32),
|
|
||||||
)),
|
|
||||||
None => Some((
|
|
||||||
c_x as i32,
|
|
||||||
(c_x + c_w) as i32,
|
|
||||||
))
|
|
||||||
};
|
|
||||||
} else if let Some((min, max)) = range_opt.take() {
|
|
||||||
f(
|
|
||||||
min,
|
|
||||||
line_y - font_size,
|
|
||||||
cmp::max(0, max - min) as u32,
|
|
||||||
line_height as u32,
|
|
||||||
Color::rgba(color.r(), color.g(), color.b(), 0x33)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
c_x += c_w;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if run.glyphs.is_empty() && end.line > line_i{
|
|
||||||
// Highlight all of internal empty lines
|
|
||||||
range_opt = Some((0, self.width));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((mut min, mut max)) = range_opt.take() {
|
|
||||||
if end.line > line_i {
|
|
||||||
// Draw to end of line
|
|
||||||
if run.rtl {
|
|
||||||
min = 0;
|
|
||||||
} else {
|
|
||||||
max = self.width;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f(
|
|
||||||
min,
|
|
||||||
line_y - font_size,
|
|
||||||
cmp::max(0, max - min) as u32,
|
|
||||||
line_height as u32,
|
|
||||||
Color::rgba(color.r(), color.g(), color.b(), 0x33)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw cursor
|
|
||||||
if let Some((cursor_glyph, cursor_glyph_offset)) = cursor_glyph_opt(&self.cursor) {
|
|
||||||
let x = match run.glyphs.get(cursor_glyph) {
|
|
||||||
Some(glyph) => {
|
|
||||||
// Start of detected glyph
|
|
||||||
if glyph.rtl {
|
|
||||||
(glyph.x + glyph.w - cursor_glyph_offset) as i32
|
|
||||||
} else {
|
|
||||||
(glyph.x + cursor_glyph_offset) as i32
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => match run.glyphs.last() {
|
|
||||||
Some(glyph) => {
|
|
||||||
// End of last glyph
|
|
||||||
if glyph.rtl {
|
|
||||||
glyph.x as i32
|
|
||||||
} else {
|
|
||||||
(glyph.x + glyph.w) as i32
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
// Start of empty line
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
f(
|
|
||||||
x,
|
|
||||||
line_y - font_size,
|
|
||||||
1,
|
|
||||||
line_height as u32,
|
|
||||||
color,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for glyph in run.glyphs.iter() {
|
for glyph in run.glyphs.iter() {
|
||||||
let (cache_key, x_int, y_int) = (glyph.cache_key, glyph.x_int, glyph.y_int);
|
let (cache_key, x_int, y_int) = (glyph.cache_key, glyph.x_int, glyph.y_int);
|
||||||
|
|
||||||
|
|
@ -1042,7 +495,7 @@ impl<'a> TextBuffer<'a> {
|
||||||
};
|
};
|
||||||
|
|
||||||
cache.with_pixels(cache_key, glyph_color, |x, y, color| {
|
cache.with_pixels(cache_key, glyph_color, |x, y, color| {
|
||||||
f(x_int + x, line_y + y_int + y, 1, 1, color)
|
f(x_int + x, run.line_y + y_int + y, 1, 1, color)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::{AttrsList, FontSystem, LayoutLine, ShapeLine};
|
use crate::{AttrsList, FontSystem, LayoutLine, ShapeLine};
|
||||||
|
|
||||||
/// A line (or paragraph) of text that is shaped and laid out
|
/// A line (or paragraph) of text that is shaped and laid out
|
||||||
pub struct TextBufferLine<'a> {
|
pub struct BufferLine<'a> {
|
||||||
//TODO: make this not pub(crate)
|
//TODO: make this not pub(crate)
|
||||||
text: String,
|
text: String,
|
||||||
attrs_list: AttrsList<'a>,
|
attrs_list: AttrsList<'a>,
|
||||||
|
|
@ -10,7 +10,7 @@ pub struct TextBufferLine<'a> {
|
||||||
layout_opt: Option<Vec<LayoutLine>>,
|
layout_opt: Option<Vec<LayoutLine>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TextBufferLine<'a> {
|
impl<'a> BufferLine<'a> {
|
||||||
/// Create a new line with the given text and attributes list
|
/// Create a new line with the given text and attributes list
|
||||||
/// Cached shaping and layout can be done using the [Self::shape] and
|
/// Cached shaping and layout can be done using the [Self::shape] and
|
||||||
/// [Self::layout] functions
|
/// [Self::layout] functions
|
||||||
|
|
|
||||||
595
src/editor.rs
Normal file
595
src/editor.rs
Normal file
|
|
@ -0,0 +1,595 @@
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use std::cmp;
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
|
use crate::{AttrsList, Buffer, BufferLine, Color, Cursor, LayoutCursor};
|
||||||
|
|
||||||
|
/// An action to perform on an [Editor]
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum Action {
|
||||||
|
/// Move cursor to previous character ([Self::Left] in LTR, [Self::Right] in RTL)
|
||||||
|
Previous,
|
||||||
|
/// Move cursor to next character ([Self::Right] in LTR, [Self::Left] in RTL)
|
||||||
|
Next,
|
||||||
|
/// Move cursor left
|
||||||
|
Left,
|
||||||
|
/// Move cursor right
|
||||||
|
Right,
|
||||||
|
/// Move cursor up
|
||||||
|
Up,
|
||||||
|
/// Move cursor down
|
||||||
|
Down,
|
||||||
|
/// Move cursor to start of line
|
||||||
|
Home,
|
||||||
|
/// Move cursor to end of line
|
||||||
|
End,
|
||||||
|
/// Scroll up one page
|
||||||
|
PageUp,
|
||||||
|
/// Scroll down one page
|
||||||
|
PageDown,
|
||||||
|
/// Insert character at cursor
|
||||||
|
Insert(char),
|
||||||
|
/// Create new line
|
||||||
|
Enter,
|
||||||
|
/// Delete text behind cursor
|
||||||
|
Backspace,
|
||||||
|
/// Delete text in front of cursor
|
||||||
|
Delete,
|
||||||
|
/// Mouse click at specified position
|
||||||
|
Click { x: i32, y: i32 },
|
||||||
|
/// Mouse drag to specified position
|
||||||
|
Drag { x: i32, y: i32 },
|
||||||
|
/// Scroll specified number of lines
|
||||||
|
Scroll { lines: i32 },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper of [Buffer] for easy editing
|
||||||
|
pub struct Editor<'a> {
|
||||||
|
pub buffer: Buffer<'a>,
|
||||||
|
cursor: Cursor,
|
||||||
|
cursor_x_opt: Option<i32>,
|
||||||
|
select_opt: Option<Cursor>,
|
||||||
|
/// True if the cursor has been moved. Set to false after processing
|
||||||
|
///
|
||||||
|
/// Usually, if this is true, you should run [Self::shape_until_cursor] before redrawing.
|
||||||
|
/// Otherwise, you should run [Self::shape_until_scroll]
|
||||||
|
pub cursor_moved: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Editor<'a> {
|
||||||
|
pub fn new(buffer: Buffer<'a>) -> Self {
|
||||||
|
Self {
|
||||||
|
buffer,
|
||||||
|
cursor: Cursor::default(),
|
||||||
|
cursor_x_opt: None,
|
||||||
|
select_opt: None,
|
||||||
|
cursor_moved: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shape_as_needed(&mut self) {
|
||||||
|
if self.cursor_moved {
|
||||||
|
self.buffer.shape_until_cursor(self.cursor);
|
||||||
|
self.cursor_moved = false;
|
||||||
|
} else {
|
||||||
|
self.buffer.shape_until_scroll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_layout_cursor(&mut self, cursor: LayoutCursor) {
|
||||||
|
let layout = self.buffer.line_layout(cursor.line).unwrap();
|
||||||
|
|
||||||
|
let layout_line = match layout.get(cursor.layout) {
|
||||||
|
Some(some) => some,
|
||||||
|
None => match layout.last() {
|
||||||
|
Some(some) => some,
|
||||||
|
None => todo!("layout cursor in line with no layouts"),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_index = match layout_line.glyphs.get(cursor.glyph) {
|
||||||
|
Some(glyph) => glyph.start,
|
||||||
|
None => match layout_line.glyphs.last() {
|
||||||
|
Some(glyph) => glyph.end,
|
||||||
|
//TODO: is this correct?
|
||||||
|
None => 0,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.cursor.line != cursor.line || self.cursor.index != new_index {
|
||||||
|
self.cursor.line = cursor.line;
|
||||||
|
self.cursor.index = new_index;
|
||||||
|
self.buffer.redraw = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current cursor position
|
||||||
|
pub fn cursor(&self) -> Cursor {
|
||||||
|
self.cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform a [Action] on the editor
|
||||||
|
pub fn action(&mut self, action: Action) {
|
||||||
|
let old_cursor = self.cursor;
|
||||||
|
|
||||||
|
match action {
|
||||||
|
Action::Previous => {
|
||||||
|
let line = &mut self.buffer.lines[self.cursor.line];
|
||||||
|
if self.cursor.index > 0 {
|
||||||
|
// Find previous character index
|
||||||
|
let mut prev_index = 0;
|
||||||
|
for (i, _) in line.text().grapheme_indices(true) {
|
||||||
|
if i < self.cursor.index {
|
||||||
|
prev_index = i;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cursor.index = prev_index;
|
||||||
|
self.buffer.redraw = true;
|
||||||
|
} else if self.cursor.line > 0 {
|
||||||
|
self.cursor.line -= 1;
|
||||||
|
self.cursor.index = self.buffer.lines[self.cursor.line].text().len();
|
||||||
|
self.buffer.redraw = true;
|
||||||
|
}
|
||||||
|
self.cursor_x_opt = None;
|
||||||
|
},
|
||||||
|
Action::Next => {
|
||||||
|
let line = &mut self.buffer.lines[self.cursor.line];
|
||||||
|
if self.cursor.index < line.text().len() {
|
||||||
|
for (i, c) in line.text().grapheme_indices(true) {
|
||||||
|
if i == self.cursor.index {
|
||||||
|
self.cursor.index += c.len();
|
||||||
|
self.buffer.redraw = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if self.cursor.line + 1 < self.buffer.lines.len() {
|
||||||
|
self.cursor.line += 1;
|
||||||
|
self.cursor.index = 0;
|
||||||
|
self.buffer.redraw = true;
|
||||||
|
}
|
||||||
|
self.cursor_x_opt = None;
|
||||||
|
},
|
||||||
|
Action::Left => {
|
||||||
|
let rtl_opt = self.buffer.lines[self.cursor.line].shape_opt().as_ref().map(|shape| shape.rtl);
|
||||||
|
if let Some(rtl) = rtl_opt {
|
||||||
|
if rtl {
|
||||||
|
self.action(Action::Next);
|
||||||
|
} else {
|
||||||
|
self.action(Action::Previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Action::Right => {
|
||||||
|
let rtl_opt = self.buffer.lines[self.cursor.line].shape_opt().as_ref().map(|shape| shape.rtl);
|
||||||
|
if let Some(rtl) = rtl_opt {
|
||||||
|
if rtl {
|
||||||
|
self.action(Action::Previous);
|
||||||
|
} else {
|
||||||
|
self.action(Action::Next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Action::Up => {
|
||||||
|
//TODO: make this preserve X as best as possible!
|
||||||
|
let mut cursor = self.buffer.layout_cursor(&self.cursor);
|
||||||
|
|
||||||
|
if self.cursor_x_opt.is_none() {
|
||||||
|
self.cursor_x_opt = Some(
|
||||||
|
cursor.glyph as i32 //TODO: glyph x position
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if cursor.layout > 0 {
|
||||||
|
cursor.layout -= 1;
|
||||||
|
} else if cursor.line > 0 {
|
||||||
|
cursor.line -= 1;
|
||||||
|
cursor.layout = usize::max_value();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(cursor_x) = self.cursor_x_opt {
|
||||||
|
cursor.glyph = cursor_x as usize; //TODO: glyph x position
|
||||||
|
}
|
||||||
|
|
||||||
|
self.set_layout_cursor(cursor);
|
||||||
|
},
|
||||||
|
Action::Down => {
|
||||||
|
//TODO: make this preserve X as best as possible!
|
||||||
|
let mut cursor = self.buffer.layout_cursor(&self.cursor);
|
||||||
|
|
||||||
|
let layout_len = self.buffer.line_layout(cursor.line).unwrap().len();
|
||||||
|
|
||||||
|
if self.cursor_x_opt.is_none() {
|
||||||
|
self.cursor_x_opt = Some(
|
||||||
|
cursor.glyph as i32 //TODO: glyph x position
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if cursor.layout + 1 < layout_len {
|
||||||
|
cursor.layout += 1;
|
||||||
|
} else if cursor.line + 1 < self.buffer.lines.len() {
|
||||||
|
cursor.line += 1;
|
||||||
|
cursor.layout = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(cursor_x) = self.cursor_x_opt {
|
||||||
|
cursor.glyph = cursor_x as usize; //TODO: glyph x position
|
||||||
|
}
|
||||||
|
|
||||||
|
self.set_layout_cursor(cursor);
|
||||||
|
},
|
||||||
|
Action::Home => {
|
||||||
|
let mut cursor = self.buffer.layout_cursor(&self.cursor);
|
||||||
|
cursor.glyph = 0;
|
||||||
|
self.set_layout_cursor(cursor);
|
||||||
|
self.cursor_x_opt = None;
|
||||||
|
},
|
||||||
|
Action::End => {
|
||||||
|
let mut cursor = self.buffer.layout_cursor(&self.cursor);
|
||||||
|
cursor.glyph = usize::max_value();
|
||||||
|
self.set_layout_cursor(cursor);
|
||||||
|
self.cursor_x_opt = None;
|
||||||
|
}
|
||||||
|
Action::PageUp => {
|
||||||
|
//TODO: move cursor
|
||||||
|
let mut scroll = self.buffer.scroll();
|
||||||
|
scroll -= self.buffer.visible_lines();
|
||||||
|
self.buffer.set_scroll(scroll);
|
||||||
|
},
|
||||||
|
Action::PageDown => {
|
||||||
|
//TODO: move cursor
|
||||||
|
let mut scroll = self.buffer.scroll();
|
||||||
|
scroll += self.buffer.visible_lines();
|
||||||
|
self.buffer.set_scroll(scroll);
|
||||||
|
},
|
||||||
|
Action::Insert(character) => {
|
||||||
|
if character.is_control()
|
||||||
|
&& !['\t', '\u{92}'].contains(&character)
|
||||||
|
{
|
||||||
|
// Filter out special chars (except for tab), use Action instead
|
||||||
|
log::debug!("Refusing to insert control character {:?}", character);
|
||||||
|
} else {
|
||||||
|
let line = &mut self.buffer.lines[self.cursor.line];
|
||||||
|
|
||||||
|
println!("Before");
|
||||||
|
for span in line.attrs_list().spans() {
|
||||||
|
println!("{:?}", span);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect text after insertion as a line
|
||||||
|
let after = line.split_off(self.cursor.index);
|
||||||
|
|
||||||
|
// Append the inserted text
|
||||||
|
line.append(BufferLine::new(
|
||||||
|
character.to_string(),
|
||||||
|
AttrsList::new(line.attrs_list().defaults() /*TODO: provide attrs?*/)
|
||||||
|
));
|
||||||
|
|
||||||
|
// Append the text after insertion
|
||||||
|
line.append(after);
|
||||||
|
|
||||||
|
println!("After");
|
||||||
|
for span in line.attrs_list().spans() {
|
||||||
|
println!("{:?}", span);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cursor.index += character.len_utf8();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Action::Enter => {
|
||||||
|
let new_line = self.buffer.lines[self.cursor.line].split_off(self.cursor.index);
|
||||||
|
|
||||||
|
self.cursor.line += 1;
|
||||||
|
self.cursor.index = 0;
|
||||||
|
|
||||||
|
self.buffer.lines.insert(self.cursor.line, new_line);
|
||||||
|
},
|
||||||
|
Action::Backspace => {
|
||||||
|
if self.cursor.index > 0 {
|
||||||
|
let line = &mut self.buffer.lines[self.cursor.line];
|
||||||
|
|
||||||
|
println!("Before");
|
||||||
|
for span in line.attrs_list().spans() {
|
||||||
|
println!("{:?}", span);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get text line after cursor
|
||||||
|
let after = line.split_off(self.cursor.index);
|
||||||
|
|
||||||
|
// Find previous character index
|
||||||
|
let mut prev_index = 0;
|
||||||
|
for (i, _) in line.text().char_indices() {
|
||||||
|
if i < self.cursor.index {
|
||||||
|
prev_index = i;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Move cursor {} to {}", self.cursor.index, prev_index);
|
||||||
|
|
||||||
|
self.cursor.index = prev_index;
|
||||||
|
|
||||||
|
// Remove character
|
||||||
|
line.split_off(self.cursor.index);
|
||||||
|
|
||||||
|
// Add text after cursor
|
||||||
|
line.append(after);
|
||||||
|
|
||||||
|
println!("After");
|
||||||
|
for span in line.attrs_list().spans() {
|
||||||
|
println!("{:?}", span);
|
||||||
|
}
|
||||||
|
} else if self.cursor.line > 0 {
|
||||||
|
let mut line_index = self.cursor.line;
|
||||||
|
let old_line = self.buffer.lines.remove(line_index);
|
||||||
|
line_index -= 1;
|
||||||
|
|
||||||
|
let line = &mut self.buffer.lines[line_index];
|
||||||
|
|
||||||
|
self.cursor.line = line_index;
|
||||||
|
self.cursor.index = line.text().len();
|
||||||
|
|
||||||
|
line.append(old_line);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Action::Delete => {
|
||||||
|
if self.cursor.index < self.buffer.lines[self.cursor.line].text().len() {
|
||||||
|
let line = &mut self.buffer.lines[self.cursor.line];
|
||||||
|
|
||||||
|
let range_opt = line
|
||||||
|
.text()
|
||||||
|
.grapheme_indices(true)
|
||||||
|
.take_while(|(i, _)| *i <= self.cursor.index)
|
||||||
|
.last()
|
||||||
|
.map(|(i, c)| {
|
||||||
|
i..(i + c.len())
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(range) = range_opt {
|
||||||
|
self.cursor.index = range.start;
|
||||||
|
|
||||||
|
// Get text after deleted EGC
|
||||||
|
let after = line.split_off(range.end);
|
||||||
|
|
||||||
|
// Delete EGC
|
||||||
|
line.split_off(range.start);
|
||||||
|
|
||||||
|
// Add text after deleted EGC
|
||||||
|
line.append(after);
|
||||||
|
}
|
||||||
|
} else if self.cursor.line + 1 < self.buffer.lines.len() {
|
||||||
|
let old_line = self.buffer.lines.remove(self.cursor.line + 1);
|
||||||
|
self.buffer.lines[self.cursor.line].append(old_line);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Action::Click { x, y } => {
|
||||||
|
self.select_opt = None;
|
||||||
|
|
||||||
|
if let Some(new_cursor) = self.buffer.hit(x, y) {
|
||||||
|
if new_cursor != self.cursor {
|
||||||
|
self.cursor = new_cursor;
|
||||||
|
self.buffer.redraw = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Action::Drag { x, y } => {
|
||||||
|
if self.select_opt.is_none() {
|
||||||
|
self.select_opt = Some(self.cursor);
|
||||||
|
self.buffer.redraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(new_cursor) = self.buffer.hit(x, y) {
|
||||||
|
if new_cursor != self.cursor {
|
||||||
|
self.cursor = new_cursor;
|
||||||
|
self.buffer.redraw = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Action::Scroll { lines } => {
|
||||||
|
let mut scroll = self.buffer.scroll();
|
||||||
|
scroll += lines;
|
||||||
|
self.buffer.set_scroll(scroll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if old_cursor != self.cursor {
|
||||||
|
self.cursor_moved = true;
|
||||||
|
|
||||||
|
/*TODO
|
||||||
|
if let Some(glyph) = run.glyphs.get(new_cursor_glyph) {
|
||||||
|
let font_opt = self.buffer.font_system().get_font(glyph.cache_key.font_id);
|
||||||
|
let text_glyph = &run.text[glyph.start..glyph.end];
|
||||||
|
log::debug!(
|
||||||
|
"{}, {}: '{}' ('{}'): '{}' ({:?})",
|
||||||
|
self.cursor.line,
|
||||||
|
self.cursor.index,
|
||||||
|
font_opt.as_ref().map_or("?", |font| font.info.family.as_str()),
|
||||||
|
font_opt.as_ref().map_or("?", |font| font.info.post_script_name.as_str()),
|
||||||
|
text_glyph,
|
||||||
|
text_glyph
|
||||||
|
);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw the editor
|
||||||
|
#[cfg(feature = "swash")]
|
||||||
|
pub fn draw<F>(&self, cache: &mut crate::SwashCache, color: Color, mut f: F)
|
||||||
|
where F: FnMut(i32, i32, u32, u32, Color)
|
||||||
|
{
|
||||||
|
let font_size = self.buffer.metrics().font_size;
|
||||||
|
let line_height = self.buffer.metrics().line_height;
|
||||||
|
|
||||||
|
for run in self.buffer.layout_runs() {
|
||||||
|
let line_i = run.line_i;
|
||||||
|
let line_y = run.line_y;
|
||||||
|
|
||||||
|
let cursor_glyph_opt = |cursor: &Cursor| -> Option<(usize, f32)> {
|
||||||
|
if cursor.line == line_i {
|
||||||
|
for (glyph_i, glyph) in run.glyphs.iter().enumerate() {
|
||||||
|
if cursor.index == glyph.start {
|
||||||
|
return Some((glyph_i, 0.0));
|
||||||
|
} else if cursor.index > glyph.start && cursor.index < glyph.end {
|
||||||
|
// Guess x offset based on characters
|
||||||
|
let mut before = 0;
|
||||||
|
let mut total = 0;
|
||||||
|
|
||||||
|
let cluster = &run.text[glyph.start..glyph.end];
|
||||||
|
for (i, _) in cluster.grapheme_indices(true) {
|
||||||
|
if glyph.start + i < cursor.index {
|
||||||
|
before += 1;
|
||||||
|
}
|
||||||
|
total += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let offset = glyph.w * (before as f32) / (total as f32);
|
||||||
|
return Some((glyph_i, offset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match run.glyphs.last() {
|
||||||
|
Some(glyph) => {
|
||||||
|
if cursor.index == glyph.end {
|
||||||
|
return Some((run.glyphs.len(), 0.0));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
return Some((0, 0.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Highlight selection (TODO: HIGHLIGHT COLOR!)
|
||||||
|
if let Some(select) = self.select_opt {
|
||||||
|
let (start, end) = match select.line.cmp(&self.cursor.line) {
|
||||||
|
cmp::Ordering::Greater => (self.cursor, select),
|
||||||
|
cmp::Ordering::Less => (select, self.cursor),
|
||||||
|
cmp::Ordering::Equal => {
|
||||||
|
/* select.line == self.cursor.line */
|
||||||
|
if select.index < self.cursor.index {
|
||||||
|
(select, self.cursor)
|
||||||
|
} else {
|
||||||
|
/* select.index >= self.cursor.index */
|
||||||
|
(self.cursor, select)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if line_i >= start.line && line_i <= end.line {
|
||||||
|
let mut range_opt = None;
|
||||||
|
for glyph in run.glyphs.iter() {
|
||||||
|
// Guess x offset based on characters
|
||||||
|
let cluster = &run.text[glyph.start..glyph.end];
|
||||||
|
let total = cluster.grapheme_indices(true).count();
|
||||||
|
let mut c_x = glyph.x;
|
||||||
|
let c_w = glyph.w / total as f32;
|
||||||
|
for (i, c) in cluster.grapheme_indices(true) {
|
||||||
|
let c_start = glyph.start + i;
|
||||||
|
let c_end = glyph.start + i + c.len();
|
||||||
|
if (start.line != line_i || c_end > start.index)
|
||||||
|
&& (end.line != line_i || c_start < end.index) {
|
||||||
|
range_opt = match range_opt.take() {
|
||||||
|
Some((min, max)) => Some((
|
||||||
|
cmp::min(min, c_x as i32),
|
||||||
|
cmp::max(max, (c_x + c_w) as i32),
|
||||||
|
)),
|
||||||
|
None => Some((
|
||||||
|
c_x as i32,
|
||||||
|
(c_x + c_w) as i32,
|
||||||
|
))
|
||||||
|
};
|
||||||
|
} else if let Some((min, max)) = range_opt.take() {
|
||||||
|
f(
|
||||||
|
min,
|
||||||
|
line_y - font_size,
|
||||||
|
cmp::max(0, max - min) as u32,
|
||||||
|
line_height as u32,
|
||||||
|
Color::rgba(color.r(), color.g(), color.b(), 0x33)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
c_x += c_w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if run.glyphs.is_empty() && end.line > line_i{
|
||||||
|
// Highlight all of internal empty lines
|
||||||
|
range_opt = Some((0, self.buffer.size().0));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((mut min, mut max)) = range_opt.take() {
|
||||||
|
if end.line > line_i {
|
||||||
|
// Draw to end of line
|
||||||
|
if run.rtl {
|
||||||
|
min = 0;
|
||||||
|
} else {
|
||||||
|
max = self.buffer.size().0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f(
|
||||||
|
min,
|
||||||
|
line_y - font_size,
|
||||||
|
cmp::max(0, max - min) as u32,
|
||||||
|
line_height as u32,
|
||||||
|
Color::rgba(color.r(), color.g(), color.b(), 0x33)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw cursor
|
||||||
|
if let Some((cursor_glyph, cursor_glyph_offset)) = cursor_glyph_opt(&self.cursor) {
|
||||||
|
let x = match run.glyphs.get(cursor_glyph) {
|
||||||
|
Some(glyph) => {
|
||||||
|
// Start of detected glyph
|
||||||
|
if glyph.rtl {
|
||||||
|
(glyph.x + glyph.w - cursor_glyph_offset) as i32
|
||||||
|
} else {
|
||||||
|
(glyph.x + cursor_glyph_offset) as i32
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => match run.glyphs.last() {
|
||||||
|
Some(glyph) => {
|
||||||
|
// End of last glyph
|
||||||
|
if glyph.rtl {
|
||||||
|
glyph.x as i32
|
||||||
|
} else {
|
||||||
|
(glyph.x + glyph.w) as i32
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
// Start of empty line
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
f(
|
||||||
|
x,
|
||||||
|
line_y - font_size,
|
||||||
|
1,
|
||||||
|
line_height as u32,
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for glyph in run.glyphs.iter() {
|
||||||
|
let (cache_key, x_int, y_int) = (glyph.cache_key, glyph.x_int, glyph.y_int);
|
||||||
|
|
||||||
|
let glyph_color = match glyph.color_opt {
|
||||||
|
Some(some) => some,
|
||||||
|
None => color,
|
||||||
|
};
|
||||||
|
|
||||||
|
cache.with_pixels(cache_key, glyph_color, |x, y, color| {
|
||||||
|
f(x_int + x, line_y + y_int + y, 1, 1, color)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/lib.rs
23
src/lib.rs
|
|
@ -8,11 +8,11 @@
|
||||||
//! swash. The other features are developed internal to this library.
|
//! swash. The other features are developed internal to this library.
|
||||||
//!
|
//!
|
||||||
//! It is recommended that you start by creating a [FontSystem], after which you can create a
|
//! It is recommended that you start by creating a [FontSystem], after which you can create a
|
||||||
//! [TextBuffer], provide it with some text, and then inspect the layout it produces. At this
|
//! [Buffer], provide it with some text, and then inspect the layout it produces. At this
|
||||||
//! point, you can use the `SwashCache` to rasterize glyphs into either images or pixels.
|
//! point, you can use the `SwashCache` to rasterize glyphs into either images or pixels.
|
||||||
//!
|
//!
|
||||||
//! ```
|
//! ```
|
||||||
//! use cosmic_text::{Attrs, Color, FontSystem, SwashCache, TextBuffer, TextMetrics};
|
//! use cosmic_text::{Attrs, Color, FontSystem, SwashCache, Buffer, Metrics};
|
||||||
//!
|
//!
|
||||||
//! // A FontSystem provides access to detected system fonts, create one per application
|
//! // A FontSystem provides access to detected system fonts, create one per application
|
||||||
//! let font_system = FontSystem::new();
|
//! let font_system = FontSystem::new();
|
||||||
|
|
@ -21,25 +21,25 @@
|
||||||
//! let mut swash_cache = SwashCache::new(&font_system);
|
//! let mut swash_cache = SwashCache::new(&font_system);
|
||||||
//!
|
//!
|
||||||
//! // Text metrics indicate the font size and line height of a buffer
|
//! // Text metrics indicate the font size and line height of a buffer
|
||||||
//! let metrics = TextMetrics::new(14, 20);
|
//! let metrics = Metrics::new(14, 20);
|
||||||
//!
|
//!
|
||||||
//! // A TextBuffer provides shaping and layout for a UTF-8 string, create one per text widget
|
//! // A Buffer provides shaping and layout for a UTF-8 string, create one per text widget
|
||||||
//! let mut text_buffer = TextBuffer::new(&font_system, metrics);
|
//! let mut buffer = Buffer::new(&font_system, metrics);
|
||||||
//!
|
//!
|
||||||
//! // Set a size for the text buffer, in pixels
|
//! // Set a size for the text buffer, in pixels
|
||||||
//! text_buffer.set_size(80, 25);
|
//! buffer.set_size(80, 25);
|
||||||
//!
|
//!
|
||||||
//! // Attributes indicate what font to choose
|
//! // Attributes indicate what font to choose
|
||||||
//! let attrs = Attrs::new();
|
//! let attrs = Attrs::new();
|
||||||
//!
|
//!
|
||||||
//! // Add some text!
|
//! // Add some text!
|
||||||
//! text_buffer.set_text("Hello, Rust! 🦀\n", attrs);
|
//! buffer.set_text("Hello, Rust! 🦀\n", attrs);
|
||||||
//!
|
//!
|
||||||
//! // Perform shaping as desired
|
//! // Perform shaping as desired
|
||||||
//! text_buffer.shape_until_scroll();
|
//! buffer.shape_until_scroll();
|
||||||
//!
|
//!
|
||||||
//! // Inspect the output runs
|
//! // Inspect the output runs
|
||||||
//! for run in text_buffer.layout_runs() {
|
//! for run in buffer.layout_runs() {
|
||||||
//! for glyph in run.glyphs.iter() {
|
//! for glyph in run.glyphs.iter() {
|
||||||
//! println!("{:#?}", glyph);
|
//! println!("{:#?}", glyph);
|
||||||
//! }
|
//! }
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
//! let text_color = Color::rgb(0xFF, 0xFF, 0xFF);
|
//! let text_color = Color::rgb(0xFF, 0xFF, 0xFF);
|
||||||
//!
|
//!
|
||||||
//! // Draw the buffer (for perfomance, instead use SwashCache directly)
|
//! // Draw the buffer (for perfomance, instead use SwashCache directly)
|
||||||
//! text_buffer.draw(&mut swash_cache, text_color, |x, y, w, h, color| {
|
//! buffer.draw(&mut swash_cache, text_color, |x, y, w, h, color| {
|
||||||
//! // Fill in your code here for drawing rectangles
|
//! // Fill in your code here for drawing rectangles
|
||||||
//! });
|
//! });
|
||||||
//! ```
|
//! ```
|
||||||
|
|
@ -66,6 +66,9 @@ mod buffer_line;
|
||||||
pub use self::cache::*;
|
pub use self::cache::*;
|
||||||
mod cache;
|
mod cache;
|
||||||
|
|
||||||
|
pub use self::editor::*;
|
||||||
|
mod editor;
|
||||||
|
|
||||||
pub use self::font::*;
|
pub use self::font::*;
|
||||||
mod font;
|
mod font;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue