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:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --all-features
|
||||
- name: Run Rust CI
|
||||
run: ./ci.sh
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "cosmic-text"
|
||||
description = "Pure Rust multi-line text handling"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0-alpha1"
|
||||
authors = ["Jeremy Soller <jeremy@system76.com>"]
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
|
@ -39,6 +39,7 @@ std = [
|
|||
"sys-locale",
|
||||
"unicode-bidi/std",
|
||||
]
|
||||
vi = ["syntect"]
|
||||
|
||||
[workspace]
|
||||
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
|
||||
#default-features = false
|
||||
#features = ["xdg-portal"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
vi = ["cosmic-text/vi"]
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ use cosmic_text::{
|
|||
Attrs,
|
||||
AttrsList,
|
||||
Buffer,
|
||||
Edit,
|
||||
FontSystem,
|
||||
Metrics,
|
||||
SyntaxEditor,
|
||||
|
|
@ -39,9 +40,6 @@ use std::{
|
|||
sync::Mutex,
|
||||
};
|
||||
|
||||
use self::syntax_text_box::syntax_text_box;
|
||||
mod syntax_text_box;
|
||||
|
||||
use self::text::text;
|
||||
mod text;
|
||||
|
||||
|
|
@ -74,7 +72,10 @@ pub struct Window {
|
|||
theme: Theme,
|
||||
path_opt: Option<PathBuf>,
|
||||
attrs: Attrs<'static>,
|
||||
#[cfg(not(feature = "vi"))]
|
||||
editor: Mutex<SyntaxEditor<'static>>,
|
||||
#[cfg(feature = "vi")]
|
||||
editor: Mutex<cosmic_text::ViEditor<'static>>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
|
@ -121,6 +122,10 @@ impl Application for Window {
|
|||
&SYNTAX_SYSTEM,
|
||||
"base16-eighties.dark"
|
||||
).unwrap();
|
||||
|
||||
#[cfg(feature = "vi")]
|
||||
let mut editor = cosmic_text::ViEditor::new(editor);
|
||||
|
||||
update_attrs(&mut editor, attrs);
|
||||
|
||||
let mut window = Window {
|
||||
|
|
@ -180,7 +185,7 @@ impl Application for Window {
|
|||
});
|
||||
|
||||
let mut editor = self.editor.lock().unwrap();
|
||||
update_attrs(&mut editor, self.attrs);
|
||||
update_attrs(&mut *editor, self.attrs);
|
||||
},
|
||||
Message::Italic(italic) => {
|
||||
self.attrs = self.attrs.style(if italic {
|
||||
|
|
@ -190,7 +195,7 @@ impl Application for Window {
|
|||
});
|
||||
|
||||
let mut editor = self.editor.lock().unwrap();
|
||||
update_attrs(&mut editor, self.attrs);
|
||||
update_attrs(&mut *editor, self.attrs);
|
||||
},
|
||||
Message::Monospaced(monospaced) => {
|
||||
self.attrs = self.attrs
|
||||
|
|
@ -202,7 +207,7 @@ impl Application for Window {
|
|||
.monospaced(monospaced);
|
||||
|
||||
let mut editor = self.editor.lock().unwrap();
|
||||
update_attrs(&mut editor, self.attrs);
|
||||
update_attrs(&mut *editor, self.attrs);
|
||||
},
|
||||
Message::MetricsChanged(metrics) => {
|
||||
let mut editor = self.editor.lock().unwrap();
|
||||
|
|
@ -220,7 +225,7 @@ impl Application for Window {
|
|||
self.attrs = self.attrs.color(cosmic_text::Color::rgba(as_u8(r), as_u8(g), as_u8(b), as_u8(a)));
|
||||
|
||||
let mut editor = self.editor.lock().unwrap();
|
||||
update_attrs(&mut editor, self.attrs);
|
||||
update_attrs(&mut *editor, self.attrs);
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -266,7 +271,7 @@ impl Application for Window {
|
|||
.align_items(Alignment::Center)
|
||||
.spacing(8)
|
||||
,
|
||||
syntax_text_box(&self.editor)
|
||||
text_box(&self.editor)
|
||||
]
|
||||
.spacing(8)
|
||||
.padding(16)
|
||||
|
|
@ -277,7 +282,7 @@ impl Application for Window {
|
|||
}
|
||||
}
|
||||
|
||||
fn update_attrs<'a>(editor: &mut SyntaxEditor<'a>, attrs: Attrs<'a>) {
|
||||
fn update_attrs<'a, T: Edit<'a>>(editor: &mut T, attrs: Attrs<'a>) {
|
||||
editor.buffer_mut().lines.iter_mut().for_each(|line| {
|
||||
line.set_attrs_list(AttrsList::new(attrs));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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::{
|
||||
Action,
|
||||
Editor,
|
||||
Edit,
|
||||
SwashCache,
|
||||
};
|
||||
use std::{
|
||||
|
|
@ -51,34 +51,35 @@ impl StyleSheet for Theme {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct TextBox<'a> {
|
||||
editor: &'a Mutex<Editor<'static>>,
|
||||
pub struct TextBox<'a, Editor> {
|
||||
editor: &'a Mutex<Editor>,
|
||||
padding: Padding,
|
||||
}
|
||||
|
||||
impl<'a> TextBox<'a> {
|
||||
pub fn new(editor: &'a Mutex<Editor<'static>>) -> Self {
|
||||
impl<'a, Editor> TextBox<'a, Editor> {
|
||||
pub fn new(editor: &'a Mutex<Editor>) -> Self {
|
||||
Self {
|
||||
editor,
|
||||
padding: Padding::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||
self.padding = padding.into();
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
pub fn text_box<'a>(editor: &'a Mutex<Editor<'static>>) -> TextBox<'a> {
|
||||
pub fn text_box<'a, Editor>(editor: &'a Mutex<Editor>) -> TextBox<'a, Editor> {
|
||||
TextBox::new(editor)
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer> for TextBox<'a>
|
||||
impl<'a, 'editor, Editor, Message, Renderer> Widget<Message, Renderer> for TextBox<'a, Editor>
|
||||
where
|
||||
Renderer: renderer::Renderer + image::Renderer<Handle = image::Handle>,
|
||||
Renderer::Theme: StyleSheet,
|
||||
Editor: Edit<'editor>,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
|
|
@ -105,17 +106,17 @@ where
|
|||
|
||||
//TODO: allow lazy shape
|
||||
let mut editor = self.editor.lock().unwrap();
|
||||
editor.buffer.shape_until(i32::max_value());
|
||||
editor.buffer_mut().shape_until(i32::max_value());
|
||||
|
||||
let mut layout_lines = 0;
|
||||
for line in editor.buffer.lines.iter() {
|
||||
for line in editor.buffer().lines.iter() {
|
||||
match line.layout_opt() {
|
||||
Some(layout) => layout_lines += layout.len(),
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
|
||||
let height = layout_lines as f32 * editor.buffer.metrics().line_height as f32;
|
||||
let height = layout_lines as f32 * editor.buffer().metrics().line_height as f32;
|
||||
let size = Size::new(limits.max().width, height);
|
||||
log::info!("size {:?}", size);
|
||||
|
||||
|
|
@ -174,7 +175,7 @@ where
|
|||
|
||||
let view_w = cmp::min(viewport.width as i32, layout.bounds().width as i32) - self.padding.horizontal() as i32;
|
||||
let view_h = cmp::min(viewport.height as i32, layout.bounds().height as i32) - self.padding.vertical() as i32;
|
||||
editor.buffer.set_size(view_w, view_h);
|
||||
editor.buffer_mut().set_size(view_w, view_h);
|
||||
|
||||
editor.shape_as_needed();
|
||||
|
||||
|
|
@ -270,6 +271,10 @@ where
|
|||
editor.action(Action::PageDown);
|
||||
status = Status::Captured;
|
||||
},
|
||||
KeyCode::Escape => {
|
||||
editor.action(Action::Escape);
|
||||
status = Status::Captured;
|
||||
},
|
||||
KeyCode::Enter => {
|
||||
editor.action(Action::Enter);
|
||||
status = Status::Captured;
|
||||
|
|
@ -327,12 +332,13 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<TextBox<'a>> for Element<'a, Message, Renderer>
|
||||
impl<'a, 'editor, Editor, Message, Renderer> From<TextBox<'a, Editor>> for Element<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: renderer::Renderer + image::Renderer<Handle = image::Handle>,
|
||||
Renderer::Theme: StyleSheet,
|
||||
Editor: Edit<'editor>,
|
||||
{
|
||||
fn from(text_box: TextBox<'a>) -> Self {
|
||||
fn from(text_box: TextBox<'a, Editor>) -> Self {
|
||||
Self::new(text_box)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,3 +12,8 @@ env_logger = "0.9"
|
|||
fontdb = "0.9"
|
||||
log = "0.4"
|
||||
orbclient = "0.3.35"
|
||||
unicode-segmentation = "1.7"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
vi = ["cosmic-text/vi"]
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use cosmic_text::{
|
|||
Action,
|
||||
Attrs,
|
||||
Buffer,
|
||||
Edit,
|
||||
Family,
|
||||
FontSystem,
|
||||
Metrics,
|
||||
|
|
@ -60,12 +61,16 @@ fn main() {
|
|||
let mut font_size_i = font_size_default;
|
||||
|
||||
let line_x = 8 * display_scale;
|
||||
|
||||
let mut editor = SyntaxEditor::new(
|
||||
Buffer::new(&font_system, font_sizes[font_size_i]),
|
||||
&syntax_system,
|
||||
"base16-eighties.dark"
|
||||
).unwrap();
|
||||
|
||||
#[cfg(feature = "vi")]
|
||||
let mut editor = cosmic_text::ViEditor::new(editor);
|
||||
|
||||
editor.buffer_mut().set_size(
|
||||
window.width() as i32 - line_x * 2,
|
||||
window.height() as i32
|
||||
|
|
@ -89,13 +94,14 @@ fn main() {
|
|||
let mut mouse_left = false;
|
||||
loop {
|
||||
editor.shape_as_needed();
|
||||
if editor.buffer_mut().redraw {
|
||||
if editor.buffer().redraw() {
|
||||
let instant = Instant::now();
|
||||
|
||||
let bg = editor.background_color();
|
||||
window.set(orbclient::Color::rgb(bg.r(), bg.g(), bg.b()));
|
||||
|
||||
editor.draw(&mut swash_cache, |x, y, w, h, color| {
|
||||
let fg = editor.foreground_color();
|
||||
editor.draw(&mut swash_cache, fg, |x, y, w, h, color| {
|
||||
window.rect(line_x + x, y, w, h, orbclient::Color { data: color.0 })
|
||||
});
|
||||
|
||||
|
|
@ -127,7 +133,7 @@ fn main() {
|
|||
|
||||
window.sync();
|
||||
|
||||
editor.buffer_mut().redraw = false;
|
||||
editor.buffer_mut().set_redraw(false);
|
||||
|
||||
log::debug!("redraw: {:?}", instant.elapsed());
|
||||
}
|
||||
|
|
@ -148,6 +154,7 @@ fn main() {
|
|||
orbclient::K_END if event.pressed => editor.action(Action::End),
|
||||
orbclient::K_PGUP if event.pressed => editor.action(Action::PageUp),
|
||||
orbclient::K_PGDN if event.pressed => editor.action(Action::PageDown),
|
||||
orbclient::K_ESC if event.pressed => editor.action(Action::Escape),
|
||||
orbclient::K_ENTER if event.pressed => editor.action(Action::Enter),
|
||||
orbclient::K_BKSP if event.pressed => editor.action(Action::Backspace),
|
||||
orbclient::K_DEL if event.pressed => editor.action(Action::Delete),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use cosmic_text::{Action, Buffer, Color, Editor, FontSystem, Metrics, SwashCache};
|
||||
use cosmic_text::{Action, Buffer, Color, Edit, Editor, FontSystem, Metrics, SwashCache};
|
||||
use orbclient::{EventOption, Renderer, Window, WindowFlag};
|
||||
use std::{env, fs, process, time::Instant};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
|
@ -10,7 +10,7 @@ fn redraw(window: &mut Window, editor: &mut Editor<'_>, swash_cache: &mut SwashC
|
|||
let font_color = Color::rgb(0xFF, 0xFF, 0xFF);
|
||||
|
||||
editor.shape_as_needed();
|
||||
if editor.buffer.redraw {
|
||||
if editor.buffer().redraw() {
|
||||
let instant = Instant::now();
|
||||
|
||||
window.set(bg_color);
|
||||
|
|
@ -21,7 +21,7 @@ fn redraw(window: &mut Window, editor: &mut Editor<'_>, swash_cache: &mut SwashC
|
|||
|
||||
window.sync();
|
||||
|
||||
editor.buffer.redraw = false;
|
||||
editor.buffer_mut().set_redraw(false);
|
||||
|
||||
let duration = instant.elapsed();
|
||||
log::debug!("redraw: {:?}", duration);
|
||||
|
|
@ -146,7 +146,7 @@ fn main() {
|
|||
|
||||
let mut wrong = 0;
|
||||
for (line_i, line) in text.lines().enumerate() {
|
||||
let buffer_line = &editor.buffer.lines[line_i];
|
||||
let buffer_line = &editor.buffer().lines[line_i];
|
||||
if buffer_line.text() != line {
|
||||
log::error!("line {}: {:?} != {:?}", line_i, buffer_line.text(), line);
|
||||
wrong += 1;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use cosmic_text::{
|
|||
Buffer,
|
||||
BufferLine,
|
||||
Color,
|
||||
Edit,
|
||||
Editor,
|
||||
Family,
|
||||
FontSystem,
|
||||
|
|
@ -49,7 +50,7 @@ fn main() {
|
|||
Metrics::new(32, 44).scale(display_scale)
|
||||
));
|
||||
|
||||
editor.buffer.set_size(
|
||||
editor.buffer_mut().set_size(
|
||||
window.width() as i32,
|
||||
window.height() as i32
|
||||
);
|
||||
|
|
@ -59,7 +60,7 @@ fn main() {
|
|||
let mono_attrs = attrs.monospaced(true).family(Family::Monospace);
|
||||
let comic_attrs = attrs.family(Family::Name("Comic Neue"));
|
||||
|
||||
editor.buffer.lines.clear();
|
||||
editor.buffer_mut().lines.clear();
|
||||
|
||||
let lines: &[&[(&str, Attrs)]] = &[
|
||||
&[
|
||||
|
|
@ -131,7 +132,7 @@ fn main() {
|
|||
let end = line_text.len();
|
||||
attrs_list.add_span(start..end, attrs);
|
||||
}
|
||||
editor.buffer.lines.push(BufferLine::new(line_text, attrs_list));
|
||||
editor.buffer_mut().lines.push(BufferLine::new(line_text, attrs_list));
|
||||
}
|
||||
|
||||
let mut swash_cache = SwashCache::new(&font_system);
|
||||
|
|
@ -145,7 +146,7 @@ fn main() {
|
|||
let font_color = Color::rgb(0xFF, 0xFF, 0xFF);
|
||||
|
||||
editor.shape_as_needed();
|
||||
if editor.buffer.redraw {
|
||||
if editor.buffer().redraw() {
|
||||
let instant = Instant::now();
|
||||
|
||||
window.set(bg_color);
|
||||
|
|
@ -156,7 +157,7 @@ fn main() {
|
|||
|
||||
window.sync();
|
||||
|
||||
editor.buffer.redraw = false;
|
||||
editor.buffer_mut().set_redraw(false);
|
||||
|
||||
let duration = instant.elapsed();
|
||||
log::debug!("redraw: {:?}", duration);
|
||||
|
|
@ -193,7 +194,7 @@ fn main() {
|
|||
}
|
||||
},
|
||||
EventOption::Resize(resize) => {
|
||||
editor.buffer.set_size(resize.width as i32, resize.height as i32);
|
||||
editor.buffer_mut().set_size(resize.width as i32, resize.height as i32);
|
||||
},
|
||||
EventOption::Quit(_) => process::exit(0),
|
||||
_ => (),
|
||||
|
|
|
|||
12
src/attrs.rs
12
src/attrs.rs
|
|
@ -35,25 +35,25 @@ impl Color {
|
|||
/// Get the red component
|
||||
#[inline]
|
||||
pub fn r(&self) -> u8 {
|
||||
((self.0 & 0x00FF0000) >> 16) as u8
|
||||
((self.0 & 0x00_FF_00_00) >> 16) as u8
|
||||
}
|
||||
|
||||
/// Get the green component
|
||||
#[inline]
|
||||
pub fn g(&self) -> u8 {
|
||||
((self.0 & 0x0000FF00) >> 8) as u8
|
||||
((self.0 & 0x00_00_FF_00) >> 8) as u8
|
||||
}
|
||||
|
||||
/// Get the blue component
|
||||
#[inline]
|
||||
pub fn b(&self) -> u8 {
|
||||
(self.0 & 0x000000FF) as u8
|
||||
(self.0 & 0x00_00_00_FF) as u8
|
||||
}
|
||||
|
||||
/// Get the alpha component
|
||||
#[inline]
|
||||
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 {
|
||||
match self {
|
||||
FamilyOwned::Name(name) => Family::Name(&name),
|
||||
FamilyOwned::Name(name) => Family::Name(name),
|
||||
FamilyOwned::Serif => Family::Serif,
|
||||
FamilyOwned::SansSerif => Family::SansSerif,
|
||||
FamilyOwned::Cursive => Family::Cursive,
|
||||
|
|
@ -278,7 +278,7 @@ impl AttrsList {
|
|||
}
|
||||
|
||||
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);
|
||||
|
||||
if resize {
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ pub struct LayoutRun<'a> {
|
|||
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> {
|
||||
buffer: &'b Buffer<'a>,
|
||||
line_i: usize,
|
||||
|
|
@ -145,8 +145,7 @@ impl fmt::Display for Metrics {
|
|||
|
||||
/// A buffer of text that is shaped and laid out
|
||||
pub struct Buffer<'a> {
|
||||
/// The [FontSystem] used by this [Buffer]
|
||||
pub font_system: &'a FontSystem,
|
||||
font_system: &'a FontSystem,
|
||||
/// [BufferLine]s (or paragraphs) of text in the buffer
|
||||
pub lines: Vec<BufferLine>,
|
||||
metrics: Metrics,
|
||||
|
|
@ -154,11 +153,11 @@ pub struct Buffer<'a> {
|
|||
height: i32,
|
||||
scroll: i32,
|
||||
/// True if a redraw is requires. Set to false after processing
|
||||
pub redraw: bool,
|
||||
redraw: bool,
|
||||
}
|
||||
|
||||
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(
|
||||
font_system: &'a FontSystem,
|
||||
metrics: Metrics,
|
||||
|
|
@ -180,7 +179,7 @@ impl<'a> Buffer<'a> {
|
|||
#[cfg(feature = "std")]
|
||||
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() {
|
||||
line.reset_layout();
|
||||
line.layout(
|
||||
|
|
@ -204,7 +203,7 @@ impl<'a> Buffer<'a> {
|
|||
|
||||
let mut reshaped = 0;
|
||||
let mut total_layout = 0;
|
||||
for line in self.lines.iter_mut() {
|
||||
for line in &mut self.lines {
|
||||
if total_layout >= lines {
|
||||
break;
|
||||
}
|
||||
|
|
@ -293,7 +292,8 @@ impl<'a> Buffer<'a> {
|
|||
pub fn layout_cursor(&self, cursor: &Cursor) -> LayoutCursor {
|
||||
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 (glyph_i, glyph) in layout_line.glyphs.iter().enumerate() {
|
||||
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
|
||||
pub fn line_shape(&mut self, line_i: usize) -> Option<&ShapeLine> {
|
||||
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
|
||||
pub fn line_layout(&mut self, line_i: usize) -> Option<&[LayoutLine]> {
|
||||
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 {
|
||||
self.metrics
|
||||
}
|
||||
|
||||
/// Set the current [Metrics]
|
||||
/// Set the current [`Metrics`]
|
||||
pub fn set_metrics(&mut self, metrics: Metrics) {
|
||||
if metrics != self.metrics {
|
||||
self.metrics = metrics;
|
||||
|
|
@ -408,6 +413,16 @@ impl<'a> Buffer<'a> {
|
|||
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
|
||||
pub fn layout_runs<'b>(&'b self) -> LayoutRunIter<'a, 'b> {
|
||||
LayoutRunIter::new(self)
|
||||
|
|
@ -439,15 +454,15 @@ impl<'a> Buffer<'a> {
|
|||
let mut new_cursor_char = 0;
|
||||
|
||||
let mut first_glyph = true;
|
||||
|
||||
|
||||
'hit: for (glyph_i, glyph) in run.glyphs.iter().enumerate() {
|
||||
if first_glyph {
|
||||
if first_glyph {
|
||||
first_glyph = false;
|
||||
if (run.rtl && x > glyph.x as i32) || (!run.rtl && x < 0) {
|
||||
new_cursor_glyph = 0;
|
||||
new_cursor_char = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if x >= glyph.x 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| {
|
||||
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 {
|
||||
/// Create a new line with the given text and attributes list
|
||||
/// Cached shaping and layout can be done using the [Self::shape] and
|
||||
/// [Self::layout] functions
|
||||
/// Cached shaping and layout can be done using the [`Self::shape`] and
|
||||
/// [`Self::layout`] functions
|
||||
pub fn new<T: Into<String>>(text: T, attrs_list: AttrsList) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
|
|
@ -40,7 +40,7 @@ impl BufferLine {
|
|||
/// Will reset shape and layout if it differs from current text and attributes list.
|
||||
/// Returns true if the line was reset
|
||||
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.attrs_list = attrs_list;
|
||||
self.reset();
|
||||
|
|
@ -143,7 +143,7 @@ impl BufferLine {
|
|||
self.shape_opt = Some(ShapeLine::new(font_system, &self.text, &self.attrs_list));
|
||||
self.layout_opt = None;
|
||||
}
|
||||
self.shape_opt.as_ref().unwrap()
|
||||
self.shape_opt.as_ref().expect("shape not found")
|
||||
}
|
||||
|
||||
/// Get line shaping cache
|
||||
|
|
@ -163,7 +163,7 @@ impl BufferLine {
|
|||
);
|
||||
self.layout_opt = Some(layout);
|
||||
}
|
||||
self.layout_opt.as_ref().unwrap()
|
||||
self.layout_opt.as_ref().expect("layout not found")
|
||||
}
|
||||
|
||||
/// Get line layout cache
|
||||
|
|
|
|||
|
|
@ -5,52 +5,13 @@ use alloc::string::{String, ToString};
|
|||
use core::cmp;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use crate::{AttrsList, Buffer, BufferLine, Cursor, LayoutCursor};
|
||||
use crate::{Action, AttrsList, Buffer, BufferLine, Cursor, Edit, LayoutCursor};
|
||||
#[cfg(feature = "swash")]
|
||||
use crate::Color;
|
||||
|
||||
/// An action to perform on an [Editor]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Action {
|
||||
/// Move cursor to previous character ([Self::Left] in LTR, [Self::Right] in RTL)
|
||||
Previous,
|
||||
/// Move cursor to next character ([Self::Right] in LTR, [Self::Left] in RTL)
|
||||
Next,
|
||||
/// Move cursor left
|
||||
Left,
|
||||
/// Move cursor right
|
||||
Right,
|
||||
/// Move cursor up
|
||||
Up,
|
||||
/// Move cursor down
|
||||
Down,
|
||||
/// Move cursor to start of line
|
||||
Home,
|
||||
/// Move cursor to end of line
|
||||
End,
|
||||
/// Scroll up one page
|
||||
PageUp,
|
||||
/// Scroll down one page
|
||||
PageDown,
|
||||
/// Insert character at cursor
|
||||
Insert(char),
|
||||
/// Create new line
|
||||
Enter,
|
||||
/// Delete text behind cursor
|
||||
Backspace,
|
||||
/// Delete text in front of cursor
|
||||
Delete,
|
||||
/// Mouse click at specified position
|
||||
Click { x: i32, y: i32 },
|
||||
/// Mouse drag to specified position
|
||||
Drag { x: i32, y: i32 },
|
||||
/// Scroll specified number of lines
|
||||
Scroll { lines: i32 },
|
||||
}
|
||||
|
||||
/// A wrapper of [Buffer] for easy editing
|
||||
/// A wrapper of [`Buffer`] for easy editing
|
||||
pub struct Editor<'a> {
|
||||
pub buffer: Buffer<'a>,
|
||||
buffer: Buffer<'a>,
|
||||
cursor: Cursor,
|
||||
cursor_x_opt: Option<i32>,
|
||||
select_opt: Option<Cursor>,
|
||||
|
|
@ -58,7 +19,7 @@ pub struct 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 {
|
||||
Self {
|
||||
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) {
|
||||
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) {
|
||||
Some(some) => some,
|
||||
|
|
@ -102,27 +53,38 @@ impl<'a> Editor<'a> {
|
|||
if self.cursor.line != cursor.line || self.cursor.index != new_index {
|
||||
self.cursor.line = cursor.line;
|
||||
self.cursor.index = new_index;
|
||||
self.buffer.redraw = true;
|
||||
self.buffer.set_redraw(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the internal [Buffer]
|
||||
pub fn buffer(&self) -> &Buffer<'a> {
|
||||
impl<'a> Edit<'a> for Editor<'a> {
|
||||
fn buffer(&self) -> &Buffer<'a> {
|
||||
&self.buffer
|
||||
}
|
||||
|
||||
/// Get the internal [Buffer], mutably
|
||||
pub fn buffer_mut(&mut self) -> &mut Buffer<'a> {
|
||||
fn buffer_mut(&mut self) -> &mut Buffer<'a> {
|
||||
&mut self.buffer
|
||||
}
|
||||
|
||||
/// Get the current cursor position
|
||||
pub fn cursor(&self) -> Cursor {
|
||||
fn cursor(&self) -> Cursor {
|
||||
self.cursor
|
||||
}
|
||||
|
||||
/// Copy selection
|
||||
pub fn copy_selection(&mut self) -> Option<String> {
|
||||
fn select_opt(&self) -> Option<Cursor> {
|
||||
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 (start, end) = match select.line.cmp(&self.cursor.line) {
|
||||
|
|
@ -166,9 +128,7 @@ impl<'a> Editor<'a> {
|
|||
Some(selection)
|
||||
}
|
||||
|
||||
/// Delete selection, adjusting cursor and returning true if there was a selection
|
||||
// Helper function for backspace, delete, insert, and enter when there is a selection
|
||||
pub fn delete_selection(&mut self) -> bool {
|
||||
fn delete_selection(&mut self) -> bool {
|
||||
let select = match self.select_opt.take() {
|
||||
Some(some) => some,
|
||||
None => return false,
|
||||
|
|
@ -235,8 +195,7 @@ impl<'a> Editor<'a> {
|
|||
true
|
||||
}
|
||||
|
||||
/// Perform an [Action] on the editor
|
||||
pub fn action(&mut self, action: Action) {
|
||||
fn action(&mut self, action: Action) {
|
||||
let old_cursor = self.cursor;
|
||||
|
||||
match action {
|
||||
|
|
@ -254,11 +213,11 @@ impl<'a> Editor<'a> {
|
|||
}
|
||||
|
||||
self.cursor.index = prev_index;
|
||||
self.buffer.redraw = true;
|
||||
self.buffer.set_redraw(true);
|
||||
} else if self.cursor.line > 0 {
|
||||
self.cursor.line -= 1;
|
||||
self.cursor.index = self.buffer.lines[self.cursor.line].text().len();
|
||||
self.buffer.redraw = true;
|
||||
self.buffer.set_redraw(true);
|
||||
}
|
||||
self.cursor_x_opt = None;
|
||||
},
|
||||
|
|
@ -268,14 +227,14 @@ impl<'a> Editor<'a> {
|
|||
for (i, c) in line.text().grapheme_indices(true) {
|
||||
if i == self.cursor.index {
|
||||
self.cursor.index += c.len();
|
||||
self.buffer.redraw = true;
|
||||
self.buffer.set_redraw(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if self.cursor.line + 1 < self.buffer.lines.len() {
|
||||
self.cursor.line += 1;
|
||||
self.cursor.index = 0;
|
||||
self.buffer.redraw = true;
|
||||
self.buffer.set_redraw(true);
|
||||
}
|
||||
self.cursor_x_opt = None;
|
||||
},
|
||||
|
|
@ -326,7 +285,7 @@ impl<'a> Editor<'a> {
|
|||
//TODO: make this preserve X as best as possible!
|
||||
let mut cursor = self.buffer.layout_cursor(&self.cursor);
|
||||
|
||||
let layout_len = self.buffer.line_layout(cursor.line).unwrap().len();
|
||||
let layout_len = self.buffer.line_layout(cursor.line).expect("layout not found").len();
|
||||
|
||||
if self.cursor_x_opt.is_none() {
|
||||
self.cursor_x_opt = Some(
|
||||
|
|
@ -371,6 +330,11 @@ impl<'a> Editor<'a> {
|
|||
scroll += self.buffer.visible_lines();
|
||||
self.buffer.set_scroll(scroll);
|
||||
},
|
||||
Action::Escape => {
|
||||
if self.select_opt.take().is_some() {
|
||||
self.buffer.set_redraw(true);
|
||||
}
|
||||
},
|
||||
Action::Insert(character) => {
|
||||
if character.is_control()
|
||||
&& !['\t', '\u{92}'].contains(&character)
|
||||
|
|
@ -484,20 +448,20 @@ impl<'a> Editor<'a> {
|
|||
if let Some(new_cursor) = self.buffer.hit(x, y) {
|
||||
if new_cursor != self.cursor {
|
||||
self.cursor = new_cursor;
|
||||
self.buffer.redraw = true;
|
||||
self.buffer.set_redraw(true);
|
||||
}
|
||||
}
|
||||
},
|
||||
Action::Drag { x, y } => {
|
||||
if self.select_opt.is_none() {
|
||||
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 new_cursor != self.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
|
||||
#[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)
|
||||
{
|
||||
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| {
|
||||
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,
|
||||
Color,
|
||||
Cursor,
|
||||
Edit,
|
||||
Editor,
|
||||
Style,
|
||||
Weight,
|
||||
|
|
@ -41,7 +42,7 @@ pub struct SyntaxSystem {
|
|||
}
|
||||
|
||||
impl SyntaxSystem {
|
||||
/// Create a new [SyntaxSystem]
|
||||
/// Create a new [`SyntaxSystem`]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
//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> {
|
||||
editor: Editor<'a>,
|
||||
syntax_system: &'a SyntaxSystem,
|
||||
|
|
@ -62,7 +63,7 @@ pub struct 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".
|
||||
///
|
||||
|
|
@ -84,12 +85,16 @@ impl<'a> SyntaxEditor<'a> {
|
|||
}
|
||||
|
||||
/// 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")]
|
||||
pub fn load_text<P: AsRef<Path>>(&mut self, path: P, attrs: crate::Attrs<'a>) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
|
||||
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
|
||||
self.syntax = match self.syntax_system.syntax_set.find_syntax_for_file(path) {
|
||||
|
|
@ -110,14 +115,61 @@ impl<'a> SyntaxEditor<'a> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Shape as needed, also doing syntax highlighting
|
||||
pub fn shape_as_needed(&mut self) {
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")]
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
let buffer = self.editor.buffer_mut();
|
||||
|
||||
let mut highlighted = 0;
|
||||
for line_i in 0..self.editor.buffer.lines.len() {
|
||||
let line = &mut self.editor.buffer.lines[line_i];
|
||||
for line_i in 0..buffer.lines.len() {
|
||||
let line = &mut buffer.lines[line_i];
|
||||
if ! line.is_reset() && line_i < self.syntax_cache.len() {
|
||||
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(
|
||||
&mut highlight_state,
|
||||
&ops,
|
||||
|
|
@ -172,14 +224,14 @@ impl<'a> SyntaxEditor<'a> {
|
|||
line.set_wrap_simple(true);
|
||||
|
||||
//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());
|
||||
if line_i < self.syntax_cache.len() {
|
||||
if self.syntax_cache[line_i] != cache_item {
|
||||
self.syntax_cache[line_i] = cache_item;
|
||||
if line_i + 1 < self.editor.buffer.lines.len() {
|
||||
self.editor.buffer.lines[line_i + 1].reset();
|
||||
if line_i + 1 < buffer.lines.len() {
|
||||
buffer.lines[line_i + 1].reset();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -188,7 +240,7 @@ impl<'a> SyntaxEditor<'a> {
|
|||
}
|
||||
|
||||
if highlighted > 0 {
|
||||
self.editor.buffer.redraw = true;
|
||||
buffer.set_redraw(true);
|
||||
#[cfg(feature = "std")]
|
||||
log::debug!("Syntax highlighted {} lines in {:?}", highlighted, now.elapsed());
|
||||
}
|
||||
|
|
@ -196,67 +248,21 @@ impl<'a> SyntaxEditor<'a> {
|
|||
self.editor.shape_as_needed();
|
||||
}
|
||||
|
||||
/// Get the internal [Buffer]
|
||||
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> {
|
||||
fn copy_selection(&mut self) -> Option<String> {
|
||||
self.editor.copy_selection()
|
||||
}
|
||||
|
||||
/// Delete selection, adjusting cursor and returning true if there was a selection
|
||||
pub fn delete_selection(&mut self) -> bool {
|
||||
fn delete_selection(&mut self) -> bool {
|
||||
self.editor.delete_selection()
|
||||
}
|
||||
|
||||
/// Perform an [Action] on the editor
|
||||
pub fn action(&mut self, action: Action) {
|
||||
fn action(&mut self, action: Action) {
|
||||
self.editor.action(action);
|
||||
}
|
||||
|
||||
/// Draw the editor
|
||||
#[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)
|
||||
{
|
||||
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
|
||||
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: pull more data from about:config font.name-list.sans-serif in Firefox
|
||||
match script {
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ impl<'a> Iterator for FontFallbackIter<'a> {
|
|||
while self.script_i.0 < self.scripts.len() {
|
||||
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() {
|
||||
let script_family = script_families[self.script_i.1];
|
||||
self.script_i.1 += 1;
|
||||
|
|
@ -133,7 +133,7 @@ impl<'a> Iterator for FontFallbackIter<'a> {
|
|||
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?
|
||||
|
|
|
|||
|
|
@ -13,6 +13,6 @@ pub fn forbidden_fallback() -> &'static [&'static str] {
|
|||
}
|
||||
|
||||
// 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
|
||||
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)
|
||||
match script {
|
||||
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
|
||||
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
|
||||
match script {
|
||||
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
|
||||
|
||||
pub(crate) mod fallback;
|
||||
use core::ops::Deref;
|
||||
|
||||
pub(crate) use self::font::*;
|
||||
mod font;
|
||||
pub(crate) mod fallback;
|
||||
|
||||
pub use self::matches::*;
|
||||
mod matches;
|
||||
|
||||
pub use self::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"))]
|
||||
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::*;
|
||||
#[cfg(feature = "std")]
|
||||
#[cfg(all(feature = "std", not(target_os = "redox")))]
|
||||
mod std;
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ impl FontSystem {
|
|||
&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) {
|
||||
|
|
@ -60,9 +62,8 @@ impl FontSystem {
|
|||
continue;
|
||||
}
|
||||
|
||||
match self.get_font(face.id) {
|
||||
Some(font) => fonts.push(font),
|
||||
None => (),
|
||||
if let Some(font) = self.get_font(face.id) {
|
||||
fonts.push(font);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
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);
|
||||
|
||||
impl FontSystem {
|
||||
/// Create a new [`FontSystem`], that allows access to any installed system fonts
|
||||
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");
|
||||
|
|
@ -51,8 +52,7 @@ impl FontSystem {
|
|||
let now = std::time::Instant::now();
|
||||
|
||||
//TODO only do this on demand!
|
||||
assert_eq!(db.len(), db.faces().len());
|
||||
for i in 0..db.len() {
|
||||
for i in 0..db.faces().len() {
|
||||
let id = db.faces()[i].id;
|
||||
unsafe { db.make_shared_face_data(id); }
|
||||
}
|
||||
|
|
@ -80,6 +80,8 @@ impl FontSystem {
|
|||
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>>> {
|
||||
self.0.with(|fields| {
|
||||
get_font(&fields, id)
|
||||
|
|
@ -88,7 +90,7 @@ impl FontSystem {
|
|||
|
||||
pub fn get_font_matches<'a>(&'a self, attrs: Attrs) -> Arc<FontMatches<'a>> {
|
||||
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
|
||||
font_matches_cache.entry(AttrsOwned::new(attrs)).or_insert_with(|| {
|
||||
let now = std::time::Instant::now();
|
||||
|
|
@ -99,9 +101,8 @@ impl FontSystem {
|
|||
continue;
|
||||
}
|
||||
|
||||
match get_font(&fields, face.id) {
|
||||
Some(font) => fonts.push(font),
|
||||
None => (),
|
||||
if let Some(font) = get_font(&fields, face.id) {
|
||||
fonts.push(font);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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>>> {
|
||||
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)?;
|
||||
match Font::new(face) {
|
||||
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
|
||||
//! 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
|
||||
//! [Buffer], provide it with some text, and then inspect the layout it produces. At this
|
||||
//! 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
|
||||
//! 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)]
|
||||
|
||||
extern crate alloc;
|
||||
|
|
@ -70,8 +105,8 @@ mod buffer_line;
|
|||
pub use self::cache::*;
|
||||
mod cache;
|
||||
|
||||
pub use self::editor::*;
|
||||
mod editor;
|
||||
pub use self::edit::*;
|
||||
mod edit;
|
||||
|
||||
pub use self::font::*;
|
||||
mod font;
|
||||
|
|
@ -86,8 +121,3 @@ mod shape;
|
|||
pub use self::swash::*;
|
||||
#[cfg(feature = "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.guess_segment_properties();
|
||||
|
||||
let rtl = match buffer.direction() {
|
||||
rustybuzz::Direction::RightToLeft => true,
|
||||
//TODO: other directions?
|
||||
_ => false,
|
||||
};
|
||||
let rtl = matches!(buffer.direction(), rustybuzz::Direction::RightToLeft);
|
||||
assert_eq!(rtl, span_rtl);
|
||||
|
||||
let glyph_buffer = rustybuzz::shape(&font.rustybuzz, &[], buffer);
|
||||
|
|
@ -64,7 +60,7 @@ fn shape_fallback(
|
|||
x_offset,
|
||||
y_offset,
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
|
@ -96,7 +92,7 @@ fn shape_fallback(
|
|||
|
||||
// Set color
|
||||
//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);
|
||||
glyph.color_opt = attrs.color_opt;
|
||||
}
|
||||
|
|
@ -145,7 +141,7 @@ fn shape_run<'a>(
|
|||
);
|
||||
|
||||
let (mut glyphs, mut missing) = shape_fallback(
|
||||
font_iter.next().unwrap(),
|
||||
font_iter.next().expect("no default font found"),
|
||||
line,
|
||||
attrs_list,
|
||||
start_run,
|
||||
|
|
@ -401,7 +397,7 @@ impl ShapeSpan {
|
|||
|
||||
// Reverse glyphs in RTL lines
|
||||
if line_rtl {
|
||||
for word in words.iter_mut() {
|
||||
for word in &mut words {
|
||||
word.glyphs.reverse();
|
||||
}
|
||||
}
|
||||
|
|
@ -493,7 +489,7 @@ impl ShapeLine {
|
|||
let end_x = if self.rtl { 0.0 } else { line_width as f32 };
|
||||
let mut x = start_x;
|
||||
let mut y = 0.0;
|
||||
for span in self.spans.iter() {
|
||||
for span in &self.spans {
|
||||
//TODO: improve performance!
|
||||
let mut word_ranges = Vec::new();
|
||||
if wrap_simple {
|
||||
|
|
@ -505,7 +501,7 @@ impl ShapeLine {
|
|||
let word = &span.words[i];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -553,7 +549,7 @@ impl ShapeLine {
|
|||
let word = &span.words[i];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -583,7 +579,7 @@ impl ShapeLine {
|
|||
for (range, wrap) in word_ranges {
|
||||
for word in span.words[range].iter() {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -605,7 +601,7 @@ impl ShapeLine {
|
|||
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 y_advance = font_size as f32 * glyph.y_advance;
|
||||
|
||||
|
|
@ -630,7 +626,7 @@ impl ShapeLine {
|
|||
}
|
||||
|
||||
if self.rtl {
|
||||
x -= x_advance
|
||||
x -= x_advance;
|
||||
}
|
||||
|
||||
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};
|
||||
|
||||
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) {
|
||||
Some(some) => some,
|
||||
None => {
|
||||
|
|
@ -61,7 +61,7 @@ impl<'a> SwashCache<'a> {
|
|||
/// Create a new swash cache
|
||||
pub fn new(font_system: &'a FontSystem) -> Self {
|
||||
Self {
|
||||
font_system: font_system,
|
||||
font_system,
|
||||
context: ScaleContext::new(),
|
||||
image_cache: Map::new()
|
||||
}
|
||||
|
|
@ -101,7 +101,7 @@ impl<'a> SwashCache<'a> {
|
|||
y + off_y,
|
||||
Color(
|
||||
((image.data[i] as u32) << 24) |
|
||||
base.0 & 0xFFFFFF
|
||||
base.0 & 0xFF_FF_FF
|
||||
)
|
||||
);
|
||||
i += 1;
|
||||
|
|
|
|||
16
test.sh
16
test.sh
|
|
@ -2,13 +2,17 @@
|
|||
|
||||
set -ex
|
||||
|
||||
echo Run CI script
|
||||
./ci.sh
|
||||
|
||||
echo Build documentation
|
||||
cargo doc
|
||||
cargo test
|
||||
cargo build --release --no-default-features
|
||||
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
|
||||
|
||||
echo Build all examples
|
||||
cargo build --release --all
|
||||
|
||||
echo Run terminal example
|
||||
target/release/terminal
|
||||
|
||||
echo Run editor-test example
|
||||
env RUST_LOG=editor_test=info target/release/editor-test
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue