Implement line numbers

This commit is contained in:
Jeremy Soller 2023-11-30 14:24:58 -07:00
parent 4a3dd101f3
commit 2494a7330c
No known key found for this signature in database
GPG key ID: DCFCA852D3906975
6 changed files with 223 additions and 53 deletions

View file

@ -150,6 +150,7 @@ pub struct Config {
pub auto_indent: bool,
pub font_name: String,
pub font_size: u16,
pub line_numbers: bool,
pub syntax_theme_dark: String,
pub syntax_theme_light: String,
pub tab_width: u16,
@ -165,6 +166,7 @@ impl Default for Config {
auto_indent: true,
font_name: "Fira Mono".to_string(),
font_size: 14,
line_numbers: true,
syntax_theme_dark: "gruvbox-dark".to_string(),
syntax_theme_light: "gruvbox-light".to_string(),
tab_width: 4,

49
src/line_number.rs Normal file
View file

@ -0,0 +1,49 @@
use cosmic_text::{
Align, Attrs, AttrsList, BufferLine, Family, FontSystem, LayoutLine, ShapeBuffer, Shaping, Wrap,
};
use std::collections::HashMap;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct LineNumberKey {
pub number: usize,
pub width: usize,
}
#[derive(Debug)]
pub struct LineNumberCache {
cache: HashMap<LineNumberKey, Vec<LayoutLine>>,
scratch: ShapeBuffer,
}
impl LineNumberCache {
pub fn new() -> Self {
Self {
cache: HashMap::new(),
scratch: ShapeBuffer::default(),
}
}
pub fn clear(&mut self) {
self.cache.clear();
}
pub fn get(&mut self, font_system: &mut FontSystem, key: LineNumberKey) -> &Vec<LayoutLine> {
self.cache.entry(key).or_insert_with(|| {
//TODO: do not repeat, used in App::init
let attrs = Attrs::new().family(Family::Monospace);
let text = format!("{:width$}", key.number, width = key.width);
let mut buffer_line = BufferLine::new(text, AttrsList::new(attrs), Shaping::Advanced);
buffer_line.set_wrap(Wrap::None);
buffer_line.set_align(Some(Align::Left));
buffer_line
.layout_in_buffer(
&mut self.scratch,
font_system,
1.0, /* font size adjusted later */
1000.0, /* dummy width */
Wrap::None,
)
.to_vec()
})
}
}

View file

@ -32,6 +32,9 @@ mod config;
use icon_cache::IconCache;
mod icon_cache;
use line_number::LineNumberCache;
mod line_number;
mod localize;
pub use self::mime_icon::{mime_icon, FALLBACK_MIME_ICON};
@ -56,6 +59,7 @@ mod text_box;
lazy_static::lazy_static! {
static ref FONT_SYSTEM: Mutex<FontSystem> = Mutex::new(FontSystem::new());
static ref ICON_CACHE: Mutex<IconCache> = Mutex::new(IconCache::new());
static ref LINE_NUMBER_CACHE: Mutex<LineNumberCache> = Mutex::new(LineNumberCache::new());
static ref SWASH_CACHE: Mutex<SwashCache> = Mutex::new(SwashCache::new());
static ref SYNTAX_SYSTEM: SyntaxSystem = {
let lazy_theme_set = two_face::theme::LazyThemeSet::from(two_face::theme::extra());
@ -190,6 +194,7 @@ pub enum Message {
Todo,
ToggleAutoIndent,
ToggleContextPage(ContextPage),
ToggleLineNumbers,
ToggleWordWrap,
Undo,
VimBindings(bool),
@ -734,6 +739,12 @@ impl Application for App {
font_system.db_mut().set_monospace_family(font_name);
}
// Reset line number cache
{
let mut line_number_cache = LINE_NUMBER_CACHE.lock().unwrap();
line_number_cache.clear();
}
// This does a complete reset of shaping data!
let entities: Vec<_> = self.tab_model.iter().collect();
for entity in entities {
@ -1130,6 +1141,20 @@ impl Application for App {
}
}
}
Message::ToggleLineNumbers => {
self.config.line_numbers = !self.config.line_numbers;
// This forces a redraw of all buffers
let entities: Vec<_> = self.tab_model.iter().collect();
for entity in entities {
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
let mut editor = tab.editor.lock().unwrap();
editor.buffer_mut().set_redraw(true);
}
}
return self.save_config();
}
Message::ToggleWordWrap => {
self.config.word_wrap = !self.config.word_wrap;
return self.save_config();
@ -1424,11 +1449,14 @@ impl Application for App {
}
}
};
let text_box = text_box(&tab.editor, self.config.metrics())
let mut text_box = text_box(&tab.editor, self.config.metrics())
.on_changed(Message::TabChanged(tab_id))
.on_context_menu(move |position_opt| {
Message::TabContextMenu(tab_id, position_opt)
});
if self.config.line_numbers {
text_box = text_box.line_numbers();
}
let tab_element: Element<'_, Message> = match tab.context_menu {
Some(position) => widget::popover(
text_box.context_menu(position),

View file

@ -230,7 +230,11 @@ pub fn menu_bar<'a>(config: &Config) -> Element<'a, Message> {
),
MenuTree::new(horizontal_rule(1)),
menu_checkbox(fl!("word-wrap"), config.word_wrap, Message::ToggleWordWrap),
menu_checkbox(fl!("show-line-numbers"), false, Message::Todo),
menu_checkbox(
fl!("show-line-numbers"),
config.line_numbers,
Message::ToggleLineNumbers,
),
menu_checkbox(fl!("highlight-current-line"), false, Message::Todo),
menu_item(fl!("syntax-highlighting"), Message::Todo),
MenuTree::new(horizontal_rule(1)),

View file

@ -20,24 +20,21 @@ use cosmic::{
use cosmic_text::{Action, Edit, Metrics, ViEditor};
use std::{cell::Cell, cmp, sync::Mutex, time::Instant};
use crate::{FONT_SYSTEM, SWASH_CACHE};
use crate::{line_number::LineNumberKey, FONT_SYSTEM, LINE_NUMBER_CACHE, SWASH_CACHE};
pub struct Appearance {
pub background_color: Option<Color>,
pub text_color: Color,
}
impl Appearance {
pub fn dark() -> Self {
Self {
background_color: Some(Color::from_rgb8(0x34, 0x34, 0x34)),
text_color: Color::from_rgb8(0xFF, 0xFF, 0xFF),
}
}
pub fn light() -> Self {
Self {
background_color: Some(Color::from_rgb8(0xFC, 0xFC, 0xFC)),
text_color: Color::from_rgb8(0x00, 0x00, 0x00),
}
}
@ -64,6 +61,7 @@ pub struct TextBox<'a, Message> {
on_changed: Option<Message>,
context_menu: Option<Point>,
on_context_menu: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
line_numbers: bool,
}
impl<'a, Message> TextBox<'a, Message>
@ -78,6 +76,7 @@ where
on_changed: None,
context_menu: None,
on_context_menu: None,
line_numbers: false,
}
}
@ -103,6 +102,11 @@ where
self.on_context_menu = Some(Box::new(on_context_menu));
self
}
pub fn line_numbers(mut self) -> Self {
self.line_numbers = true;
self
}
}
pub fn text_box<'a, Message>(
@ -124,8 +128,15 @@ fn draw_rect(
start_y: i32,
w: i32,
h: i32,
color: u32,
cosmic_color: cosmic_text::Color,
) {
// Grab alpha channel and green channel
let mut color = cosmic_color.0 & 0xFF00FF00;
// Shift red channel
color |= (cosmic_color.0 & 0x00FF0000) >> 16;
// Shift blue channel
color |= (cosmic_color.0 & 0x000000FF) << 16;
let alpha = (color >> 24) & 0xFF;
if alpha == 0 {
// Do not draw if alpha is zero
@ -273,18 +284,6 @@ where
let appearance = theme.appearance();
if let Some(background_color) = appearance.background_color {
renderer.fill_quad(
renderer::Quad {
bounds: layout.bounds(),
border_radius: 0.0.into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
background_color,
);
}
let text_color = cosmic_text::Color::rgba(
cmp::max(0, cmp::min(255, (appearance.text_color.r * 255.0) as i32)) as u8,
cmp::max(0, cmp::min(255, (appearance.text_color.g * 255.0) as i32)) as u8,
@ -316,23 +315,25 @@ where
let image_w = image_w - scrollbar_w;
let mut font_system = FONT_SYSTEM.lock().unwrap();
let mut editor = editor.borrow_with(&mut font_system);
// Set metrics and size
editor.buffer_mut().set_metrics_and_size(
&mut font_system,
self.metrics.scale(scale_factor),
image_w as f32,
image_h as f32,
);
// Shape and layout as needed
editor.shape_as_needed();
editor.shape_as_needed(&mut font_system);
let mut handle_opt = state.handle_opt.lock().unwrap();
if editor.buffer().redraw() || handle_opt.is_none() {
// Draw to pixel buffer
let mut pixels = vec![0; image_w as usize * image_h as usize * 4];
{
let mut swash_cache = SWASH_CACHE.lock().unwrap();
let buffer = unsafe {
std::slice::from_raw_parts_mut(
pixels.as_mut_ptr() as *mut u32,
@ -340,25 +341,111 @@ where
)
};
let (gutter, gutter_foreground) = {
let convert_color = |color: syntect::highlighting::Color| {
cosmic_text::Color::rgba(color.r, color.g, color.b, color.a)
};
let syntax_theme = editor.theme();
let gutter = syntax_theme
.settings
.gutter
.map_or(editor.background_color(), convert_color);
let gutter_foreground = syntax_theme
.settings
.gutter_foreground
.map_or(editor.foreground_color(), convert_color);
(gutter, gutter_foreground)
};
let mut line_number_width = 0.0;
if self.line_numbers {
// Ensure fill with gutter color
//TODO: optimize to only fill gutter
draw_rect(buffer, image_w, image_h, 0, 0, image_w, image_h, gutter);
// Calculate number of characters needed in line number
let mut line_number_chars = 1;
{
let mut line_count = editor.buffer().lines.len();
while line_count >= 10 {
line_count /= 10;
line_number_chars += 1;
}
}
// Draw line numbers
//TODO: move to cosmic-text?
{
let mut line_number_cache = LINE_NUMBER_CACHE.lock().unwrap();
for run in editor.buffer().layout_runs() {
let line_number = run.line_i.saturating_add(1);
let line_top = run.line_top;
for layout_line in line_number_cache.get(
&mut font_system,
LineNumberKey {
number: line_number,
width: line_number_chars,
},
) {
// These values must be scaled since layout is done at font size 1.0
let max_ascent = layout_line.max_ascent * self.metrics.font_size;
let max_descent = layout_line.max_descent * self.metrics.font_size;
let line_width = layout_line.w * self.metrics.font_size;
if line_width > line_number_width {
line_number_width = line_width;
}
// This code comes from cosmic_text::LayoutRunIter
let glyph_height = max_ascent + max_descent;
let centering_offset =
(self.metrics.line_height - glyph_height) / 2.0;
let line_y = line_top + centering_offset + max_ascent;
for layout_glyph in layout_line.glyphs.iter() {
let physical_glyph =
layout_glyph.physical((0., line_y), self.metrics.font_size);
swash_cache.with_pixels(
&mut font_system,
physical_glyph.cache_key,
gutter_foreground,
|x, y, color| {
draw_rect(
buffer,
image_w,
image_h,
physical_glyph.x + x,
physical_glyph.y + y,
1,
1,
color,
);
},
);
}
}
}
}
}
// Draw editor
let editor_offset_x = (line_number_width + 8.0).ceil() as i32;
editor.draw(
&mut SWASH_CACHE.lock().unwrap(),
&mut font_system,
&mut swash_cache,
text_color,
|x, y, w, h, color| {
// Grab alpha channel and green channel
let mut image_color = color.0 & 0xFF00FF00;
// Shift red channel
image_color |= (color.0 & 0x00FF0000) >> 16;
// Shift blue channel
image_color |= (color.0 & 0x000000FF) << 16;
draw_rect(
buffer,
image_w,
image_h,
x,
editor_offset_x + x,
y,
w as i32,
h as i32,
image_color,
color,
);
},
);