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:
parent
271ca5cf7a
commit
ee54e7626b
33 changed files with 982 additions and 616 deletions
12
.github/workflows/rust.yml
vendored
12
.github/workflows/rust.yml
vendored
|
|
@ -11,12 +11,12 @@ env:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Build
|
- uses: actions-rs/clippy-check@v1
|
||||||
run: cargo build --verbose
|
with:
|
||||||
- name: Run tests
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: cargo test --verbose
|
args: --all-features
|
||||||
|
- name: Run Rust CI
|
||||||
|
run: ./ci.sh
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-text"
|
name = "cosmic-text"
|
||||||
description = "Pure Rust multi-line text handling"
|
description = "Pure Rust multi-line text handling"
|
||||||
version = "0.4.1"
|
version = "0.5.0-alpha1"
|
||||||
authors = ["Jeremy Soller <jeremy@system76.com>"]
|
authors = ["Jeremy Soller <jeremy@system76.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
|
|
@ -39,6 +39,7 @@ std = [
|
||||||
"sys-locale",
|
"sys-locale",
|
||||||
"unicode-bidi/std",
|
"unicode-bidi/std",
|
||||||
]
|
]
|
||||||
|
vi = ["syntect"]
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
|
|
|
||||||
32
ci.sh
Executable file
32
ci.sh
Executable file
|
|
@ -0,0 +1,32 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
function build {
|
||||||
|
cargo build --release "$@"
|
||||||
|
cargo clippy --no-deps "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
echo Build with default features
|
||||||
|
build
|
||||||
|
|
||||||
|
echo Build with no default features
|
||||||
|
build --no-default-features
|
||||||
|
|
||||||
|
echo Build with only std feature
|
||||||
|
build --no-default-features --features std
|
||||||
|
|
||||||
|
echo Build with only swash feature
|
||||||
|
build --no-default-features --features swash
|
||||||
|
|
||||||
|
echo Build with only syntect feature
|
||||||
|
build --no-default-features --features syntect
|
||||||
|
|
||||||
|
echo Build with only vi feature
|
||||||
|
build --no-default-features --features vi
|
||||||
|
|
||||||
|
echo Build with all features
|
||||||
|
build --all-features
|
||||||
|
|
||||||
|
echo Run tests
|
||||||
|
cargo test
|
||||||
|
|
@ -23,3 +23,7 @@ version = "0.10"
|
||||||
#TODO: iced portal
|
#TODO: iced portal
|
||||||
#default-features = false
|
#default-features = false
|
||||||
#features = ["xdg-portal"]
|
#features = ["xdg-portal"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
vi = ["cosmic-text/vi"]
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ use cosmic_text::{
|
||||||
Attrs,
|
Attrs,
|
||||||
AttrsList,
|
AttrsList,
|
||||||
Buffer,
|
Buffer,
|
||||||
|
Edit,
|
||||||
FontSystem,
|
FontSystem,
|
||||||
Metrics,
|
Metrics,
|
||||||
SyntaxEditor,
|
SyntaxEditor,
|
||||||
|
|
@ -39,9 +40,6 @@ use std::{
|
||||||
sync::Mutex,
|
sync::Mutex,
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::syntax_text_box::syntax_text_box;
|
|
||||||
mod syntax_text_box;
|
|
||||||
|
|
||||||
use self::text::text;
|
use self::text::text;
|
||||||
mod text;
|
mod text;
|
||||||
|
|
||||||
|
|
@ -74,7 +72,10 @@ pub struct Window {
|
||||||
theme: Theme,
|
theme: Theme,
|
||||||
path_opt: Option<PathBuf>,
|
path_opt: Option<PathBuf>,
|
||||||
attrs: Attrs<'static>,
|
attrs: Attrs<'static>,
|
||||||
|
#[cfg(not(feature = "vi"))]
|
||||||
editor: Mutex<SyntaxEditor<'static>>,
|
editor: Mutex<SyntaxEditor<'static>>,
|
||||||
|
#[cfg(feature = "vi")]
|
||||||
|
editor: Mutex<cosmic_text::ViEditor<'static>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
|
@ -121,6 +122,10 @@ impl Application for Window {
|
||||||
&SYNTAX_SYSTEM,
|
&SYNTAX_SYSTEM,
|
||||||
"base16-eighties.dark"
|
"base16-eighties.dark"
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
|
||||||
|
#[cfg(feature = "vi")]
|
||||||
|
let mut editor = cosmic_text::ViEditor::new(editor);
|
||||||
|
|
||||||
update_attrs(&mut editor, attrs);
|
update_attrs(&mut editor, attrs);
|
||||||
|
|
||||||
let mut window = Window {
|
let mut window = Window {
|
||||||
|
|
@ -180,7 +185,7 @@ impl Application for Window {
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut editor = self.editor.lock().unwrap();
|
let mut editor = self.editor.lock().unwrap();
|
||||||
update_attrs(&mut editor, 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 {
|
||||||
|
|
@ -190,7 +195,7 @@ impl Application for Window {
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut editor = self.editor.lock().unwrap();
|
let mut editor = self.editor.lock().unwrap();
|
||||||
update_attrs(&mut editor, self.attrs);
|
update_attrs(&mut *editor, self.attrs);
|
||||||
},
|
},
|
||||||
Message::Monospaced(monospaced) => {
|
Message::Monospaced(monospaced) => {
|
||||||
self.attrs = self.attrs
|
self.attrs = self.attrs
|
||||||
|
|
@ -202,7 +207,7 @@ impl Application for Window {
|
||||||
.monospaced(monospaced);
|
.monospaced(monospaced);
|
||||||
|
|
||||||
let mut editor = self.editor.lock().unwrap();
|
let mut editor = self.editor.lock().unwrap();
|
||||||
update_attrs(&mut editor, self.attrs);
|
update_attrs(&mut *editor, self.attrs);
|
||||||
},
|
},
|
||||||
Message::MetricsChanged(metrics) => {
|
Message::MetricsChanged(metrics) => {
|
||||||
let mut editor = self.editor.lock().unwrap();
|
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)));
|
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();
|
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)
|
.align_items(Alignment::Center)
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
,
|
,
|
||||||
syntax_text_box(&self.editor)
|
text_box(&self.editor)
|
||||||
]
|
]
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
.padding(16)
|
.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| {
|
editor.buffer_mut().lines.iter_mut().for_each(|line| {
|
||||||
line.set_attrs_list(AttrsList::new(attrs));
|
line.set_attrs_list(AttrsList::new(attrs));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -17,7 +17,7 @@ use cosmic::{
|
||||||
};
|
};
|
||||||
use cosmic_text::{
|
use cosmic_text::{
|
||||||
Action,
|
Action,
|
||||||
Editor,
|
Edit,
|
||||||
SwashCache,
|
SwashCache,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
|
|
@ -51,34 +51,35 @@ impl StyleSheet for Theme {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TextBox<'a> {
|
pub struct TextBox<'a, Editor> {
|
||||||
editor: &'a Mutex<Editor<'static>>,
|
editor: &'a Mutex<Editor>,
|
||||||
padding: Padding,
|
padding: Padding,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TextBox<'a> {
|
impl<'a, Editor> TextBox<'a, Editor> {
|
||||||
pub fn new(editor: &'a Mutex<Editor<'static>>) -> Self {
|
pub fn new(editor: &'a Mutex<Editor>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
editor,
|
editor,
|
||||||
padding: Padding::new(0),
|
padding: Padding::new(0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||||
self.padding = padding.into();
|
self.padding = padding.into();
|
||||||
self
|
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)
|
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
|
where
|
||||||
Renderer: renderer::Renderer + image::Renderer<Handle = image::Handle>,
|
Renderer: renderer::Renderer + image::Renderer<Handle = image::Handle>,
|
||||||
Renderer::Theme: StyleSheet,
|
Renderer::Theme: StyleSheet,
|
||||||
|
Editor: Edit<'editor>,
|
||||||
{
|
{
|
||||||
fn tag(&self) -> tree::Tag {
|
fn tag(&self) -> tree::Tag {
|
||||||
tree::Tag::of::<State>()
|
tree::Tag::of::<State>()
|
||||||
|
|
@ -105,17 +106,17 @@ where
|
||||||
|
|
||||||
//TODO: allow lazy shape
|
//TODO: allow lazy shape
|
||||||
let mut editor = self.editor.lock().unwrap();
|
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;
|
let mut layout_lines = 0;
|
||||||
for line in editor.buffer.lines.iter() {
|
for line in editor.buffer().lines.iter() {
|
||||||
match line.layout_opt() {
|
match line.layout_opt() {
|
||||||
Some(layout) => layout_lines += layout.len(),
|
Some(layout) => layout_lines += layout.len(),
|
||||||
None => (),
|
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);
|
let size = Size::new(limits.max().width, height);
|
||||||
log::info!("size {:?}", size);
|
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_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;
|
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();
|
editor.shape_as_needed();
|
||||||
|
|
||||||
|
|
@ -270,6 +271,10 @@ where
|
||||||
editor.action(Action::PageDown);
|
editor.action(Action::PageDown);
|
||||||
status = Status::Captured;
|
status = Status::Captured;
|
||||||
},
|
},
|
||||||
|
KeyCode::Escape => {
|
||||||
|
editor.action(Action::Escape);
|
||||||
|
status = Status::Captured;
|
||||||
|
},
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
editor.action(Action::Enter);
|
editor.action(Action::Enter);
|
||||||
status = Status::Captured;
|
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
|
where
|
||||||
Renderer: renderer::Renderer + image::Renderer<Handle = image::Handle>,
|
Renderer: renderer::Renderer + image::Renderer<Handle = image::Handle>,
|
||||||
Renderer::Theme: StyleSheet,
|
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)
|
Self::new(text_box)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,8 @@ env_logger = "0.9"
|
||||||
fontdb = "0.9"
|
fontdb = "0.9"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
orbclient = "0.3.35"
|
orbclient = "0.3.35"
|
||||||
|
unicode-segmentation = "1.7"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
vi = ["cosmic-text/vi"]
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use cosmic_text::{
|
||||||
Action,
|
Action,
|
||||||
Attrs,
|
Attrs,
|
||||||
Buffer,
|
Buffer,
|
||||||
|
Edit,
|
||||||
Family,
|
Family,
|
||||||
FontSystem,
|
FontSystem,
|
||||||
Metrics,
|
Metrics,
|
||||||
|
|
@ -60,12 +61,16 @@ fn main() {
|
||||||
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 editor = SyntaxEditor::new(
|
let mut editor = SyntaxEditor::new(
|
||||||
Buffer::new(&font_system, font_sizes[font_size_i]),
|
Buffer::new(&font_system, font_sizes[font_size_i]),
|
||||||
&syntax_system,
|
&syntax_system,
|
||||||
"base16-eighties.dark"
|
"base16-eighties.dark"
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
|
||||||
|
#[cfg(feature = "vi")]
|
||||||
|
let mut editor = cosmic_text::ViEditor::new(editor);
|
||||||
|
|
||||||
editor.buffer_mut().set_size(
|
editor.buffer_mut().set_size(
|
||||||
window.width() as i32 - line_x * 2,
|
window.width() as i32 - line_x * 2,
|
||||||
window.height() as i32
|
window.height() as i32
|
||||||
|
|
@ -89,13 +94,14 @@ fn main() {
|
||||||
let mut mouse_left = false;
|
let mut mouse_left = false;
|
||||||
loop {
|
loop {
|
||||||
editor.shape_as_needed();
|
editor.shape_as_needed();
|
||||||
if editor.buffer_mut().redraw {
|
if editor.buffer().redraw() {
|
||||||
let instant = Instant::now();
|
let instant = Instant::now();
|
||||||
|
|
||||||
let bg = editor.background_color();
|
let bg = editor.background_color();
|
||||||
window.set(orbclient::Color::rgb(bg.r(), bg.g(), bg.b()));
|
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 })
|
window.rect(line_x + x, y, w, h, orbclient::Color { data: color.0 })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -127,7 +133,7 @@ fn main() {
|
||||||
|
|
||||||
window.sync();
|
window.sync();
|
||||||
|
|
||||||
editor.buffer_mut().redraw = false;
|
editor.buffer_mut().set_redraw(false);
|
||||||
|
|
||||||
log::debug!("redraw: {:?}", instant.elapsed());
|
log::debug!("redraw: {:?}", instant.elapsed());
|
||||||
}
|
}
|
||||||
|
|
@ -148,6 +154,7 @@ fn main() {
|
||||||
orbclient::K_END if event.pressed => editor.action(Action::End),
|
orbclient::K_END if event.pressed => editor.action(Action::End),
|
||||||
orbclient::K_PGUP if event.pressed => editor.action(Action::PageUp),
|
orbclient::K_PGUP if event.pressed => editor.action(Action::PageUp),
|
||||||
orbclient::K_PGDN if event.pressed => editor.action(Action::PageDown),
|
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_ENTER if event.pressed => editor.action(Action::Enter),
|
||||||
orbclient::K_BKSP if event.pressed => editor.action(Action::Backspace),
|
orbclient::K_BKSP if event.pressed => editor.action(Action::Backspace),
|
||||||
orbclient::K_DEL if event.pressed => editor.action(Action::Delete),
|
orbclient::K_DEL if event.pressed => editor.action(Action::Delete),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
// 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 orbclient::{EventOption, Renderer, Window, WindowFlag};
|
||||||
use std::{env, fs, process, time::Instant};
|
use std::{env, fs, process, time::Instant};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
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);
|
let font_color = Color::rgb(0xFF, 0xFF, 0xFF);
|
||||||
|
|
||||||
editor.shape_as_needed();
|
editor.shape_as_needed();
|
||||||
if editor.buffer.redraw {
|
if editor.buffer().redraw() {
|
||||||
let instant = Instant::now();
|
let instant = Instant::now();
|
||||||
|
|
||||||
window.set(bg_color);
|
window.set(bg_color);
|
||||||
|
|
@ -21,7 +21,7 @@ fn redraw(window: &mut Window, editor: &mut Editor<'_>, swash_cache: &mut SwashC
|
||||||
|
|
||||||
window.sync();
|
window.sync();
|
||||||
|
|
||||||
editor.buffer.redraw = false;
|
editor.buffer_mut().set_redraw(false);
|
||||||
|
|
||||||
let duration = instant.elapsed();
|
let duration = instant.elapsed();
|
||||||
log::debug!("redraw: {:?}", duration);
|
log::debug!("redraw: {:?}", duration);
|
||||||
|
|
@ -146,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 = &editor.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;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use cosmic_text::{
|
||||||
Buffer,
|
Buffer,
|
||||||
BufferLine,
|
BufferLine,
|
||||||
Color,
|
Color,
|
||||||
|
Edit,
|
||||||
Editor,
|
Editor,
|
||||||
Family,
|
Family,
|
||||||
FontSystem,
|
FontSystem,
|
||||||
|
|
@ -49,7 +50,7 @@ fn main() {
|
||||||
Metrics::new(32, 44).scale(display_scale)
|
Metrics::new(32, 44).scale(display_scale)
|
||||||
));
|
));
|
||||||
|
|
||||||
editor.buffer.set_size(
|
editor.buffer_mut().set_size(
|
||||||
window.width() as i32,
|
window.width() as i32,
|
||||||
window.height() as i32
|
window.height() as i32
|
||||||
);
|
);
|
||||||
|
|
@ -59,7 +60,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"));
|
||||||
|
|
||||||
editor.buffer.lines.clear();
|
editor.buffer_mut().lines.clear();
|
||||||
|
|
||||||
let lines: &[&[(&str, Attrs)]] = &[
|
let lines: &[&[(&str, Attrs)]] = &[
|
||||||
&[
|
&[
|
||||||
|
|
@ -131,7 +132,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);
|
||||||
}
|
}
|
||||||
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);
|
let mut swash_cache = SwashCache::new(&font_system);
|
||||||
|
|
@ -145,7 +146,7 @@ fn main() {
|
||||||
let font_color = Color::rgb(0xFF, 0xFF, 0xFF);
|
let font_color = Color::rgb(0xFF, 0xFF, 0xFF);
|
||||||
|
|
||||||
editor.shape_as_needed();
|
editor.shape_as_needed();
|
||||||
if editor.buffer.redraw {
|
if editor.buffer().redraw() {
|
||||||
let instant = Instant::now();
|
let instant = Instant::now();
|
||||||
|
|
||||||
window.set(bg_color);
|
window.set(bg_color);
|
||||||
|
|
@ -156,7 +157,7 @@ fn main() {
|
||||||
|
|
||||||
window.sync();
|
window.sync();
|
||||||
|
|
||||||
editor.buffer.redraw = false;
|
editor.buffer_mut().set_redraw(false);
|
||||||
|
|
||||||
let duration = instant.elapsed();
|
let duration = instant.elapsed();
|
||||||
log::debug!("redraw: {:?}", duration);
|
log::debug!("redraw: {:?}", duration);
|
||||||
|
|
@ -193,7 +194,7 @@ fn main() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
EventOption::Resize(resize) => {
|
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),
|
EventOption::Quit(_) => process::exit(0),
|
||||||
_ => (),
|
_ => (),
|
||||||
|
|
|
||||||
12
src/attrs.rs
12
src/attrs.rs
|
|
@ -35,25 +35,25 @@ impl Color {
|
||||||
/// Get the red component
|
/// Get the red component
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn r(&self) -> u8 {
|
pub fn r(&self) -> u8 {
|
||||||
((self.0 & 0x00FF0000) >> 16) as u8
|
((self.0 & 0x00_FF_00_00) >> 16) as u8
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the green component
|
/// Get the green component
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn g(&self) -> u8 {
|
pub fn g(&self) -> u8 {
|
||||||
((self.0 & 0x0000FF00) >> 8) as u8
|
((self.0 & 0x00_00_FF_00) >> 8) as u8
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the blue component
|
/// Get the blue component
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn b(&self) -> u8 {
|
pub fn b(&self) -> u8 {
|
||||||
(self.0 & 0x000000FF) as u8
|
(self.0 & 0x00_00_00_FF) as u8
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the alpha component
|
/// Get the alpha component
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn a(&self) -> u8 {
|
pub fn a(&self) -> u8 {
|
||||||
((self.0 & 0xFF000000) >> 24) as u8
|
((self.0 & 0xFF_00_00_00) >> 24) as u8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,7 +81,7 @@ impl FamilyOwned {
|
||||||
|
|
||||||
pub fn as_family(&self) -> Family {
|
pub fn as_family(&self) -> Family {
|
||||||
match self {
|
match self {
|
||||||
FamilyOwned::Name(name) => Family::Name(&name),
|
FamilyOwned::Name(name) => Family::Name(name),
|
||||||
FamilyOwned::Serif => Family::Serif,
|
FamilyOwned::Serif => Family::Serif,
|
||||||
FamilyOwned::SansSerif => Family::SansSerif,
|
FamilyOwned::SansSerif => Family::SansSerif,
|
||||||
FamilyOwned::Cursive => Family::Cursive,
|
FamilyOwned::Cursive => Family::Cursive,
|
||||||
|
|
@ -278,7 +278,7 @@ impl AttrsList {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (key, resize) in removes {
|
for (key, resize) in removes {
|
||||||
let (range, attrs) = self.spans.get_key_value(&key.start).map(|v| (v.0.clone(), v.1.clone())).unwrap();
|
let (range, attrs) = self.spans.get_key_value(&key.start).map(|v| (v.0.clone(), v.1.clone())).expect("attrs span not found");
|
||||||
self.spans.remove(key);
|
self.spans.remove(key);
|
||||||
|
|
||||||
if resize {
|
if resize {
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ pub struct LayoutRun<'a> {
|
||||||
pub line_y: i32,
|
pub line_y: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An iterator of visible text lines, see [LayoutRun]
|
/// An iterator of visible text lines, see [`LayoutRun`]
|
||||||
pub struct LayoutRunIter<'a, 'b> {
|
pub struct LayoutRunIter<'a, 'b> {
|
||||||
buffer: &'b Buffer<'a>,
|
buffer: &'b Buffer<'a>,
|
||||||
line_i: usize,
|
line_i: usize,
|
||||||
|
|
@ -145,8 +145,7 @@ impl fmt::Display for Metrics {
|
||||||
|
|
||||||
/// A buffer of text that is shaped and laid out
|
/// A buffer of text that is shaped and laid out
|
||||||
pub struct Buffer<'a> {
|
pub struct Buffer<'a> {
|
||||||
/// The [FontSystem] used by this [Buffer]
|
font_system: &'a FontSystem,
|
||||||
pub font_system: &'a FontSystem,
|
|
||||||
/// [BufferLine]s (or paragraphs) of text in the buffer
|
/// [BufferLine]s (or paragraphs) of text in the buffer
|
||||||
pub lines: Vec<BufferLine>,
|
pub lines: Vec<BufferLine>,
|
||||||
metrics: Metrics,
|
metrics: Metrics,
|
||||||
|
|
@ -154,11 +153,11 @@ pub struct Buffer<'a> {
|
||||||
height: i32,
|
height: i32,
|
||||||
scroll: i32,
|
scroll: i32,
|
||||||
/// 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,
|
redraw: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Buffer<'a> {
|
impl<'a> Buffer<'a> {
|
||||||
/// Create a new [Buffer] with the provided [FontSystem] and [Metrics]
|
/// Create a new [`Buffer`] with the provided [`FontSystem`] and [`Metrics`]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
font_system: &'a FontSystem,
|
font_system: &'a FontSystem,
|
||||||
metrics: Metrics,
|
metrics: Metrics,
|
||||||
|
|
@ -180,7 +179,7 @@ impl<'a> Buffer<'a> {
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
let instant = std::time::Instant::now();
|
let instant = std::time::Instant::now();
|
||||||
|
|
||||||
for line in self.lines.iter_mut() {
|
for line in &mut self.lines {
|
||||||
if line.shape_opt().is_some() {
|
if line.shape_opt().is_some() {
|
||||||
line.reset_layout();
|
line.reset_layout();
|
||||||
line.layout(
|
line.layout(
|
||||||
|
|
@ -204,7 +203,7 @@ impl<'a> Buffer<'a> {
|
||||||
|
|
||||||
let mut reshaped = 0;
|
let mut reshaped = 0;
|
||||||
let mut total_layout = 0;
|
let mut total_layout = 0;
|
||||||
for line in self.lines.iter_mut() {
|
for line in &mut self.lines {
|
||||||
if total_layout >= lines {
|
if total_layout >= lines {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -293,7 +292,8 @@ impl<'a> Buffer<'a> {
|
||||||
pub fn layout_cursor(&self, cursor: &Cursor) -> LayoutCursor {
|
pub fn layout_cursor(&self, cursor: &Cursor) -> LayoutCursor {
|
||||||
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?
|
//TODO: ensure layout is done?
|
||||||
|
let layout = line.layout_opt().as_ref().expect("layout not found");
|
||||||
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 {
|
||||||
|
|
@ -333,24 +333,29 @@ impl<'a> Buffer<'a> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get [`FontSystem`] used by this [`Buffer`]
|
||||||
|
pub fn font_system(&self) -> &'a FontSystem {
|
||||||
|
self.font_system
|
||||||
|
}
|
||||||
|
|
||||||
/// Shape the provided line index and return the result
|
/// Shape the provided line index and return the result
|
||||||
pub fn line_shape(&mut self, line_i: usize) -> Option<&ShapeLine> {
|
pub fn line_shape(&mut self, line_i: usize) -> Option<&ShapeLine> {
|
||||||
let line = self.lines.get_mut(line_i)?;
|
let line = self.lines.get_mut(line_i)?;
|
||||||
Some(line.shape(&self.font_system))
|
Some(line.shape(self.font_system))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lay out the provided line index and return the result
|
/// Lay out the provided line index and return the result
|
||||||
pub fn line_layout(&mut self, line_i: usize) -> Option<&[LayoutLine]> {
|
pub fn line_layout(&mut self, line_i: usize) -> Option<&[LayoutLine]> {
|
||||||
let line = self.lines.get_mut(line_i)?;
|
let line = self.lines.get_mut(line_i)?;
|
||||||
Some(line.layout(&self.font_system, self.metrics.font_size, self.width))
|
Some(line.layout(self.font_system, self.metrics.font_size, self.width))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current [Metrics]
|
/// Get the current [`Metrics`]
|
||||||
pub fn metrics(&self) -> Metrics {
|
pub fn metrics(&self) -> Metrics {
|
||||||
self.metrics
|
self.metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the current [Metrics]
|
/// Set the current [`Metrics`]
|
||||||
pub fn set_metrics(&mut self, metrics: Metrics) {
|
pub fn set_metrics(&mut self, metrics: Metrics) {
|
||||||
if metrics != self.metrics {
|
if metrics != self.metrics {
|
||||||
self.metrics = metrics;
|
self.metrics = metrics;
|
||||||
|
|
@ -408,6 +413,16 @@ impl<'a> Buffer<'a> {
|
||||||
self.shape_until_scroll();
|
self.shape_until_scroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True if a redraw is needed
|
||||||
|
pub fn redraw(&self) -> bool {
|
||||||
|
self.redraw
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set redraw needed flag
|
||||||
|
pub fn set_redraw(&mut self, redraw: bool) {
|
||||||
|
self.redraw = redraw;
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the visible layout runs for rendering and other tasks
|
/// Get the visible layout runs for rendering and other tasks
|
||||||
pub fn layout_runs<'b>(&'b self) -> LayoutRunIter<'a, 'b> {
|
pub fn layout_runs<'b>(&'b self) -> LayoutRunIter<'a, 'b> {
|
||||||
LayoutRunIter::new(self)
|
LayoutRunIter::new(self)
|
||||||
|
|
@ -439,15 +454,15 @@ impl<'a> Buffer<'a> {
|
||||||
let mut new_cursor_char = 0;
|
let mut new_cursor_char = 0;
|
||||||
|
|
||||||
let mut first_glyph = true;
|
let mut first_glyph = true;
|
||||||
|
|
||||||
'hit: for (glyph_i, glyph) in run.glyphs.iter().enumerate() {
|
'hit: for (glyph_i, glyph) in run.glyphs.iter().enumerate() {
|
||||||
if first_glyph {
|
if first_glyph {
|
||||||
first_glyph = false;
|
first_glyph = false;
|
||||||
if (run.rtl && x > glyph.x as i32) || (!run.rtl && x < 0) {
|
if (run.rtl && x > glyph.x as i32) || (!run.rtl && x < 0) {
|
||||||
new_cursor_glyph = 0;
|
new_cursor_glyph = 0;
|
||||||
new_cursor_char = 0;
|
new_cursor_char = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if x >= glyph.x as i32
|
if x >= glyph.x as i32
|
||||||
&& x <= (glyph.x + glyph.w) as i32
|
&& x <= (glyph.x + glyph.w) as i32
|
||||||
{
|
{
|
||||||
|
|
@ -528,7 +543,7 @@ impl<'a> Buffer<'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, run.line_y + y_int + y, 1, 1, color)
|
f(x_int + x, run.line_y + y_int + y, 1, 1, color);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ pub struct BufferLine {
|
||||||
|
|
||||||
impl BufferLine {
|
impl BufferLine {
|
||||||
/// 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
|
||||||
pub fn new<T: Into<String>>(text: T, attrs_list: AttrsList) -> Self {
|
pub fn new<T: Into<String>>(text: T, attrs_list: AttrsList) -> Self {
|
||||||
Self {
|
Self {
|
||||||
text: text.into(),
|
text: text.into(),
|
||||||
|
|
@ -40,7 +40,7 @@ impl BufferLine {
|
||||||
/// Will reset shape and layout if it differs from current text and attributes list.
|
/// Will reset shape and layout if it differs from current text and attributes list.
|
||||||
/// Returns true if the line was reset
|
/// Returns true if the line was reset
|
||||||
pub fn set_text<T: AsRef<str> + Into<String>>(&mut self, text: T, attrs_list: AttrsList) -> bool {
|
pub fn set_text<T: AsRef<str> + Into<String>>(&mut self, text: T, attrs_list: AttrsList) -> bool {
|
||||||
if text.as_ref() != &self.text || attrs_list != self.attrs_list {
|
if text.as_ref() != self.text || attrs_list != self.attrs_list {
|
||||||
self.text = text.into();
|
self.text = text.into();
|
||||||
self.attrs_list = attrs_list;
|
self.attrs_list = attrs_list;
|
||||||
self.reset();
|
self.reset();
|
||||||
|
|
@ -143,7 +143,7 @@ impl BufferLine {
|
||||||
self.shape_opt = Some(ShapeLine::new(font_system, &self.text, &self.attrs_list));
|
self.shape_opt = Some(ShapeLine::new(font_system, &self.text, &self.attrs_list));
|
||||||
self.layout_opt = None;
|
self.layout_opt = None;
|
||||||
}
|
}
|
||||||
self.shape_opt.as_ref().unwrap()
|
self.shape_opt.as_ref().expect("shape not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get line shaping cache
|
/// Get line shaping cache
|
||||||
|
|
@ -163,7 +163,7 @@ impl BufferLine {
|
||||||
);
|
);
|
||||||
self.layout_opt = Some(layout);
|
self.layout_opt = Some(layout);
|
||||||
}
|
}
|
||||||
self.layout_opt.as_ref().unwrap()
|
self.layout_opt.as_ref().expect("layout not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get line layout cache
|
/// Get line layout cache
|
||||||
|
|
|
||||||
|
|
@ -5,52 +5,13 @@ use alloc::string::{String, ToString};
|
||||||
use core::cmp;
|
use core::cmp;
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
use crate::{AttrsList, Buffer, BufferLine, Cursor, LayoutCursor};
|
use crate::{Action, AttrsList, Buffer, BufferLine, Cursor, Edit, LayoutCursor};
|
||||||
#[cfg(feature = "swash")]
|
#[cfg(feature = "swash")]
|
||||||
use crate::Color;
|
use crate::Color;
|
||||||
|
|
||||||
/// An action to perform on an [Editor]
|
/// A wrapper of [`Buffer`] for easy editing
|
||||||
#[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 struct Editor<'a> {
|
||||||
pub buffer: Buffer<'a>,
|
buffer: Buffer<'a>,
|
||||||
cursor: Cursor,
|
cursor: Cursor,
|
||||||
cursor_x_opt: Option<i32>,
|
cursor_x_opt: Option<i32>,
|
||||||
select_opt: Option<Cursor>,
|
select_opt: Option<Cursor>,
|
||||||
|
|
@ -58,7 +19,7 @@ pub struct Editor<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Editor<'a> {
|
impl<'a> Editor<'a> {
|
||||||
/// Create a new [Editor] with the provided [Buffer]
|
/// Create a new [`Editor`] with the provided [`Buffer`]
|
||||||
pub fn new(buffer: Buffer<'a>) -> Self {
|
pub fn new(buffer: Buffer<'a>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
buffer,
|
buffer,
|
||||||
|
|
@ -69,18 +30,8 @@ impl<'a> Editor<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shape lines until scroll, after adjusting scroll if the cursor moved
|
|
||||||
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) {
|
fn set_layout_cursor(&mut self, cursor: LayoutCursor) {
|
||||||
let layout = self.buffer.line_layout(cursor.line).unwrap();
|
let layout = self.buffer.line_layout(cursor.line).expect("layout not found");
|
||||||
|
|
||||||
let layout_line = match layout.get(cursor.layout) {
|
let layout_line = match layout.get(cursor.layout) {
|
||||||
Some(some) => some,
|
Some(some) => some,
|
||||||
|
|
@ -102,27 +53,38 @@ impl<'a> Editor<'a> {
|
||||||
if self.cursor.line != cursor.line || self.cursor.index != new_index {
|
if self.cursor.line != cursor.line || self.cursor.index != new_index {
|
||||||
self.cursor.line = cursor.line;
|
self.cursor.line = cursor.line;
|
||||||
self.cursor.index = new_index;
|
self.cursor.index = new_index;
|
||||||
self.buffer.redraw = true;
|
self.buffer.set_redraw(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the internal [Buffer]
|
impl<'a> Edit<'a> for Editor<'a> {
|
||||||
pub fn buffer(&self) -> &Buffer<'a> {
|
fn buffer(&self) -> &Buffer<'a> {
|
||||||
&self.buffer
|
&self.buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the internal [Buffer], mutably
|
fn buffer_mut(&mut self) -> &mut Buffer<'a> {
|
||||||
pub fn buffer_mut(&mut self) -> &mut Buffer<'a> {
|
|
||||||
&mut self.buffer
|
&mut self.buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current cursor position
|
fn cursor(&self) -> Cursor {
|
||||||
pub fn cursor(&self) -> Cursor {
|
|
||||||
self.cursor
|
self.cursor
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copy selection
|
fn select_opt(&self) -> Option<Cursor> {
|
||||||
pub fn copy_selection(&mut self) -> Option<String> {
|
self.select_opt
|
||||||
|
}
|
||||||
|
|
||||||
|
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 copy_selection(&mut self) -> Option<String> {
|
||||||
let select = self.select_opt?;
|
let select = self.select_opt?;
|
||||||
|
|
||||||
let (start, end) = match select.line.cmp(&self.cursor.line) {
|
let (start, end) = match select.line.cmp(&self.cursor.line) {
|
||||||
|
|
@ -166,9 +128,7 @@ impl<'a> Editor<'a> {
|
||||||
Some(selection)
|
Some(selection)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete selection, adjusting cursor and returning true if there was a selection
|
fn delete_selection(&mut self) -> bool {
|
||||||
// Helper function for backspace, delete, insert, and enter when there is a selection
|
|
||||||
pub fn delete_selection(&mut self) -> bool {
|
|
||||||
let select = match self.select_opt.take() {
|
let select = match self.select_opt.take() {
|
||||||
Some(some) => some,
|
Some(some) => some,
|
||||||
None => return false,
|
None => return false,
|
||||||
|
|
@ -235,8 +195,7 @@ impl<'a> Editor<'a> {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform an [Action] on the editor
|
fn action(&mut self, action: Action) {
|
||||||
pub fn action(&mut self, action: Action) {
|
|
||||||
let old_cursor = self.cursor;
|
let old_cursor = self.cursor;
|
||||||
|
|
||||||
match action {
|
match action {
|
||||||
|
|
@ -254,11 +213,11 @@ impl<'a> Editor<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.cursor.index = prev_index;
|
self.cursor.index = prev_index;
|
||||||
self.buffer.redraw = true;
|
self.buffer.set_redraw(true);
|
||||||
} else if self.cursor.line > 0 {
|
} else if self.cursor.line > 0 {
|
||||||
self.cursor.line -= 1;
|
self.cursor.line -= 1;
|
||||||
self.cursor.index = self.buffer.lines[self.cursor.line].text().len();
|
self.cursor.index = self.buffer.lines[self.cursor.line].text().len();
|
||||||
self.buffer.redraw = true;
|
self.buffer.set_redraw(true);
|
||||||
}
|
}
|
||||||
self.cursor_x_opt = None;
|
self.cursor_x_opt = None;
|
||||||
},
|
},
|
||||||
|
|
@ -268,14 +227,14 @@ impl<'a> Editor<'a> {
|
||||||
for (i, c) in line.text().grapheme_indices(true) {
|
for (i, c) in line.text().grapheme_indices(true) {
|
||||||
if i == self.cursor.index {
|
if i == self.cursor.index {
|
||||||
self.cursor.index += c.len();
|
self.cursor.index += c.len();
|
||||||
self.buffer.redraw = true;
|
self.buffer.set_redraw(true);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if self.cursor.line + 1 < self.buffer.lines.len() {
|
} else if self.cursor.line + 1 < self.buffer.lines.len() {
|
||||||
self.cursor.line += 1;
|
self.cursor.line += 1;
|
||||||
self.cursor.index = 0;
|
self.cursor.index = 0;
|
||||||
self.buffer.redraw = true;
|
self.buffer.set_redraw(true);
|
||||||
}
|
}
|
||||||
self.cursor_x_opt = None;
|
self.cursor_x_opt = None;
|
||||||
},
|
},
|
||||||
|
|
@ -326,7 +285,7 @@ impl<'a> Editor<'a> {
|
||||||
//TODO: make this preserve X as best as possible!
|
//TODO: make this preserve X as best as possible!
|
||||||
let mut cursor = self.buffer.layout_cursor(&self.cursor);
|
let mut cursor = self.buffer.layout_cursor(&self.cursor);
|
||||||
|
|
||||||
let layout_len = self.buffer.line_layout(cursor.line).unwrap().len();
|
let layout_len = self.buffer.line_layout(cursor.line).expect("layout not found").len();
|
||||||
|
|
||||||
if self.cursor_x_opt.is_none() {
|
if self.cursor_x_opt.is_none() {
|
||||||
self.cursor_x_opt = Some(
|
self.cursor_x_opt = Some(
|
||||||
|
|
@ -371,6 +330,11 @@ impl<'a> Editor<'a> {
|
||||||
scroll += self.buffer.visible_lines();
|
scroll += self.buffer.visible_lines();
|
||||||
self.buffer.set_scroll(scroll);
|
self.buffer.set_scroll(scroll);
|
||||||
},
|
},
|
||||||
|
Action::Escape => {
|
||||||
|
if self.select_opt.take().is_some() {
|
||||||
|
self.buffer.set_redraw(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
Action::Insert(character) => {
|
Action::Insert(character) => {
|
||||||
if character.is_control()
|
if character.is_control()
|
||||||
&& !['\t', '\u{92}'].contains(&character)
|
&& !['\t', '\u{92}'].contains(&character)
|
||||||
|
|
@ -484,20 +448,20 @@ impl<'a> Editor<'a> {
|
||||||
if let Some(new_cursor) = self.buffer.hit(x, y) {
|
if let Some(new_cursor) = self.buffer.hit(x, y) {
|
||||||
if new_cursor != self.cursor {
|
if new_cursor != self.cursor {
|
||||||
self.cursor = new_cursor;
|
self.cursor = new_cursor;
|
||||||
self.buffer.redraw = true;
|
self.buffer.set_redraw(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Action::Drag { x, y } => {
|
Action::Drag { x, y } => {
|
||||||
if self.select_opt.is_none() {
|
if self.select_opt.is_none() {
|
||||||
self.select_opt = Some(self.cursor);
|
self.select_opt = Some(self.cursor);
|
||||||
self.buffer.redraw = true;
|
self.buffer.set_redraw(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(new_cursor) = self.buffer.hit(x, y) {
|
if let Some(new_cursor) = self.buffer.hit(x, y) {
|
||||||
if new_cursor != self.cursor {
|
if new_cursor != self.cursor {
|
||||||
self.cursor = new_cursor;
|
self.cursor = new_cursor;
|
||||||
self.buffer.redraw = true;
|
self.buffer.set_redraw(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -531,7 +495,7 @@ impl<'a> Editor<'a> {
|
||||||
|
|
||||||
/// Draw the editor
|
/// Draw the editor
|
||||||
#[cfg(feature = "swash")]
|
#[cfg(feature = "swash")]
|
||||||
pub fn draw<F>(&self, cache: &mut crate::SwashCache, color: Color, mut f: F)
|
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.buffer.metrics().font_size;
|
let font_size = self.buffer.metrics().font_size;
|
||||||
|
|
@ -699,7 +663,7 @@ impl<'a> Editor<'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, line_y + y_int + y, 1, 1, color);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
93
src/edit/mod.rs
Normal file
93
src/edit/mod.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
use alloc::string::String;
|
||||||
|
|
||||||
|
use crate::{Buffer, Cursor};
|
||||||
|
#[cfg(feature = "swash")]
|
||||||
|
use crate::Color;
|
||||||
|
|
||||||
|
pub use self::editor::*;
|
||||||
|
mod editor;
|
||||||
|
|
||||||
|
#[cfg(feature = "syntect")]
|
||||||
|
pub use self::syntect::*;
|
||||||
|
#[cfg(feature = "syntect")]
|
||||||
|
mod syntect;
|
||||||
|
|
||||||
|
#[cfg(feature = "vi")]
|
||||||
|
pub use self::vi::*;
|
||||||
|
#[cfg(feature = "vi")]
|
||||||
|
mod vi;
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
/// Escape, clears selection
|
||||||
|
Escape,
|
||||||
|
/// 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 trait to allow easy replacements of [`Editor`], like `SyntaxEditor`
|
||||||
|
pub trait Edit<'a> {
|
||||||
|
/// Get the internal [`Buffer`]
|
||||||
|
fn buffer(&self) -> &Buffer<'a>;
|
||||||
|
|
||||||
|
/// Get the internal [`Buffer`], mutably
|
||||||
|
fn buffer_mut(&mut self) -> &mut Buffer<'a>;
|
||||||
|
|
||||||
|
/// Get the current cursor position
|
||||||
|
fn cursor(&self) -> Cursor;
|
||||||
|
|
||||||
|
/// Get the current selection position
|
||||||
|
fn select_opt(&self) -> Option<Cursor>;
|
||||||
|
|
||||||
|
/// Shape lines until scroll, after adjusting scroll if the cursor moved
|
||||||
|
fn shape_as_needed(&mut self);
|
||||||
|
|
||||||
|
/// Copy selection
|
||||||
|
fn copy_selection(&mut self) -> Option<String>;
|
||||||
|
|
||||||
|
/// Delete selection, adjusting cursor and returning true if there was a selection
|
||||||
|
// Also used by backspace, delete, insert, and enter when there is a selection
|
||||||
|
fn delete_selection(&mut self) -> bool;
|
||||||
|
|
||||||
|
/// Perform an [Action] on the editor
|
||||||
|
fn action(&mut self, action: Action);
|
||||||
|
|
||||||
|
/// Draw the editor
|
||||||
|
#[cfg(feature = "swash")]
|
||||||
|
fn draw<F>(&self, cache: &mut crate::SwashCache, color: Color, f: F)
|
||||||
|
where F: FnMut(i32, i32, u32, u32, Color);
|
||||||
|
}
|
||||||
|
|
@ -30,6 +30,7 @@ use crate::{
|
||||||
Buffer,
|
Buffer,
|
||||||
Color,
|
Color,
|
||||||
Cursor,
|
Cursor,
|
||||||
|
Edit,
|
||||||
Editor,
|
Editor,
|
||||||
Style,
|
Style,
|
||||||
Weight,
|
Weight,
|
||||||
|
|
@ -41,7 +42,7 @@ pub struct SyntaxSystem {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SyntaxSystem {
|
impl SyntaxSystem {
|
||||||
/// Create a new [SyntaxSystem]
|
/// Create a new [`SyntaxSystem`]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
//TODO: store newlines in buffer
|
//TODO: store newlines in buffer
|
||||||
|
|
@ -51,7 +52,7 @@ impl SyntaxSystem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A wrapper of [Editor] with syntax highlighting provided by [SyntaxSystem]
|
/// A wrapper of [`Editor`] with syntax highlighting provided by [`SyntaxSystem`]
|
||||||
pub struct SyntaxEditor<'a> {
|
pub struct SyntaxEditor<'a> {
|
||||||
editor: Editor<'a>,
|
editor: Editor<'a>,
|
||||||
syntax_system: &'a SyntaxSystem,
|
syntax_system: &'a SyntaxSystem,
|
||||||
|
|
@ -62,7 +63,7 @@ pub struct SyntaxEditor<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> SyntaxEditor<'a> {
|
impl<'a> SyntaxEditor<'a> {
|
||||||
/// Create a new [SyntaxEditor] with the provided [Buffer], [SyntaxSystem], and theme name.
|
/// Create a new [`SyntaxEditor`] with the provided [`Buffer`], [`SyntaxSystem`], and theme name.
|
||||||
///
|
///
|
||||||
/// A good default theme name is "base16-eighties.dark".
|
/// A good default theme name is "base16-eighties.dark".
|
||||||
///
|
///
|
||||||
|
|
@ -84,12 +85,16 @@ impl<'a> SyntaxEditor<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load text from a file, and also set syntax to the best option
|
/// Load text from a file, and also set syntax to the best option
|
||||||
|
///
|
||||||
|
/// ## Errors
|
||||||
|
///
|
||||||
|
/// Returns an [`io::Error`] if reading the file fails
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
pub fn load_text<P: AsRef<Path>>(&mut self, path: P, attrs: crate::Attrs<'a>) -> io::Result<()> {
|
pub fn load_text<P: AsRef<Path>>(&mut self, path: P, attrs: crate::Attrs<'a>) -> io::Result<()> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
|
||||||
let text = fs::read_to_string(path)?;
|
let text = fs::read_to_string(path)?;
|
||||||
self.editor.buffer.set_text(&text, attrs);
|
self.editor.buffer_mut().set_text(&text, attrs);
|
||||||
|
|
||||||
//TODO: re-use text
|
//TODO: re-use text
|
||||||
self.syntax = match self.syntax_system.syntax_set.find_syntax_for_file(path) {
|
self.syntax = match self.syntax_system.syntax_set.find_syntax_for_file(path) {
|
||||||
|
|
@ -110,14 +115,61 @@ impl<'a> SyntaxEditor<'a> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shape as needed, also doing syntax highlighting
|
/// Get the default background color
|
||||||
pub fn shape_as_needed(&mut self) {
|
pub fn background_color(&self) -> Color {
|
||||||
|
if let Some(background) = self.theme.settings.background {
|
||||||
|
Color::rgba(
|
||||||
|
background.r,
|
||||||
|
background.g,
|
||||||
|
background.b,
|
||||||
|
background.a,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Color::rgb(0, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the default foreground (text) color
|
||||||
|
pub fn foreground_color(&self) -> Color {
|
||||||
|
if let Some(foreground) = self.theme.settings.foreground {
|
||||||
|
Color::rgba(
|
||||||
|
foreground.r,
|
||||||
|
foreground.g,
|
||||||
|
foreground.b,
|
||||||
|
foreground.a,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Color::rgb(0xFF, 0xFF, 0xFF)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Edit<'a> for SyntaxEditor<'a> {
|
||||||
|
fn buffer(&self) -> &Buffer<'a> {
|
||||||
|
self.editor.buffer()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buffer_mut(&mut self) -> &mut Buffer<'a> {
|
||||||
|
self.editor.buffer_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor(&self) -> Cursor {
|
||||||
|
self.editor.cursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_opt(&self) -> Option<Cursor> {
|
||||||
|
self.editor.select_opt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shape_as_needed(&mut self) {
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
let now = std::time::Instant::now();
|
let now = std::time::Instant::now();
|
||||||
|
|
||||||
|
let buffer = self.editor.buffer_mut();
|
||||||
|
|
||||||
let mut highlighted = 0;
|
let mut highlighted = 0;
|
||||||
for line_i in 0..self.editor.buffer.lines.len() {
|
for line_i in 0..buffer.lines.len() {
|
||||||
let line = &mut self.editor.buffer.lines[line_i];
|
let line = &mut buffer.lines[line_i];
|
||||||
if ! line.is_reset() && line_i < self.syntax_cache.len() {
|
if ! line.is_reset() && line_i < self.syntax_cache.len() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -132,7 +184,7 @@ impl<'a> SyntaxEditor<'a> {
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let ops = parse_state.parse_line(line.text(), &self.syntax_system.syntax_set).unwrap();
|
let ops = parse_state.parse_line(line.text(), &self.syntax_system.syntax_set).expect("failed to parse syntax");
|
||||||
let ranges = RangedHighlightIterator::new(
|
let ranges = RangedHighlightIterator::new(
|
||||||
&mut highlight_state,
|
&mut highlight_state,
|
||||||
&ops,
|
&ops,
|
||||||
|
|
@ -172,14 +224,14 @@ impl<'a> SyntaxEditor<'a> {
|
||||||
line.set_wrap_simple(true);
|
line.set_wrap_simple(true);
|
||||||
|
|
||||||
//TODO: efficiently do syntax highlighting without having to shape whole buffer
|
//TODO: efficiently do syntax highlighting without having to shape whole buffer
|
||||||
line.shape(self.editor.buffer.font_system);
|
buffer.line_shape(line_i);
|
||||||
|
|
||||||
let cache_item = (parse_state.clone(), highlight_state.clone());
|
let cache_item = (parse_state.clone(), highlight_state.clone());
|
||||||
if line_i < self.syntax_cache.len() {
|
if line_i < self.syntax_cache.len() {
|
||||||
if self.syntax_cache[line_i] != cache_item {
|
if self.syntax_cache[line_i] != cache_item {
|
||||||
self.syntax_cache[line_i] = cache_item;
|
self.syntax_cache[line_i] = cache_item;
|
||||||
if line_i + 1 < self.editor.buffer.lines.len() {
|
if line_i + 1 < buffer.lines.len() {
|
||||||
self.editor.buffer.lines[line_i + 1].reset();
|
buffer.lines[line_i + 1].reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -188,7 +240,7 @@ impl<'a> SyntaxEditor<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if highlighted > 0 {
|
if highlighted > 0 {
|
||||||
self.editor.buffer.redraw = true;
|
buffer.set_redraw(true);
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
log::debug!("Syntax highlighted {} lines in {:?}", highlighted, now.elapsed());
|
log::debug!("Syntax highlighted {} lines in {:?}", highlighted, now.elapsed());
|
||||||
}
|
}
|
||||||
|
|
@ -196,67 +248,21 @@ impl<'a> SyntaxEditor<'a> {
|
||||||
self.editor.shape_as_needed();
|
self.editor.shape_as_needed();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the internal [Buffer]
|
fn copy_selection(&mut self) -> Option<String> {
|
||||||
pub fn buffer(&self) -> &Buffer<'a> {
|
|
||||||
&self.editor.buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the internal [Buffer], mutably
|
|
||||||
pub fn buffer_mut(&mut self) -> &mut Buffer<'a> {
|
|
||||||
&mut self.editor.buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the current [Cursor] position
|
|
||||||
pub fn cursor(&self) -> Cursor {
|
|
||||||
self.editor.cursor()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the default background color
|
|
||||||
pub fn background_color(&self) -> Color {
|
|
||||||
if let Some(background) = self.theme.settings.background {
|
|
||||||
Color::rgba(
|
|
||||||
background.r,
|
|
||||||
background.g,
|
|
||||||
background.b,
|
|
||||||
background.a,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Color::rgb(0, 0, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the default foreground (text) color
|
|
||||||
pub fn foreground_color(&self) -> Color {
|
|
||||||
if let Some(foreground) = self.theme.settings.foreground {
|
|
||||||
Color::rgba(
|
|
||||||
foreground.r,
|
|
||||||
foreground.g,
|
|
||||||
foreground.b,
|
|
||||||
foreground.a,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Color::rgb(0xFF, 0xFF, 0xFF)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Copy selection
|
|
||||||
pub fn copy_selection(&mut self) -> Option<String> {
|
|
||||||
self.editor.copy_selection()
|
self.editor.copy_selection()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete selection, adjusting cursor and returning true if there was a selection
|
fn delete_selection(&mut self) -> bool {
|
||||||
pub fn delete_selection(&mut self) -> bool {
|
|
||||||
self.editor.delete_selection()
|
self.editor.delete_selection()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform an [Action] on the editor
|
fn action(&mut self, action: Action) {
|
||||||
pub fn action(&mut self, action: Action) {
|
|
||||||
self.editor.action(action);
|
self.editor.action(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the editor
|
/// Draw the editor
|
||||||
#[cfg(feature = "swash")]
|
#[cfg(feature = "swash")]
|
||||||
pub fn draw<F>(&self, cache: &mut crate::SwashCache, mut f: F)
|
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 size = self.buffer().size();
|
let size = self.buffer().size();
|
||||||
393
src/edit/vi.rs
Normal file
393
src/edit/vi.rs
Normal file
|
|
@ -0,0 +1,393 @@
|
||||||
|
use alloc::string::String;
|
||||||
|
use core::cmp;
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Action,
|
||||||
|
Buffer,
|
||||||
|
Color,
|
||||||
|
Cursor,
|
||||||
|
Edit,
|
||||||
|
SyntaxEditor,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
enum Mode {
|
||||||
|
Normal,
|
||||||
|
Insert,
|
||||||
|
Command,
|
||||||
|
Search,
|
||||||
|
SearchBackwards,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ViEditor<'a> {
|
||||||
|
editor: SyntaxEditor<'a>,
|
||||||
|
mode: Mode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ViEditor<'a> {
|
||||||
|
pub fn new(editor: SyntaxEditor<'a>) -> Self {
|
||||||
|
Self {
|
||||||
|
editor,
|
||||||
|
mode: Mode::Normal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load text from a file, and also set syntax to the best option
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
pub fn load_text<P: AsRef<std::path::Path>>(&mut self, path: P, attrs: crate::Attrs<'a>) -> std::io::Result<()> {
|
||||||
|
self.editor.load_text(path, attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the default background color
|
||||||
|
pub fn background_color(&self) -> Color {
|
||||||
|
self.editor.background_color()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the default foreground (text) color
|
||||||
|
pub fn foreground_color(&self) -> Color {
|
||||||
|
self.editor.foreground_color()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Edit<'a> for ViEditor<'a> {
|
||||||
|
fn buffer(&self) -> &Buffer<'a> {
|
||||||
|
self.editor.buffer()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buffer_mut(&mut self) -> &mut Buffer<'a> {
|
||||||
|
self.editor.buffer_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor(&self) -> Cursor {
|
||||||
|
self.editor.cursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_opt(&self) -> Option<Cursor> {
|
||||||
|
self.editor.select_opt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shape_as_needed(&mut self) {
|
||||||
|
self.editor.shape_as_needed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_selection(&mut self) -> Option<String> {
|
||||||
|
self.editor.copy_selection()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_selection(&mut self) -> bool {
|
||||||
|
self.editor.delete_selection()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action(&mut self, action: Action) {
|
||||||
|
let old_mode = self.mode;
|
||||||
|
|
||||||
|
match self.mode {
|
||||||
|
Mode::Normal => match action {
|
||||||
|
Action::Insert(c) => match c {
|
||||||
|
// Enter insert mode after cursor
|
||||||
|
'a' => {
|
||||||
|
self.editor.action(Action::Right);
|
||||||
|
self.mode = Mode::Insert;
|
||||||
|
},
|
||||||
|
// Enter insert mode at end of line
|
||||||
|
'A' => {
|
||||||
|
self.editor.action(Action::End);
|
||||||
|
self.mode = Mode::Insert;
|
||||||
|
},
|
||||||
|
// Enter insert mode at cursor
|
||||||
|
'i' => {
|
||||||
|
self.mode = Mode::Insert;
|
||||||
|
},
|
||||||
|
// Enter insert mode at start of line
|
||||||
|
'I' => {
|
||||||
|
//TODO: soft home, skip whitespace
|
||||||
|
self.editor.action(Action::Home);
|
||||||
|
self.mode = Mode::Insert;
|
||||||
|
}
|
||||||
|
// Create line after and enter insert mode
|
||||||
|
'o' => {
|
||||||
|
self.editor.action(Action::End);
|
||||||
|
self.editor.action(Action::Enter);
|
||||||
|
self.mode = Mode::Insert;
|
||||||
|
},
|
||||||
|
// Create line before and enter insert mode
|
||||||
|
'O' => {
|
||||||
|
self.editor.action(Action::Home);
|
||||||
|
self.editor.action(Action::Enter);
|
||||||
|
self.editor.shape_as_needed(); // TODO: do not require this?
|
||||||
|
self.editor.action(Action::Up);
|
||||||
|
self.mode = Mode::Insert;
|
||||||
|
},
|
||||||
|
// Left
|
||||||
|
'h' => self.editor.action(Action::Left),
|
||||||
|
// Top of screen
|
||||||
|
//TODO: 'H' => self.editor.action(Action::ScreenHigh),
|
||||||
|
// Down
|
||||||
|
'j' => self.editor.action(Action::Down),
|
||||||
|
// Up
|
||||||
|
'k' => self.editor.action(Action::Up),
|
||||||
|
// Right
|
||||||
|
'l' => self.editor.action(Action::Right),
|
||||||
|
// Bottom of screen
|
||||||
|
//TODO: 'L' => self.editor.action(Action::ScreenLow),
|
||||||
|
// Middle of screen
|
||||||
|
//TODO: 'M' => self.editor.action(Action::ScreenMiddle),
|
||||||
|
// Remove character at cursor
|
||||||
|
'x' => self.editor.action(Action::Delete),
|
||||||
|
// Remove character before cursor
|
||||||
|
'X' => self.editor.action(Action::Backspace),
|
||||||
|
// Go to start of line
|
||||||
|
'0' => self.editor.action(Action::Home),
|
||||||
|
// Go to end of line
|
||||||
|
'$' => self.editor.action(Action::End),
|
||||||
|
// Go to start of line after whitespace
|
||||||
|
//TODO: implement this
|
||||||
|
'^' => self.editor.action(Action::Home),
|
||||||
|
// Enter command mode
|
||||||
|
':' => {
|
||||||
|
self.mode = Mode::Command;
|
||||||
|
},
|
||||||
|
// Enter search mode
|
||||||
|
'/' => {
|
||||||
|
self.mode = Mode::Search;
|
||||||
|
},
|
||||||
|
// Enter search backwards mode
|
||||||
|
'?' => {
|
||||||
|
self.mode = Mode::SearchBackwards;
|
||||||
|
},
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
_ => self.editor.action(action),
|
||||||
|
},
|
||||||
|
Mode::Insert => match action {
|
||||||
|
Action::Escape => {
|
||||||
|
let cursor = self.cursor();
|
||||||
|
let layout_cursor = self.buffer().layout_cursor(&cursor);
|
||||||
|
if layout_cursor.glyph > 0 {
|
||||||
|
self.editor.action(Action::Left);
|
||||||
|
}
|
||||||
|
self.mode = Mode::Normal;
|
||||||
|
},
|
||||||
|
_ => self.editor.action(action),
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
//TODO: other modes
|
||||||
|
self.mode = Mode::Normal;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.mode != old_mode {
|
||||||
|
self.buffer_mut().set_redraw(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "swash")]
|
||||||
|
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, f32)> {
|
||||||
|
//TODO: better calculation of width
|
||||||
|
let default_width = (font_size as f32) / 2.0;
|
||||||
|
if cursor.line == line_i {
|
||||||
|
for (glyph_i, glyph) in run.glyphs.iter().enumerate() {
|
||||||
|
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 width = glyph.w / (total as f32);
|
||||||
|
let offset = (before as f32) * width;
|
||||||
|
return Some((glyph_i, offset, width));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match run.glyphs.last() {
|
||||||
|
Some(glyph) => {
|
||||||
|
if cursor.index == glyph.end {
|
||||||
|
return Some((run.glyphs.len(), 0.0, default_width));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
return Some((0, 0.0, default_width));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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_width)) = cursor_glyph_opt(&self.cursor()) {
|
||||||
|
let block_cursor = match self.mode {
|
||||||
|
Mode::Normal => true,
|
||||||
|
Mode::Insert => false,
|
||||||
|
_ => true /*TODO: determine block cursor in other modes*/
|
||||||
|
};
|
||||||
|
|
||||||
|
let (start_x, end_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,
|
||||||
|
(glyph.x + glyph.w - cursor_glyph_offset - cursor_glyph_width) as i32,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
(glyph.x + cursor_glyph_offset) as i32,
|
||||||
|
(glyph.x + cursor_glyph_offset + cursor_glyph_width) as i32
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => match run.glyphs.last() {
|
||||||
|
Some(glyph) => {
|
||||||
|
// End of last glyph
|
||||||
|
if glyph.rtl {
|
||||||
|
(
|
||||||
|
glyph.x as i32,
|
||||||
|
(glyph.x - cursor_glyph_width) as i32
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
(glyph.x + glyph.w) as i32,
|
||||||
|
(glyph.x + glyph.w + cursor_glyph_width) as i32
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
// Start of empty line
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
cursor_glyph_width as i32
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if block_cursor {
|
||||||
|
let left_x = cmp::min(start_x, end_x);
|
||||||
|
let right_x = cmp::max(start_x, end_x);
|
||||||
|
f(
|
||||||
|
left_x,
|
||||||
|
line_y - font_size,
|
||||||
|
(right_x - left_x) as u32,
|
||||||
|
line_height as u32,
|
||||||
|
Color::rgba(color.r(), color.g(), color.b(), 0x33),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
f(
|
||||||
|
start_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)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -36,7 +36,7 @@ fn han_unification(locale: &str) -> &'static [&'static str] {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallbacks to use per script
|
// Fallbacks to use per script
|
||||||
pub fn script_fallback(script: &Script, locale: &str) -> &'static [&'static str] {
|
pub fn script_fallback(script: Script, locale: &str) -> &'static [&'static str] {
|
||||||
//TODO: abstract style (sans/serif/monospaced)
|
//TODO: abstract style (sans/serif/monospaced)
|
||||||
//TODO: pull more data from about:config font.name-list.sans-serif in Firefox
|
//TODO: pull more data from about:config font.name-list.sans-serif in Firefox
|
||||||
match script {
|
match script {
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ impl<'a> Iterator for FontFallbackIter<'a> {
|
||||||
while self.script_i.0 < self.scripts.len() {
|
while self.script_i.0 < self.scripts.len() {
|
||||||
let script = self.scripts[self.script_i.0];
|
let script = self.scripts[self.script_i.0];
|
||||||
|
|
||||||
let script_families = script_fallback(&script, self.locale);
|
let script_families = script_fallback(script, self.locale);
|
||||||
while self.script_i.1 < script_families.len() {
|
while self.script_i.1 < script_families.len() {
|
||||||
let script_family = script_families[self.script_i.1];
|
let script_family = script_families[self.script_i.1];
|
||||||
self.script_i.1 += 1;
|
self.script_i.1 += 1;
|
||||||
|
|
@ -133,7 +133,7 @@ impl<'a> Iterator for FontFallbackIter<'a> {
|
||||||
return Some(font);
|
return Some(font);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log::warn!("failed to find family '{}'", common_family)
|
log::warn!("failed to find family '{}'", common_family);
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: do we need to do this?
|
//TODO: do we need to do this?
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,6 @@ pub fn forbidden_fallback() -> &'static [&'static str] {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallbacks to use per script
|
// Fallbacks to use per script
|
||||||
pub fn script_fallback(script: &Script, locale: &str) -> &'static [&'static str] {
|
pub fn script_fallback(_script: Script, _locale: &str) -> &'static [&'static str] {
|
||||||
&[]
|
&[]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ fn han_unification(locale: &str) -> &'static [&'static str] {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallbacks to use per script
|
// Fallbacks to use per script
|
||||||
pub fn script_fallback(script: &Script, locale: &str) -> &'static [&'static str] {
|
pub fn script_fallback(script: Script, locale: &str) -> &'static [&'static str] {
|
||||||
//TODO: abstract style (sans/serif/monospaced)
|
//TODO: abstract style (sans/serif/monospaced)
|
||||||
match script {
|
match script {
|
||||||
Script::Adlam => &["Noto Sans Adlam", "Noto Sans Adlam Unjoined"],
|
Script::Adlam => &["Noto Sans Adlam", "Noto Sans Adlam Unjoined"],
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ fn han_unification(locale: &str) -> &'static [&'static str] {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallbacks to use per script
|
// Fallbacks to use per script
|
||||||
pub fn script_fallback(script: &Script, locale: &str) -> &'static [&'static str] {
|
pub fn script_fallback(script: Script, locale: &str) -> &'static [&'static str] {
|
||||||
//TODO: better match https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/platform/fonts/win/font_fallback_win.cc#L99
|
//TODO: better match https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/platform/fonts/win/font_fallback_win.cc#L99
|
||||||
match script {
|
match script {
|
||||||
Script::Adlam => &["Ebrima"],
|
Script::Adlam => &["Ebrima"],
|
||||||
|
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
|
||||||
|
|
||||||
use core::ops::Deref;
|
|
||||||
|
|
||||||
pub struct Font<'a> {
|
|
||||||
pub info: &'a fontdb::FaceInfo,
|
|
||||||
pub data: &'a [u8],
|
|
||||||
pub rustybuzz: rustybuzz::Face<'a>,
|
|
||||||
#[cfg(feature = "swash")]
|
|
||||||
pub swash: (u32, swash::CacheKey),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Font<'a> {
|
|
||||||
pub fn new(info: &'a fontdb::FaceInfo) -> Option<Self> {
|
|
||||||
let data = match &info.source {
|
|
||||||
fontdb::Source::Binary(data) => data.deref().as_ref(),
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
fontdb::Source::File(path) => {
|
|
||||||
log::warn!("Unsupported fontdb Source::File('{}')", path.display());
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
fontdb::Source::SharedFile(_path, data) => data.deref().as_ref(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(Self {
|
|
||||||
info,
|
|
||||||
data,
|
|
||||||
rustybuzz: rustybuzz::Face::from_slice(data, info.index)?,
|
|
||||||
#[cfg(feature = "swash")]
|
|
||||||
swash: {
|
|
||||||
let swash = swash::FontRef::from_index(data, info.index as usize)?;
|
|
||||||
(swash.offset, swash.key)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "swash")]
|
|
||||||
pub fn as_swash(&self) -> swash::FontRef {
|
|
||||||
swash::FontRef {
|
|
||||||
data: self.data,
|
|
||||||
offset: self.swash.0,
|
|
||||||
key: self.swash.1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +1,54 @@
|
||||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
pub(crate) mod fallback;
|
use core::ops::Deref;
|
||||||
|
|
||||||
pub(crate) use self::font::*;
|
pub(crate) mod fallback;
|
||||||
mod font;
|
|
||||||
|
|
||||||
pub use self::matches::*;
|
pub use self::matches::*;
|
||||||
mod matches;
|
mod matches;
|
||||||
|
|
||||||
pub use self::system::*;
|
pub use self::system::*;
|
||||||
mod system;
|
mod system;
|
||||||
|
|
||||||
|
pub struct Font<'a> {
|
||||||
|
pub info: &'a fontdb::FaceInfo,
|
||||||
|
pub data: &'a [u8],
|
||||||
|
pub rustybuzz: rustybuzz::Face<'a>,
|
||||||
|
#[cfg(feature = "swash")]
|
||||||
|
pub swash: (u32, swash::CacheKey),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Font<'a> {
|
||||||
|
pub fn new(info: &'a fontdb::FaceInfo) -> Option<Self> {
|
||||||
|
let data = match &info.source {
|
||||||
|
fontdb::Source::Binary(data) => data.deref().as_ref(),
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
fontdb::Source::File(path) => {
|
||||||
|
log::warn!("Unsupported fontdb Source::File('{}')", path.display());
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
fontdb::Source::SharedFile(_path, data) => data.deref().as_ref(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Self {
|
||||||
|
info,
|
||||||
|
data,
|
||||||
|
rustybuzz: rustybuzz::Face::from_slice(data, info.index)?,
|
||||||
|
#[cfg(feature = "swash")]
|
||||||
|
swash: {
|
||||||
|
let swash = swash::FontRef::from_index(data, info.index as usize)?;
|
||||||
|
(swash.offset, swash.key)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "swash")]
|
||||||
|
pub fn as_swash(&self) -> swash::FontRef {
|
||||||
|
swash::FontRef {
|
||||||
|
data: self.data,
|
||||||
|
offset: self.swash.0,
|
||||||
|
key: self.swash.1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,13 @@ pub use self::no_std::*;
|
||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
mod no_std;
|
mod no_std;
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
//TODO: use std implementation on Redox
|
||||||
|
#[cfg(all(feature = "std", target_os = "redox"))]
|
||||||
|
pub use self::redox::*;
|
||||||
|
#[cfg(all(feature = "std", target_os = "redox"))]
|
||||||
|
mod redox;
|
||||||
|
|
||||||
|
#[cfg(all(feature = "std", not(target_os = "redox")))]
|
||||||
pub use self::std::*;
|
pub use self::std::*;
|
||||||
#[cfg(feature = "std")]
|
#[cfg(all(feature = "std", not(target_os = "redox")))]
|
||||||
mod std;
|
mod std;
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@ impl FontSystem {
|
||||||
&self.db
|
&self.db
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clippy false positive
|
||||||
|
#[allow(clippy::needless_lifetimes)]
|
||||||
pub fn get_font<'a>(&'a self, id: fontdb::ID) -> Option<Arc<Font<'a>>> {
|
pub fn get_font<'a>(&'a self, id: fontdb::ID) -> Option<Arc<Font<'a>>> {
|
||||||
let face = self.db.face(id)?;
|
let face = self.db.face(id)?;
|
||||||
match Font::new(face) {
|
match Font::new(face) {
|
||||||
|
|
@ -60,9 +62,8 @@ impl FontSystem {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.get_font(face.id) {
|
if let Some(font) = self.get_font(face.id) {
|
||||||
Some(font) => fonts.push(font),
|
fonts.push(font);
|
||||||
None => (),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
104
src/font/system/redox.rs
Normal file
104
src/font/system/redox.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use alloc::{
|
||||||
|
string::{String, ToString},
|
||||||
|
sync::Arc,
|
||||||
|
vec::Vec,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{Attrs, Font, FontMatches};
|
||||||
|
|
||||||
|
/// Access system fonts
|
||||||
|
pub struct FontSystem{
|
||||||
|
locale: String,
|
||||||
|
db: fontdb::Database,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FontSystem {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let locale = sys_locale::get_locale().unwrap_or_else(|| {
|
||||||
|
log::warn!("failed to get system locale, falling back to en-US");
|
||||||
|
String::from("en-US")
|
||||||
|
});
|
||||||
|
log::info!("Locale: {}", locale);
|
||||||
|
|
||||||
|
//TODO: allow loading fonts from memory
|
||||||
|
let mut db = fontdb::Database::new();
|
||||||
|
{
|
||||||
|
let now = std::time::Instant::now();
|
||||||
|
|
||||||
|
db.load_fonts_dir("/ui/fonts");
|
||||||
|
//TODO: configurable default fonts
|
||||||
|
db.set_monospace_family("Fira Mono");
|
||||||
|
db.set_sans_serif_family("Fira Sans");
|
||||||
|
db.set_serif_family("DejaVu Serif");
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"Parsed {} font faces in {}ms.",
|
||||||
|
db.len(),
|
||||||
|
now.elapsed().as_millis()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let now = std::time::Instant::now();
|
||||||
|
|
||||||
|
//TODO only do this on demand!
|
||||||
|
for i in 0..db.faces().len() {
|
||||||
|
let id = db.faces()[i].id;
|
||||||
|
unsafe { db.make_shared_face_data(id); }
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"Mapped {} font faces in {}ms.",
|
||||||
|
db.len(),
|
||||||
|
now.elapsed().as_millis()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
locale,
|
||||||
|
db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn locale(&self) -> &str {
|
||||||
|
&self.locale
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn db(&self) -> &fontdb::Database {
|
||||||
|
&self.db
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clippy false positive
|
||||||
|
#[allow(clippy::needless_lifetimes)]
|
||||||
|
pub fn get_font<'a>(&'a self, id: fontdb::ID) -> Option<Arc<Font<'a>>> {
|
||||||
|
let face = self.db.face(id)?;
|
||||||
|
match Font::new(face) {
|
||||||
|
Some(font) => Some(Arc::new(font)),
|
||||||
|
None => {
|
||||||
|
log::warn!("failed to load font '{}'", face.post_script_name);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_font_matches<'a>(&'a self, attrs: Attrs) -> Arc<FontMatches<'a>> {
|
||||||
|
let mut fonts = Vec::new();
|
||||||
|
for face in self.db.faces() {
|
||||||
|
if !attrs.matches(face) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(font) = self.get_font(face.id) {
|
||||||
|
fonts.push(font);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Arc::new(FontMatches {
|
||||||
|
locale: &self.locale,
|
||||||
|
default_family: self.db.family_name(&attrs.family).to_string(),
|
||||||
|
fonts
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,7 @@ struct FontSystemInner {
|
||||||
pub struct FontSystem(FontSystemInner);
|
pub struct FontSystem(FontSystemInner);
|
||||||
|
|
||||||
impl FontSystem {
|
impl FontSystem {
|
||||||
|
/// Create a new [`FontSystem`], that allows access to any installed system fonts
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let locale = sys_locale::get_locale().unwrap_or_else(|| {
|
let locale = sys_locale::get_locale().unwrap_or_else(|| {
|
||||||
log::warn!("failed to get system locale, falling back to en-US");
|
log::warn!("failed to get system locale, falling back to en-US");
|
||||||
|
|
@ -51,8 +52,7 @@ impl FontSystem {
|
||||||
let now = std::time::Instant::now();
|
let now = std::time::Instant::now();
|
||||||
|
|
||||||
//TODO only do this on demand!
|
//TODO only do this on demand!
|
||||||
assert_eq!(db.len(), db.faces().len());
|
for i in 0..db.faces().len() {
|
||||||
for i in 0..db.len() {
|
|
||||||
let id = db.faces()[i].id;
|
let id = db.faces()[i].id;
|
||||||
unsafe { db.make_shared_face_data(id); }
|
unsafe { db.make_shared_face_data(id); }
|
||||||
}
|
}
|
||||||
|
|
@ -80,6 +80,8 @@ impl FontSystem {
|
||||||
self.0.borrow_db()
|
self.0.borrow_db()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clippy false positive
|
||||||
|
#[allow(clippy::needless_lifetimes)]
|
||||||
pub fn get_font<'a>(&'a self, id: fontdb::ID) -> Option<Arc<Font<'a>>> {
|
pub fn get_font<'a>(&'a self, id: fontdb::ID) -> Option<Arc<Font<'a>>> {
|
||||||
self.0.with(|fields| {
|
self.0.with(|fields| {
|
||||||
get_font(&fields, id)
|
get_font(&fields, id)
|
||||||
|
|
@ -88,7 +90,7 @@ impl FontSystem {
|
||||||
|
|
||||||
pub fn get_font_matches<'a>(&'a self, attrs: Attrs) -> Arc<FontMatches<'a>> {
|
pub fn get_font_matches<'a>(&'a self, attrs: Attrs) -> Arc<FontMatches<'a>> {
|
||||||
self.0.with(|fields| {
|
self.0.with(|fields| {
|
||||||
let mut font_matches_cache = fields.font_matches_cache.lock().unwrap();
|
let mut font_matches_cache = fields.font_matches_cache.lock().expect("failed to lock font matches cache");
|
||||||
//TODO: do not create AttrsOwned unless entry does not already exist
|
//TODO: do not create AttrsOwned unless entry does not already exist
|
||||||
font_matches_cache.entry(AttrsOwned::new(attrs)).or_insert_with(|| {
|
font_matches_cache.entry(AttrsOwned::new(attrs)).or_insert_with(|| {
|
||||||
let now = std::time::Instant::now();
|
let now = std::time::Instant::now();
|
||||||
|
|
@ -99,9 +101,8 @@ impl FontSystem {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
match get_font(&fields, face.id) {
|
if let Some(font) = get_font(&fields, face.id) {
|
||||||
Some(font) => fonts.push(font),
|
fonts.push(font);
|
||||||
None => (),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,7 +122,7 @@ impl FontSystem {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_font<'b>(fields: &ouroboros_impl_font_system_inner::BorrowedFields<'_, 'b>, id: fontdb::ID) -> Option<Arc<Font<'b>>> {
|
fn get_font<'b>(fields: &ouroboros_impl_font_system_inner::BorrowedFields<'_, 'b>, id: fontdb::ID) -> Option<Arc<Font<'b>>> {
|
||||||
fields.font_cache.lock().unwrap().entry(id).or_insert_with(|| {
|
fields.font_cache.lock().expect("failed to lock font cache").entry(id).or_insert_with(|| {
|
||||||
let face = fields.db.face(id)?;
|
let face = fields.db.face(id)?;
|
||||||
match Font::new(face) {
|
match Font::new(face) {
|
||||||
Some(font) => Some(Arc::new(font)),
|
Some(font) => Some(Arc::new(font)),
|
||||||
|
|
|
||||||
48
src/lib.rs
48
src/lib.rs
|
|
@ -7,8 +7,8 @@
|
||||||
//! rustybuzz, font discovery utilizes fontdb, and the rasterization is optional and utilizes
|
//! rustybuzz, font discovery utilizes fontdb, and the rasterization is optional and utilizes
|
||||||
//! 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
|
||||||
//! [Buffer], 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.
|
||||||
//!
|
//!
|
||||||
//! ```
|
//! ```
|
||||||
|
|
@ -54,6 +54,41 @@
|
||||||
//! });
|
//! });
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
|
// Not interested in these lints
|
||||||
|
#![allow(clippy::new_without_default)]
|
||||||
|
|
||||||
|
// TODO: address ocurrances and then deny
|
||||||
|
//
|
||||||
|
// Indexing a slice can cause panics and that is something we always want to avoid
|
||||||
|
#![allow(clippy::indexing_slicing)]
|
||||||
|
// Overflows can produce unpredictable results and are only checked in debug builds
|
||||||
|
#![allow(clippy::integer_arithmetic)]
|
||||||
|
|
||||||
|
// Soundness issues
|
||||||
|
//
|
||||||
|
// Dereferencing unalinged pointers may be undefined behavior
|
||||||
|
#![deny(clippy::cast_ptr_alignment)]
|
||||||
|
// Avoid panicking in without information about the panic. Use expect
|
||||||
|
#![deny(clippy::unwrap_used)]
|
||||||
|
// This is usually a serious issue - a missing import of a define where it is interpreted
|
||||||
|
// as a catch-all variable in a match, for example
|
||||||
|
#![deny(unreachable_patterns)]
|
||||||
|
// Ensure that all must_use results are used
|
||||||
|
#![deny(unused_must_use)]
|
||||||
|
|
||||||
|
// Style issues
|
||||||
|
//
|
||||||
|
// Documentation not ideal
|
||||||
|
#![warn(clippy::doc_markdown)]
|
||||||
|
// Document possible errors
|
||||||
|
#![warn(clippy::missing_errors_doc)]
|
||||||
|
// Document possible panics
|
||||||
|
#![warn(clippy::missing_panics_doc)]
|
||||||
|
// Ensure semicolons are present
|
||||||
|
#![warn(clippy::semicolon_if_nothing_returned)]
|
||||||
|
// Ensure numbers are readable
|
||||||
|
#![warn(clippy::unreadable_literal)]
|
||||||
|
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
#![cfg_attr(not(feature = "std"), no_std)]
|
||||||
|
|
||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
|
|
@ -70,8 +105,8 @@ mod buffer_line;
|
||||||
pub use self::cache::*;
|
pub use self::cache::*;
|
||||||
mod cache;
|
mod cache;
|
||||||
|
|
||||||
pub use self::editor::*;
|
pub use self::edit::*;
|
||||||
mod editor;
|
mod edit;
|
||||||
|
|
||||||
pub use self::font::*;
|
pub use self::font::*;
|
||||||
mod font;
|
mod font;
|
||||||
|
|
@ -86,8 +121,3 @@ mod shape;
|
||||||
pub use self::swash::*;
|
pub use self::swash::*;
|
||||||
#[cfg(feature = "swash")]
|
#[cfg(feature = "swash")]
|
||||||
mod swash;
|
mod swash;
|
||||||
|
|
||||||
#[cfg(feature = "syntect")]
|
|
||||||
pub use self::syntect::*;
|
|
||||||
#[cfg(feature = "syntect")]
|
|
||||||
mod syntect;
|
|
||||||
|
|
|
||||||
26
src/shape.rs
26
src/shape.rs
|
|
@ -30,11 +30,7 @@ fn shape_fallback(
|
||||||
buffer.push_str(run);
|
buffer.push_str(run);
|
||||||
buffer.guess_segment_properties();
|
buffer.guess_segment_properties();
|
||||||
|
|
||||||
let rtl = match buffer.direction() {
|
let rtl = matches!(buffer.direction(), rustybuzz::Direction::RightToLeft);
|
||||||
rustybuzz::Direction::RightToLeft => true,
|
|
||||||
//TODO: other directions?
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
assert_eq!(rtl, span_rtl);
|
assert_eq!(rtl, span_rtl);
|
||||||
|
|
||||||
let glyph_buffer = rustybuzz::shape(&font.rustybuzz, &[], buffer);
|
let glyph_buffer = rustybuzz::shape(&font.rustybuzz, &[], buffer);
|
||||||
|
|
@ -64,7 +60,7 @@ fn shape_fallback(
|
||||||
x_offset,
|
x_offset,
|
||||||
y_offset,
|
y_offset,
|
||||||
font_id: font.info.id,
|
font_id: font.info.id,
|
||||||
glyph_id: info.glyph_id.try_into().unwrap(),
|
glyph_id: info.glyph_id.try_into().expect("failed to cast glyph ID"),
|
||||||
color_opt: None,
|
color_opt: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +92,7 @@ fn shape_fallback(
|
||||||
|
|
||||||
// Set color
|
// Set color
|
||||||
//TODO: these attributes should not be related to shaping
|
//TODO: these attributes should not be related to shaping
|
||||||
for glyph in glyphs.iter_mut() {
|
for glyph in &mut glyphs {
|
||||||
let attrs = attrs_list.get_span(glyph.start);
|
let attrs = attrs_list.get_span(glyph.start);
|
||||||
glyph.color_opt = attrs.color_opt;
|
glyph.color_opt = attrs.color_opt;
|
||||||
}
|
}
|
||||||
|
|
@ -145,7 +141,7 @@ fn shape_run<'a>(
|
||||||
);
|
);
|
||||||
|
|
||||||
let (mut glyphs, mut missing) = shape_fallback(
|
let (mut glyphs, mut missing) = shape_fallback(
|
||||||
font_iter.next().unwrap(),
|
font_iter.next().expect("no default font found"),
|
||||||
line,
|
line,
|
||||||
attrs_list,
|
attrs_list,
|
||||||
start_run,
|
start_run,
|
||||||
|
|
@ -401,7 +397,7 @@ impl ShapeSpan {
|
||||||
|
|
||||||
// Reverse glyphs in RTL lines
|
// Reverse glyphs in RTL lines
|
||||||
if line_rtl {
|
if line_rtl {
|
||||||
for word in words.iter_mut() {
|
for word in &mut words {
|
||||||
word.glyphs.reverse();
|
word.glyphs.reverse();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -493,7 +489,7 @@ impl ShapeLine {
|
||||||
let end_x = if self.rtl { 0.0 } else { line_width as f32 };
|
let end_x = if self.rtl { 0.0 } else { line_width as f32 };
|
||||||
let mut x = start_x;
|
let mut x = start_x;
|
||||||
let mut y = 0.0;
|
let mut y = 0.0;
|
||||||
for span in self.spans.iter() {
|
for span in &self.spans {
|
||||||
//TODO: improve performance!
|
//TODO: improve performance!
|
||||||
let mut word_ranges = Vec::new();
|
let mut word_ranges = Vec::new();
|
||||||
if wrap_simple {
|
if wrap_simple {
|
||||||
|
|
@ -505,7 +501,7 @@ impl ShapeLine {
|
||||||
let word = &span.words[i];
|
let word = &span.words[i];
|
||||||
|
|
||||||
let mut word_size = 0.0;
|
let mut word_size = 0.0;
|
||||||
for glyph in word.glyphs.iter() {
|
for glyph in &word.glyphs {
|
||||||
word_size += font_size as f32 * glyph.x_advance;
|
word_size += font_size as f32 * glyph.x_advance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -553,7 +549,7 @@ impl ShapeLine {
|
||||||
let word = &span.words[i];
|
let word = &span.words[i];
|
||||||
|
|
||||||
let mut word_size = 0.0;
|
let mut word_size = 0.0;
|
||||||
for glyph in word.glyphs.iter() {
|
for glyph in &word.glyphs {
|
||||||
word_size += font_size as f32 * glyph.x_advance;
|
word_size += font_size as f32 * glyph.x_advance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -583,7 +579,7 @@ impl ShapeLine {
|
||||||
for (range, wrap) in word_ranges {
|
for (range, wrap) in word_ranges {
|
||||||
for word in span.words[range].iter() {
|
for word in span.words[range].iter() {
|
||||||
let mut word_size = 0.0;
|
let mut word_size = 0.0;
|
||||||
for glyph in word.glyphs.iter() {
|
for glyph in &word.glyphs {
|
||||||
word_size += font_size as f32 * glyph.x_advance;
|
word_size += font_size as f32 * glyph.x_advance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -605,7 +601,7 @@ impl ShapeLine {
|
||||||
y = 0.0;
|
y = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
for glyph in word.glyphs.iter() {
|
for glyph in &word.glyphs {
|
||||||
let x_advance = font_size as f32 * glyph.x_advance;
|
let x_advance = font_size as f32 * glyph.x_advance;
|
||||||
let y_advance = font_size as f32 * glyph.y_advance;
|
let y_advance = font_size as f32 * glyph.y_advance;
|
||||||
|
|
||||||
|
|
@ -630,7 +626,7 @@ impl ShapeLine {
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.rtl {
|
if self.rtl {
|
||||||
x -= x_advance
|
x -= x_advance;
|
||||||
}
|
}
|
||||||
|
|
||||||
glyphs.push(glyph.layout(font_size, x, y, span.rtl));
|
glyphs.push(glyph.layout(font_size, x, y, span.rtl));
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use crate::{CacheKey, Color, FontSystem};
|
||||||
|
|
||||||
pub use swash::scale::image::{Content as SwashContent, Image as SwashImage};
|
pub use swash::scale::image::{Content as SwashContent, Image as SwashImage};
|
||||||
|
|
||||||
fn swash_image<'a>(font_system: &'a FontSystem, context: &mut ScaleContext, cache_key: CacheKey) -> Option<SwashImage> {
|
fn swash_image(font_system: &FontSystem, context: &mut ScaleContext, cache_key: CacheKey) -> Option<SwashImage> {
|
||||||
let font = match font_system.get_font(cache_key.font_id) {
|
let font = match font_system.get_font(cache_key.font_id) {
|
||||||
Some(some) => some,
|
Some(some) => some,
|
||||||
None => {
|
None => {
|
||||||
|
|
@ -61,7 +61,7 @@ impl<'a> SwashCache<'a> {
|
||||||
/// Create a new swash cache
|
/// Create a new swash cache
|
||||||
pub fn new(font_system: &'a FontSystem) -> Self {
|
pub fn new(font_system: &'a FontSystem) -> Self {
|
||||||
Self {
|
Self {
|
||||||
font_system: font_system,
|
font_system,
|
||||||
context: ScaleContext::new(),
|
context: ScaleContext::new(),
|
||||||
image_cache: Map::new()
|
image_cache: Map::new()
|
||||||
}
|
}
|
||||||
|
|
@ -101,7 +101,7 @@ impl<'a> SwashCache<'a> {
|
||||||
y + off_y,
|
y + off_y,
|
||||||
Color(
|
Color(
|
||||||
((image.data[i] as u32) << 24) |
|
((image.data[i] as u32) << 24) |
|
||||||
base.0 & 0xFFFFFF
|
base.0 & 0xFF_FF_FF
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
i += 1;
|
i += 1;
|
||||||
|
|
|
||||||
16
test.sh
16
test.sh
|
|
@ -2,13 +2,17 @@
|
||||||
|
|
||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
|
echo Run CI script
|
||||||
|
./ci.sh
|
||||||
|
|
||||||
|
echo Build documentation
|
||||||
cargo doc
|
cargo doc
|
||||||
cargo test
|
|
||||||
cargo build --release --no-default-features
|
echo Build all examples
|
||||||
cargo build --release --no-default-features --features std
|
|
||||||
cargo build --release --no-default-features --features swash
|
|
||||||
cargo build --release --no-default-features --features syntect
|
|
||||||
cargo build --release --all-features
|
|
||||||
cargo build --release --all
|
cargo build --release --all
|
||||||
|
|
||||||
|
echo Run terminal example
|
||||||
target/release/terminal
|
target/release/terminal
|
||||||
|
|
||||||
|
echo Run editor-test example
|
||||||
env RUST_LOG=editor_test=info target/release/editor-test
|
env RUST_LOG=editor_test=info target/release/editor-test
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue