Add Renderer trait for more flexible rendering of buffers and editors

This commit is contained in:
Jeremy Soller 2025-11-06 11:23:11 -07:00
parent 8a7bc790e5
commit 9339446cfa
No known key found for this signature in database
GPG key ID: 670FDFB5428E05CA
7 changed files with 135 additions and 79 deletions

View file

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- Add `Renderer` trait for more flexible rendering of buffers and editors
## [0.15.0] - 2025-10-30 ## [0.15.0] - 2025-10-30
### Added ### Added

View file

@ -9,13 +9,10 @@ use core::{cmp, fmt};
use core_maths::CoreFloat; use core_maths::CoreFloat;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
#[cfg(feature = "swash")]
use crate::Color;
use crate::{ use crate::{
Affinity, Align, Attrs, AttrsList, BidiParagraphs, BorrowedWithFontSystem, BufferLine, Cursor, Affinity, Align, Attrs, AttrsList, BidiParagraphs, BorrowedWithFontSystem, BufferLine, Color,
FontSystem, LayoutCursor, LayoutGlyph, LayoutLine, LineEnding, LineIter, Motion, Scroll, Cursor, FontSystem, LayoutCursor, LayoutGlyph, LayoutLine, LineEnding, LineIter, Motion,
ShapeLine, Shaping, Wrap, Renderer, Scroll, ShapeLine, Shaping, Wrap,
}; };
/// A line of visible text for rendering /// A line of visible text for rendering
@ -1350,29 +1347,24 @@ impl Buffer {
font_system: &mut FontSystem, font_system: &mut FontSystem,
cache: &mut crate::SwashCache, cache: &mut crate::SwashCache,
color: Color, color: Color,
mut f: F, callback: F,
) where ) where
F: FnMut(i32, i32, u32, u32, Color), F: FnMut(i32, i32, u32, u32, Color),
{ {
let mut renderer = crate::LegacyRenderer {
font_system,
cache,
callback,
};
self.render(&mut renderer, color);
}
pub fn render<R: Renderer>(&self, renderer: &mut R, color: Color) {
for run in self.layout_runs() { for run in self.layout_runs() {
for glyph in run.glyphs { for glyph in run.glyphs {
let physical_glyph = glyph.physical((0., 0.), 1.0); let physical_glyph = glyph.physical((0., run.line_y), 1.0);
let glyph_color = glyph.color_opt.map_or(color, |some| some); let glyph_color = glyph.color_opt.map_or(color, |some| some);
renderer.glyph(physical_glyph, glyph_color);
cache.with_pixels(
font_system,
physical_glyph.cache_key,
glyph_color,
|x, y, color| {
f(
physical_glyph.x + x,
run.line_y as i32 + physical_glyph.y + y,
1,
1,
color,
);
},
);
} }
} }
} }

View file

@ -5,17 +5,13 @@ use alloc::{
string::{String, ToString}, string::{String, ToString},
vec::Vec, vec::Vec,
}; };
use core::cmp;
#[cfg(feature = "swash")]
use std::cmp;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
#[cfg(feature = "swash")]
use crate::Color;
use crate::{ use crate::{
Action, Attrs, AttrsList, BorrowedWithFontSystem, BufferLine, BufferRef, Change, ChangeItem, Action, Attrs, AttrsList, BorrowedWithFontSystem, BufferLine, BufferRef, Change, ChangeItem,
Cursor, Edit, FontSystem, LayoutRun, LineEnding, LineIter, Selection, Shaping, Color, Cursor, Edit, FontSystem, LayoutRun, LineEnding, LineIter, Renderer, Selection, Shaping,
}; };
/// A wrapper of [`Buffer`] for easy editing /// A wrapper of [`Buffer`] for easy editing
@ -115,10 +111,32 @@ impl<'buffer> Editor<'buffer> {
cursor_color: Color, cursor_color: Color,
selection_color: Color, selection_color: Color,
selected_text_color: Color, selected_text_color: Color,
mut f: F, callback: F,
) where ) where
F: FnMut(i32, i32, u32, u32, Color), F: FnMut(i32, i32, u32, u32, Color),
{ {
let mut renderer = crate::LegacyRenderer {
font_system,
cache,
callback,
};
self.render(
&mut renderer,
text_color,
cursor_color,
selection_color,
selected_text_color,
);
}
pub fn render<R: Renderer>(
&self,
renderer: &mut R,
text_color: Color,
cursor_color: Color,
selection_color: Color,
selected_text_color: Color,
) {
let selection_bounds = self.selection_bounds(); let selection_bounds = self.selection_bounds();
self.with_buffer(|buffer| { self.with_buffer(|buffer| {
for run in buffer.layout_runs() { for run in buffer.layout_runs() {
@ -151,7 +169,7 @@ impl<'buffer> Editor<'buffer> {
None => Some((c_x as i32, (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() { } else if let Some((min, max)) = range_opt.take() {
f( renderer.rectangle(
min, min,
line_top as i32, line_top as i32,
cmp::max(0, max - min) as u32, cmp::max(0, max - min) as u32,
@ -177,7 +195,7 @@ impl<'buffer> Editor<'buffer> {
max = buffer.size().0.unwrap_or(0.0) as i32; max = buffer.size().0.unwrap_or(0.0) as i32;
} }
} }
f( renderer.rectangle(
min, min,
line_top as i32, line_top as i32,
cmp::max(0, max - min) as u32, cmp::max(0, max - min) as u32,
@ -190,11 +208,11 @@ impl<'buffer> Editor<'buffer> {
// Draw cursor // Draw cursor
if let Some((x, y)) = cursor_position(&self.cursor, &run) { if let Some((x, y)) = cursor_position(&self.cursor, &run) {
f(x, y, 1, line_height as u32, cursor_color); renderer.rectangle(x, y, 1, line_height as u32, cursor_color);
} }
for glyph in run.glyphs { for glyph in run.glyphs {
let physical_glyph = glyph.physical((0., 0.), 1.0); let physical_glyph = glyph.physical((0., line_y), 1.0);
let mut glyph_color = glyph.color_opt.map_or(text_color, |some| some); let mut glyph_color = glyph.color_opt.map_or(text_color, |some| some);
if text_color != selected_text_color { if text_color != selected_text_color {
@ -209,20 +227,7 @@ impl<'buffer> Editor<'buffer> {
} }
} }
cache.with_pixels( renderer.glyph(physical_glyph, glyph_color);
font_system,
physical_glyph.cache_key,
glyph_color,
|x, y, color| {
f(
physical_glyph.x + x,
line_y as i32 + physical_glyph.y + y,
1,
1,
color,
);
},
);
} }
} }
}); });

View file

@ -9,7 +9,7 @@ use syntect::parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet};
use crate::{ use crate::{
Action, AttrsList, BorrowedWithFontSystem, BufferRef, Change, Color, Cursor, Edit, Editor, Action, AttrsList, BorrowedWithFontSystem, BufferRef, Change, Color, Cursor, Edit, Editor,
FontSystem, Selection, Shaping, Style, Weight, FontSystem, Renderer, Selection, Shaping, Style, Weight,
}; };
pub use syntect::highlighting::Theme as SyntaxTheme; pub use syntect::highlighting::Theme as SyntaxTheme;
@ -219,24 +219,31 @@ impl<'syntax_system, 'buffer> SyntaxEditor<'syntax_system, 'buffer> {
/// Draw the editor /// Draw the editor
#[cfg(feature = "swash")] #[cfg(feature = "swash")]
pub fn draw<F>(&self, font_system: &mut FontSystem, cache: &mut crate::SwashCache, mut f: F) pub fn draw<F>(&self, font_system: &mut FontSystem, cache: &mut crate::SwashCache, callback: F)
where where
F: FnMut(i32, i32, u32, u32, Color), F: FnMut(i32, i32, u32, u32, Color),
{ {
let mut renderer = crate::LegacyRenderer {
font_system,
cache,
callback,
};
self.render(&mut renderer);
}
pub fn render<R: Renderer>(&self, renderer: &mut R) {
let size = self.with_buffer(|buffer| buffer.size()); let size = self.with_buffer(|buffer| buffer.size());
if let Some(width) = size.0 { if let Some(width) = size.0 {
if let Some(height) = size.1 { if let Some(height) = size.1 {
f(0, 0, width as u32, height as u32, self.background_color()); renderer.rectangle(0, 0, width as u32, height as u32, self.background_color());
} }
} }
self.editor.draw( self.editor.render(
font_system, renderer,
cache,
self.foreground_color(), self.foreground_color(),
self.cursor_color(), self.cursor_color(),
self.selection_color(), self.selection_color(),
self.foreground_color(), self.foreground_color(),
f,
); );
} }
} }

View file

@ -1,15 +1,13 @@
use alloc::{collections::BTreeMap, string::String}; use alloc::{collections::BTreeMap, string::String};
#[cfg(feature = "swash")]
use core::cmp; use core::cmp;
use modit::{Event, Key, Parser, TextObject, WordIter}; use modit::{Event, Key, Parser, TextObject, WordIter};
use crate::{ use crate::{
Action, AttrsList, BorrowedWithFontSystem, BufferRef, Change, Color, Cursor, Edit, FontSystem, Action, AttrsList, BorrowedWithFontSystem, BufferRef, Change, Color, Cursor, Edit, FontSystem,
Motion, Selection, SyntaxEditor, SyntaxTheme, Motion, Renderer, Selection, SyntaxEditor, SyntaxTheme,
}; };
pub use modit::{ViMode, ViParser}; pub use modit::{ViMode, ViParser};
#[cfg(feature = "swash")]
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
fn undo_2_action<'buffer, E: Edit<'buffer>>( fn undo_2_action<'buffer, E: Edit<'buffer>>(
@ -305,10 +303,19 @@ impl<'syntax_system, 'buffer> ViEditor<'syntax_system, 'buffer> {
} }
#[cfg(feature = "swash")] #[cfg(feature = "swash")]
pub fn draw<F>(&self, font_system: &mut FontSystem, cache: &mut crate::SwashCache, mut f: F) pub fn draw<F>(&self, font_system: &mut FontSystem, cache: &mut crate::SwashCache, callback: F)
where where
F: FnMut(i32, i32, u32, u32, Color), F: FnMut(i32, i32, u32, u32, Color),
{ {
let mut renderer = crate::LegacyRenderer {
font_system,
cache,
callback,
};
self.render(&mut renderer);
}
pub fn render<R: Renderer>(&self, renderer: &mut R) {
let background_color = self.background_color(); let background_color = self.background_color();
let foreground_color = self.foreground_color(); let foreground_color = self.foreground_color();
let cursor_color = self.cursor_color(); let cursor_color = self.cursor_color();
@ -317,7 +324,7 @@ impl<'syntax_system, 'buffer> ViEditor<'syntax_system, 'buffer> {
let size = buffer.size(); let size = buffer.size();
if let Some(width) = size.0 { if let Some(width) = size.0 {
if let Some(height) = size.1 { if let Some(height) = size.1 {
f(0, 0, width as u32, height as u32, background_color); renderer.rectangle(0, 0, width as u32, height as u32, background_color);
} }
} }
let font_size = buffer.metrics().font_size; let font_size = buffer.metrics().font_size;
@ -388,7 +395,7 @@ impl<'syntax_system, 'buffer> ViEditor<'syntax_system, 'buffer> {
None => Some((c_x as i32, (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() { } else if let Some((min, max)) = range_opt.take() {
f( renderer.rectangle(
min, min,
line_top as i32, line_top as i32,
cmp::max(0, max - min) as u32, cmp::max(0, max - min) as u32,
@ -414,7 +421,7 @@ impl<'syntax_system, 'buffer> ViEditor<'syntax_system, 'buffer> {
max = buffer.size().0.unwrap_or(0.0) as i32; max = buffer.size().0.unwrap_or(0.0) as i32;
} }
} }
f( renderer.rectangle(
min, min,
line_top as i32, line_top as i32,
cmp::max(0, max - min) as u32, cmp::max(0, max - min) as u32,
@ -476,7 +483,7 @@ impl<'syntax_system, 'buffer> ViEditor<'syntax_system, 'buffer> {
if block_cursor { if block_cursor {
let left_x = cmp::min(start_x, end_x); let left_x = cmp::min(start_x, end_x);
let right_x = cmp::max(start_x, end_x); let right_x = cmp::max(start_x, end_x);
f( renderer.rectangle(
left_x, left_x,
line_top as i32, line_top as i32,
(right_x - left_x) as u32, (right_x - left_x) as u32,
@ -484,7 +491,7 @@ impl<'syntax_system, 'buffer> ViEditor<'syntax_system, 'buffer> {
selection_color, selection_color,
); );
} else { } else {
f( renderer.rectangle(
start_x, start_x,
line_top as i32, line_top as i32,
1, 1,
@ -495,27 +502,14 @@ impl<'syntax_system, 'buffer> ViEditor<'syntax_system, 'buffer> {
} }
for glyph in run.glyphs.iter() { for glyph in run.glyphs.iter() {
let physical_glyph = glyph.physical((0., 0.), 1.0); let physical_glyph = glyph.physical((0., line_y), 1.0);
let glyph_color = match glyph.color_opt { let glyph_color = match glyph.color_opt {
Some(some) => some, Some(some) => some,
None => foreground_color, None => foreground_color,
}; };
cache.with_pixels( renderer.glyph(physical_glyph, glyph_color);
font_system,
physical_glyph.cache_key,
glyph_color,
|x, y, color| {
f(
physical_glyph.x + x,
line_y as i32 + physical_glyph.y + y,
1,
1,
color,
);
},
);
} }
} }
}); });

View file

@ -129,6 +129,9 @@ mod layout;
pub use self::line_ending::*; pub use self::line_ending::*;
mod line_ending; mod line_ending;
pub use self::render::*;
mod render;
pub use self::shape::*; pub use self::shape::*;
mod shape; mod shape;

49
src/render.rs Normal file
View file

@ -0,0 +1,49 @@
//! Helpers for rendering buffers and editors
use crate::{Color, PhysicalGlyph};
#[cfg(feature = "swash")]
use crate::{FontSystem, SwashCache};
/// Custom renderer for buffers and editors
pub trait Renderer {
/// Render a rectangle at x, y with size w, h and the provided [`Color`].
fn rectangle(&mut self, x: i32, y: i32, w: u32, h: u32, color: Color);
/// Render a [`PhysicalGlyph`] with the provided [`Color`].
/// For performance, consider using [`SwashCache`].
fn glyph(&mut self, physical_glyph: PhysicalGlyph, color: Color);
}
/// Helper to migrate from old renderer
//TODO: remove in future version
#[cfg(feature = "swash")]
#[derive(Debug)]
pub struct LegacyRenderer<'a, F: FnMut(i32, i32, u32, u32, Color)> {
pub font_system: &'a mut FontSystem,
pub cache: &'a mut SwashCache,
pub callback: F,
}
#[cfg(feature = "swash")]
impl<'a, F: FnMut(i32, i32, u32, u32, Color)> Renderer for LegacyRenderer<'a, F> {
fn rectangle(&mut self, x: i32, y: i32, w: u32, h: u32, color: Color) {
(self.callback)(x, y, w, h, color);
}
fn glyph(&mut self, physical_glyph: PhysicalGlyph, color: Color) {
self.cache.with_pixels(
self.font_system,
physical_glyph.cache_key,
color,
|x, y, pixel_color| {
(self.callback)(
physical_glyph.x + x,
physical_glyph.y + y,
1,
1,
pixel_color,
);
},
);
}
}