Vi-style editor and other editor improvements (#40)

* WIP VI wrapper for editor

* WIP: block cursor

* Create Edit trait, run CI on all feature options

* Add prints describing build steps to ci.sh

* Custom rendering for Vi editor

* Clippy fixes

* More clippy fixes

* Show clippy results in CI

* Fix for Redox

* Fix clippy lint

* Add vi feature to enable vi-style editor

* Add escape to libcosmic text box
This commit is contained in:
Jeremy Soller 2022-11-15 12:26:59 -07:00 committed by GitHub
parent 271ca5cf7a
commit ee54e7626b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 982 additions and 616 deletions

View file

@ -23,3 +23,7 @@ version = "0.10"
#TODO: iced portal
#default-features = false
#features = ["xdg-portal"]
[features]
default = []
vi = ["cosmic-text/vi"]

View file

@ -27,6 +27,7 @@ use cosmic_text::{
Attrs,
AttrsList,
Buffer,
Edit,
FontSystem,
Metrics,
SyntaxEditor,
@ -39,9 +40,6 @@ use std::{
sync::Mutex,
};
use self::syntax_text_box::syntax_text_box;
mod syntax_text_box;
use self::text::text;
mod text;
@ -74,7 +72,10 @@ pub struct Window {
theme: Theme,
path_opt: Option<PathBuf>,
attrs: Attrs<'static>,
#[cfg(not(feature = "vi"))]
editor: Mutex<SyntaxEditor<'static>>,
#[cfg(feature = "vi")]
editor: Mutex<cosmic_text::ViEditor<'static>>,
}
#[allow(dead_code)]
@ -121,6 +122,10 @@ impl Application for Window {
&SYNTAX_SYSTEM,
"base16-eighties.dark"
).unwrap();
#[cfg(feature = "vi")]
let mut editor = cosmic_text::ViEditor::new(editor);
update_attrs(&mut editor, attrs);
let mut window = Window {
@ -180,7 +185,7 @@ impl Application for Window {
});
let mut editor = self.editor.lock().unwrap();
update_attrs(&mut editor, self.attrs);
update_attrs(&mut *editor, self.attrs);
},
Message::Italic(italic) => {
self.attrs = self.attrs.style(if italic {
@ -190,7 +195,7 @@ impl Application for Window {
});
let mut editor = self.editor.lock().unwrap();
update_attrs(&mut editor, self.attrs);
update_attrs(&mut *editor, self.attrs);
},
Message::Monospaced(monospaced) => {
self.attrs = self.attrs
@ -202,7 +207,7 @@ impl Application for Window {
.monospaced(monospaced);
let mut editor = self.editor.lock().unwrap();
update_attrs(&mut editor, self.attrs);
update_attrs(&mut *editor, self.attrs);
},
Message::MetricsChanged(metrics) => {
let mut editor = self.editor.lock().unwrap();
@ -220,7 +225,7 @@ impl Application for Window {
self.attrs = self.attrs.color(cosmic_text::Color::rgba(as_u8(r), as_u8(g), as_u8(b), as_u8(a)));
let mut editor = self.editor.lock().unwrap();
update_attrs(&mut editor, self.attrs);
update_attrs(&mut *editor, self.attrs);
},
}
@ -266,7 +271,7 @@ impl Application for Window {
.align_items(Alignment::Center)
.spacing(8)
,
syntax_text_box(&self.editor)
text_box(&self.editor)
]
.spacing(8)
.padding(16)
@ -277,7 +282,7 @@ impl Application for Window {
}
}
fn update_attrs<'a>(editor: &mut SyntaxEditor<'a>, attrs: Attrs<'a>) {
fn update_attrs<'a, T: Edit<'a>>(editor: &mut T, attrs: Attrs<'a>) {
editor.buffer_mut().lines.iter_mut().for_each(|line| {
line.set_attrs_list(AttrsList::new(attrs));
});

View file

@ -1,304 +0,0 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use cosmic::{
iced_native::{
{Color, Element, Length, Point, Rectangle, Shell, Size},
clipboard::Clipboard,
event::{Event, Status},
image,
keyboard::{Event as KeyEvent, KeyCode},
layout::{self, Layout},
mouse::{self, Button, Event as MouseEvent, ScrollDelta},
renderer,
widget::{self, tree, Widget},
Padding
},
};
use cosmic_text::{
Action,
SyntaxEditor,
SwashCache,
};
use std::{
cmp,
sync::Mutex,
time::Instant,
};
use super::text;
pub struct SyntaxTextBox<'a> {
editor: &'a Mutex<SyntaxEditor<'static>>,
padding: Padding,
}
impl<'a> SyntaxTextBox<'a> {
pub fn new(editor: &'a Mutex<SyntaxEditor<'static>>) -> Self {
Self {
editor,
padding: Padding::new(0),
}
}
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
self
}
}
pub fn syntax_text_box<'a>(editor: &'a Mutex<SyntaxEditor<'static>>) -> SyntaxTextBox<'a> {
SyntaxTextBox::new(editor)
}
impl<'a, Message, Renderer> Widget<Message, Renderer> for SyntaxTextBox<'a>
where
Renderer: renderer::Renderer + image::Renderer<Handle = image::Handle>,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::new())
}
fn width(&self) -> Length {
Length::Fill
}
fn height(&self) -> Length {
Length::Fill
}
fn layout(
&self,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits.width(Length::Fill).height(Length::Fill);
//TODO: allow lazy shape
let mut editor = self.editor.lock().unwrap();
editor.buffer_mut().shape_until(i32::max_value());
let mut layout_lines = 0;
for line in editor.buffer().lines.iter() {
match line.layout_opt() {
Some(layout) => layout_lines += layout.len(),
None => (),
}
}
let height = layout_lines as f32 * editor.buffer().metrics().line_height as f32;
let size = Size::new(limits.max().width, height);
log::info!("size {:?}", size);
layout::Node::new(limits.resolve(size))
}
fn mouse_interaction(
&self,
_tree: &widget::Tree,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
if layout.bounds().contains(cursor_position) {
mouse::Interaction::Text
} else {
mouse::Interaction::Idle
}
}
fn draw(
&self,
tree: &widget::Tree,
renderer: &mut Renderer,
_theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor_position: Point,
viewport: &Rectangle,
) {
let state = tree.state.downcast_ref::<State>();
let mut editor = self.editor.lock().unwrap();
let view_w = cmp::min(viewport.width as i32, layout.bounds().width as i32) - self.padding.horizontal() as i32;
let view_h = cmp::min(viewport.height as i32, layout.bounds().height as i32)- self.padding.vertical() as i32;
editor.buffer_mut().set_size(view_w, view_h);
editor.shape_as_needed();
let instant = Instant::now();
let mut pixels = vec![0; view_w as usize * view_h as usize * 4];
editor.draw(&mut state.cache.lock().unwrap(), |x, y, w, h, color| {
if w <= 0 || h <= 0 {
// Do not draw invalid sized rectangles
return;
}
if w > 1 || h > 1 {
// Draw rectangles with optimized quad renderer
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle::new(
layout.position() + [x as f32, y as f32].into() + [self.padding.left as f32, self.padding.top as f32].into(),
Size::new(w as f32, h as f32)
),
border_radius: 0.0,
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
Color::from_rgba8(
color.r(),
color.g(),
color.b(),
(color.a() as f32) / 255.0
)
);
} else {
text::draw_pixel(&mut pixels, view_w, view_h, x, y, color);
}
});
let handle = image::Handle::from_pixels(view_w as u32, view_h as u32, pixels);
image::Renderer::draw(renderer, handle, Rectangle::new(
layout.position() + [self.padding.left as f32, self.padding.top as f32].into(),
Size::new(view_w as f32, view_h as f32)
));
let duration = instant.elapsed();
log::debug!("redraw {}, {}: {:?}", view_w, view_h, duration);
}
fn on_event(
&mut self,
tree: &mut widget::Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
_shell: &mut Shell<'_, Message>,
) -> Status {
let state = tree.state.downcast_mut::<State>();
let mut editor = self.editor.lock().unwrap();
let mut status = Status::Ignored;
match event {
Event::Keyboard(KeyEvent::KeyPressed { key_code, modifiers }) => match key_code {
KeyCode::Left => {
editor.action(Action::Left);
status = Status::Captured;
},
KeyCode::Right => {
editor.action(Action::Right);
status = Status::Captured;
},
KeyCode::Up => {
editor.action(Action::Up);
status = Status::Captured;
},
KeyCode::Down => {
editor.action(Action::Down);
status = Status::Captured;
},
KeyCode::Home => {
editor.action(Action::Home);
status = Status::Captured;
},
KeyCode::End => {
editor.action(Action::End);
status = Status::Captured;
},
KeyCode::PageUp => {
editor.action(Action::PageUp);
status = Status::Captured;
},
KeyCode::PageDown => {
editor.action(Action::PageDown);
status = Status::Captured;
},
KeyCode::Enter => {
editor.action(Action::Enter);
status = Status::Captured;
},
KeyCode::Backspace => {
editor.action(Action::Backspace);
status = Status::Captured;
},
KeyCode::Delete => {
editor.action(Action::Delete);
status = Status::Captured;
},
_ => ()
},
Event::Keyboard(KeyEvent::CharacterReceived(character)) => {
editor.action(Action::Insert(character));
status = Status::Captured;
},
Event::Mouse(MouseEvent::ButtonPressed(Button::Left)) => {
if layout.bounds().contains(cursor_position) {
editor.action(Action::Click {
x: (cursor_position.x - layout.bounds().x) as i32 - self.padding.left as i32,
y: (cursor_position.y - layout.bounds().y) as i32 - self.padding.top as i32,
});
state.is_dragging = true;
status = Status::Captured;
}
},
Event::Mouse(MouseEvent::ButtonReleased(Button::Left)) => {
state.is_dragging = false;
status = Status::Captured;
},
Event::Mouse(MouseEvent::CursorMoved { .. }) => {
if state.is_dragging {
editor.action(Action::Drag {
x: (cursor_position.x - layout.bounds().x) as i32 - self.padding.left as i32,
y: (cursor_position.y - layout.bounds().y) as i32 - self.padding.top as i32,
});
status = Status::Captured;
}
},
Event::Mouse(MouseEvent::WheelScrolled { delta }) => match delta {
ScrollDelta::Lines { x, y } => {
editor.action(Action::Scroll {
lines: (-y * 6.0) as i32,
});
status = Status::Captured;
},
_ => (),
},
_ => ()
}
status
}
}
impl<'a, Message, Renderer> From<SyntaxTextBox<'a>> for Element<'a, Message, Renderer>
where
Renderer: renderer::Renderer + image::Renderer<Handle = image::Handle>,
{
fn from(text_box: SyntaxTextBox<'a>) -> Self {
Self::new(text_box)
}
}
pub struct State {
is_dragging: bool,
cache: Mutex<SwashCache<'static>>,
}
impl State {
/// Creates a new [`State`].
pub fn new() -> State {
State {
is_dragging: false,
cache: Mutex::new(SwashCache::new(&crate::FONT_SYSTEM)),
}
}
}

View file

@ -17,7 +17,7 @@ use cosmic::{
};
use cosmic_text::{
Action,
Editor,
Edit,
SwashCache,
};
use std::{
@ -51,34 +51,35 @@ impl StyleSheet for Theme {
}
}
pub struct TextBox<'a> {
editor: &'a Mutex<Editor<'static>>,
pub struct TextBox<'a, Editor> {
editor: &'a Mutex<Editor>,
padding: Padding,
}
impl<'a> TextBox<'a> {
pub fn new(editor: &'a Mutex<Editor<'static>>) -> Self {
impl<'a, Editor> TextBox<'a, Editor> {
pub fn new(editor: &'a Mutex<Editor>) -> Self {
Self {
editor,
padding: Padding::new(0),
}
}
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
self
}
}
pub fn text_box<'a>(editor: &'a Mutex<Editor<'static>>) -> TextBox<'a> {
pub fn text_box<'a, Editor>(editor: &'a Mutex<Editor>) -> TextBox<'a, Editor> {
TextBox::new(editor)
}
impl<'a, Message, Renderer> Widget<Message, Renderer> for TextBox<'a>
impl<'a, 'editor, Editor, Message, Renderer> Widget<Message, Renderer> for TextBox<'a, Editor>
where
Renderer: renderer::Renderer + image::Renderer<Handle = image::Handle>,
Renderer::Theme: StyleSheet,
Editor: Edit<'editor>,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
@ -105,17 +106,17 @@ where
//TODO: allow lazy shape
let mut editor = self.editor.lock().unwrap();
editor.buffer.shape_until(i32::max_value());
editor.buffer_mut().shape_until(i32::max_value());
let mut layout_lines = 0;
for line in editor.buffer.lines.iter() {
for line in editor.buffer().lines.iter() {
match line.layout_opt() {
Some(layout) => layout_lines += layout.len(),
None => (),
}
}
let height = layout_lines as f32 * editor.buffer.metrics().line_height as f32;
let height = layout_lines as f32 * editor.buffer().metrics().line_height as f32;
let size = Size::new(limits.max().width, height);
log::info!("size {:?}", size);
@ -174,7 +175,7 @@ where
let view_w = cmp::min(viewport.width as i32, layout.bounds().width as i32) - self.padding.horizontal() as i32;
let view_h = cmp::min(viewport.height as i32, layout.bounds().height as i32) - self.padding.vertical() as i32;
editor.buffer.set_size(view_w, view_h);
editor.buffer_mut().set_size(view_w, view_h);
editor.shape_as_needed();
@ -270,6 +271,10 @@ where
editor.action(Action::PageDown);
status = Status::Captured;
},
KeyCode::Escape => {
editor.action(Action::Escape);
status = Status::Captured;
},
KeyCode::Enter => {
editor.action(Action::Enter);
status = Status::Captured;
@ -327,12 +332,13 @@ where
}
}
impl<'a, Message, Renderer> From<TextBox<'a>> for Element<'a, Message, Renderer>
impl<'a, 'editor, Editor, Message, Renderer> From<TextBox<'a, Editor>> for Element<'a, Message, Renderer>
where
Renderer: renderer::Renderer + image::Renderer<Handle = image::Handle>,
Renderer::Theme: StyleSheet,
Editor: Edit<'editor>,
{
fn from(text_box: TextBox<'a>) -> Self {
fn from(text_box: TextBox<'a, Editor>) -> Self {
Self::new(text_box)
}
}

View file

@ -12,3 +12,8 @@ env_logger = "0.9"
fontdb = "0.9"
log = "0.4"
orbclient = "0.3.35"
unicode-segmentation = "1.7"
[features]
default = []
vi = ["cosmic-text/vi"]

View file

@ -4,6 +4,7 @@ use cosmic_text::{
Action,
Attrs,
Buffer,
Edit,
Family,
FontSystem,
Metrics,
@ -60,12 +61,16 @@ fn main() {
let mut font_size_i = font_size_default;
let line_x = 8 * display_scale;
let mut editor = SyntaxEditor::new(
Buffer::new(&font_system, font_sizes[font_size_i]),
&syntax_system,
"base16-eighties.dark"
).unwrap();
#[cfg(feature = "vi")]
let mut editor = cosmic_text::ViEditor::new(editor);
editor.buffer_mut().set_size(
window.width() as i32 - line_x * 2,
window.height() as i32
@ -89,13 +94,14 @@ fn main() {
let mut mouse_left = false;
loop {
editor.shape_as_needed();
if editor.buffer_mut().redraw {
if editor.buffer().redraw() {
let instant = Instant::now();
let bg = editor.background_color();
window.set(orbclient::Color::rgb(bg.r(), bg.g(), bg.b()));
editor.draw(&mut swash_cache, |x, y, w, h, color| {
let fg = editor.foreground_color();
editor.draw(&mut swash_cache, fg, |x, y, w, h, color| {
window.rect(line_x + x, y, w, h, orbclient::Color { data: color.0 })
});
@ -127,7 +133,7 @@ fn main() {
window.sync();
editor.buffer_mut().redraw = false;
editor.buffer_mut().set_redraw(false);
log::debug!("redraw: {:?}", instant.elapsed());
}
@ -148,6 +154,7 @@ fn main() {
orbclient::K_END if event.pressed => editor.action(Action::End),
orbclient::K_PGUP if event.pressed => editor.action(Action::PageUp),
orbclient::K_PGDN if event.pressed => editor.action(Action::PageDown),
orbclient::K_ESC if event.pressed => editor.action(Action::Escape),
orbclient::K_ENTER if event.pressed => editor.action(Action::Enter),
orbclient::K_BKSP if event.pressed => editor.action(Action::Backspace),
orbclient::K_DEL if event.pressed => editor.action(Action::Delete),

View file

@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use cosmic_text::{Action, Buffer, Color, Editor, FontSystem, Metrics, SwashCache};
use cosmic_text::{Action, Buffer, Color, Edit, Editor, FontSystem, Metrics, SwashCache};
use orbclient::{EventOption, Renderer, Window, WindowFlag};
use std::{env, fs, process, time::Instant};
use unicode_segmentation::UnicodeSegmentation;
@ -10,7 +10,7 @@ fn redraw(window: &mut Window, editor: &mut Editor<'_>, swash_cache: &mut SwashC
let font_color = Color::rgb(0xFF, 0xFF, 0xFF);
editor.shape_as_needed();
if editor.buffer.redraw {
if editor.buffer().redraw() {
let instant = Instant::now();
window.set(bg_color);
@ -21,7 +21,7 @@ fn redraw(window: &mut Window, editor: &mut Editor<'_>, swash_cache: &mut SwashC
window.sync();
editor.buffer.redraw = false;
editor.buffer_mut().set_redraw(false);
let duration = instant.elapsed();
log::debug!("redraw: {:?}", duration);
@ -146,7 +146,7 @@ fn main() {
let mut wrong = 0;
for (line_i, line) in text.lines().enumerate() {
let buffer_line = &editor.buffer.lines[line_i];
let buffer_line = &editor.buffer().lines[line_i];
if buffer_line.text() != line {
log::error!("line {}: {:?} != {:?}", line_i, buffer_line.text(), line);
wrong += 1;

View file

@ -7,6 +7,7 @@ use cosmic_text::{
Buffer,
BufferLine,
Color,
Edit,
Editor,
Family,
FontSystem,
@ -49,7 +50,7 @@ fn main() {
Metrics::new(32, 44).scale(display_scale)
));
editor.buffer.set_size(
editor.buffer_mut().set_size(
window.width() as i32,
window.height() as i32
);
@ -59,7 +60,7 @@ fn main() {
let mono_attrs = attrs.monospaced(true).family(Family::Monospace);
let comic_attrs = attrs.family(Family::Name("Comic Neue"));
editor.buffer.lines.clear();
editor.buffer_mut().lines.clear();
let lines: &[&[(&str, Attrs)]] = &[
&[
@ -131,7 +132,7 @@ fn main() {
let end = line_text.len();
attrs_list.add_span(start..end, attrs);
}
editor.buffer.lines.push(BufferLine::new(line_text, attrs_list));
editor.buffer_mut().lines.push(BufferLine::new(line_text, attrs_list));
}
let mut swash_cache = SwashCache::new(&font_system);
@ -145,7 +146,7 @@ fn main() {
let font_color = Color::rgb(0xFF, 0xFF, 0xFF);
editor.shape_as_needed();
if editor.buffer.redraw {
if editor.buffer().redraw() {
let instant = Instant::now();
window.set(bg_color);
@ -156,7 +157,7 @@ fn main() {
window.sync();
editor.buffer.redraw = false;
editor.buffer_mut().set_redraw(false);
let duration = instant.elapsed();
log::debug!("redraw: {:?}", duration);
@ -193,7 +194,7 @@ fn main() {
}
},
EventOption::Resize(resize) => {
editor.buffer.set_size(resize.width as i32, resize.height as i32);
editor.buffer_mut().set_size(resize.width as i32, resize.height as i32);
},
EventOption::Quit(_) => process::exit(0),
_ => (),