From 2494a7330c129cdcb04b8c76f04c7ec11d6cb179 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 30 Nov 2023 14:24:58 -0700 Subject: [PATCH] Implement line numbers --- Cargo.lock | 46 +++++++-------- src/config.rs | 2 + src/line_number.rs | 49 ++++++++++++++++ src/main.rs | 30 +++++++++- src/menu.rs | 6 +- src/text_box.rs | 143 ++++++++++++++++++++++++++++++++++++--------- 6 files changed, 223 insertions(+), 53 deletions(-) create mode 100644 src/line_number.rs diff --git a/Cargo.lock b/Cargo.lock index 4a0708f..287132c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -801,7 +801,7 @@ dependencies = [ "bitflags 1.3.2", "block", "cocoa-foundation", - "core-foundation 0.9.3", + "core-foundation 0.9.4", "core-graphics 0.22.3", "foreign-types 0.3.2", "libc", @@ -817,7 +817,7 @@ dependencies = [ "bitflags 1.3.2", "block", "cocoa-foundation", - "core-foundation 0.9.3", + "core-foundation 0.9.4", "core-graphics 0.23.1", "foreign-types 0.5.0", "libc", @@ -832,7 +832,7 @@ checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" dependencies = [ "bitflags 1.3.2", "block", - "core-foundation 0.9.3", + "core-foundation 0.9.4", "core-graphics-types", "libc", "objc", @@ -881,11 +881,11 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ - "core-foundation-sys 0.8.4", + "core-foundation-sys 0.8.6", "libc", ] @@ -897,9 +897,9 @@ checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "core-graphics" @@ -920,7 +920,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.3", + "core-foundation 0.9.4", "core-graphics-types", "foreign-types 0.3.2", "libc", @@ -933,7 +933,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "970a29baf4110c26fedbc7f82107d42c23f7e88e404c4577ed73fe99ff85a212" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.3", + "core-foundation 0.9.4", "core-graphics-types", "foreign-types 0.5.0", "libc", @@ -946,7 +946,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bb142d41022986c1d8ff29103a1411c8a3dfad3552f87a4f8dc50d61d4f4e33" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.3", + "core-foundation 0.9.4", "libc", ] @@ -1033,7 +1033,7 @@ dependencies = [ [[package]] name = "cosmic-text" version = "0.10.0" -source = "git+https://github.com/pop-os/cosmic-text#daa5a6615c52d352e9c87d30e1ab35b8dd14bd91" +source = "git+https://github.com/pop-os/cosmic-text#9278e7d0c4cd6c88eabe881f431a4b50c39d3fe9" dependencies = [ "cosmic_undo_2", "fontdb 0.16.0", @@ -3124,9 +3124,9 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" [[package]] name = "locale_config" @@ -3399,9 +3399,9 @@ dependencies = [ [[package]] name = "modit" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12ac86b3a7bf5696735981a33eb6c82c9d316c24653f8b24b4811173d824f69" +checksum = "338da7e99141cfac01ae0d0b2ba09cb690ff81887bd2a1ab14b8d88ac43dc9d2" dependencies = [ "log", ] @@ -4672,7 +4672,7 @@ dependencies = [ "bitflags 2.4.1", "errno", "libc", - "linux-raw-sys 0.4.11", + "linux-raw-sys 0.4.12", "windows-sys 0.48.0", ] @@ -6483,7 +6483,7 @@ checksum = "79610794594d5e86be473ef7763f604f2159cbac8c94debd00df8fb41e86c2f8" dependencies = [ "bitflags 1.3.2", "cocoa 0.24.1", - "core-foundation 0.9.3", + "core-foundation 0.9.4", "core-graphics 0.22.3", "core-video-sys", "dispatch", @@ -6515,7 +6515,7 @@ dependencies = [ "android-activity", "bitflags 1.3.2", "cfg_aliases", - "core-foundation 0.9.3", + "core-foundation 0.9.4", "core-graphics 0.22.3", "dispatch", "instant", @@ -6745,18 +6745,18 @@ checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697" [[package]] name = "zerocopy" -version = "0.7.26" +version = "0.7.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0" +checksum = "f43de342578a3a14a9314a2dab1942cbfcbe5686e1f91acdc513058063eafe18" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.26" +version = "0.7.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f" +checksum = "e1012d89e3acb79fad7a799ce96866cfb8098b74638465ea1b1533d35900ca90" dependencies = [ "proc-macro2", "quote", diff --git a/src/config.rs b/src/config.rs index 8492c3d..57708de 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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, diff --git a/src/line_number.rs b/src/line_number.rs new file mode 100644 index 0000000..ccc8a96 --- /dev/null +++ b/src/line_number.rs @@ -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>, + 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 { + 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() + }) + } +} diff --git a/src/main.rs b/src/main.rs index 7b27a5f..a228b74 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 = Mutex::new(FontSystem::new()); static ref ICON_CACHE: Mutex = Mutex::new(IconCache::new()); + static ref LINE_NUMBER_CACHE: Mutex = Mutex::new(LineNumberCache::new()); static ref SWASH_CACHE: Mutex = 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::(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), diff --git a/src/menu.rs b/src/menu.rs index 80818ee..30ab75a 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -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)), diff --git a/src/text_box.rs b/src/text_box.rs index 0ebb250..3973420 100644 --- a/src/text_box.rs +++ b/src/text_box.rs @@ -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, 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, context_menu: Option, on_context_menu: Option) -> 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, ); }, );