diff --git a/egui/src/containers/window.rs b/egui/src/containers/window.rs index 24768017..58efd4a8 100644 --- a/egui/src/containers/window.rs +++ b/egui/src/containers/window.rs @@ -592,7 +592,7 @@ fn paint_frame_interaction( struct TitleBar { title_label: Label, - title_galley: font::Galley, + title_galley: Galley, title_rect: Rect, rect: Rect, } diff --git a/egui/src/paint/command.rs b/egui/src/paint/command.rs index 7696ad5e..561549ec 100644 --- a/egui/src/paint/command.rs +++ b/egui/src/paint/command.rs @@ -1,5 +1,5 @@ use { - super::{font::Galley, fonts::TextStyle, Fonts, Srgba, Triangles}, + super::{fonts::TextStyle, Fonts, Galley, Srgba, Triangles}, crate::{ align::{anchor_rect, Align}, math::{Pos2, Rect}, diff --git a/egui/src/paint/font.rs b/egui/src/paint/font.rs index d636e989..9520ff64 100644 --- a/egui/src/paint/font.rs +++ b/egui/src/paint/font.rs @@ -9,162 +9,11 @@ use { use crate::{ math::{vec2, Vec2}, mutex::Mutex, + paint::{Galley, Line}, }; use super::texture_atlas::TextureAtlas; -#[derive(Clone, Copy, Debug, Default)] -pub struct GalleyCursor { - /// character count in whole galley - pub char_idx: usize, - /// line number - pub line: usize, - /// character count on this line - pub column: usize, -} - -/// A collection of text locked into place. -#[derive(Clone, Debug, Default)] -pub struct Galley { - /// The full text, including any an all `\n`. - pub text: String, - - /// Lines of text, from top to bottom. - /// The number of chars in all lines sum up to text.chars().count(). - /// Note that each paragraph (pieces of text separated with `\n`) - /// can be split up into multiple lines. - pub lines: Vec, - - // Optimization: calculated once and reused. - pub size: Vec2, -} - -/// A typeset piece of text on a single line. -#[derive(Clone, Debug)] -pub struct Line { - /// The start of each character, probably starting at zero. - /// The last element is the end of the last character. - /// This is never empty. - /// Unit: points. - /// - /// `x_offsets.len() + (ends_with_newline as usize) == text.chars().count() + 1` - pub x_offsets: Vec, - - /// Top of the line, offset within the Galley. - /// Unit: points. - pub y_min: f32, - - /// Bottom of the line, offset within the Galley. - /// Unit: points. - pub y_max: f32, - - /// If true, this Line came from a paragraph ending with a `\n`. - /// The `\n` itself is omitted from `x_offsets`. - /// A `\n` in the input text always creates a new `Line` below it, - /// so that text that ends with `\n` has an empty `Line` last. - /// This also implies that the last `Line` in a `Galley` always has `ends_with_newline == false`. - pub ends_with_newline: bool, -} - -impl Galley { - pub fn sanity_check(&self) { - let mut char_count = 0; - for line in &self.lines { - line.sanity_check(); - char_count += line.char_count_including_newline(); - } - assert_eq!(char_count, self.text.chars().count()); - if let Some(last_line) = self.lines.last() { - debug_assert!( - !last_line.ends_with_newline, - "If the text ends with '\\n', there would be an empty Line last.\n\ - Galley: {:#?}", - self - ); - } - } - - /// If given a char index after the first line, the end of the last character is returned instead. - /// Returns a Vec2 rather than a Pos2 as this is an offset into the galley. *shrug* - pub fn char_start_pos(&self, char_idx: usize) -> Vec2 { - let mut char_count = 0; - for line in &self.lines { - let line_char_count = line.char_count_including_newline(); - if char_count <= char_idx && char_idx < char_count + line_char_count { - let line_char_offset = char_idx - char_count; - return vec2(line.x_offsets[line_char_offset], line.y_min); - } - char_count += line_char_count; - } - - if let Some(last) = self.lines.last() { - vec2(last.max_x(), last.y_min) - } else { - // Empty galley - vec2(0.0, 0.0) - } - } - - /// Character offset at the given position within the galley - pub fn char_at(&self, pos: Vec2) -> GalleyCursor { - let mut best_y_dist = f32::INFINITY; - let mut cursor = GalleyCursor::default(); - - let mut char_count = 0; - for (line_nr, line) in self.lines.iter().enumerate() { - let y_dist = (line.y_min - pos.y).abs().min((line.y_max - pos.y).abs()); - if y_dist < best_y_dist { - best_y_dist = y_dist; - let column = line.char_at(pos.x); - cursor = GalleyCursor { - char_idx: char_count + column, - line: line_nr, - column, - } - } - char_count += line.char_count_including_newline(); - } - cursor - } -} - -impl Line { - pub fn sanity_check(&self) { - assert!(!self.x_offsets.is_empty()); - } - - /// Excludes the implicit `\n` after the `Line`, if any. - pub fn char_count_excluding_newline(&self) -> usize { - assert!(!self.x_offsets.is_empty()); - self.x_offsets.len() - 1 - } - - /// Includes the implicit `\n` after the `Line`, if any. - pub fn char_count_including_newline(&self) -> usize { - self.char_count_excluding_newline() + (self.ends_with_newline as usize) - } - - pub fn min_x(&self) -> f32 { - *self.x_offsets.first().unwrap() - } - - pub fn max_x(&self) -> f32 { - *self.x_offsets.last().unwrap() - } - - /// Closest char at the desired x coordinate. - /// Returns something in the range `[0, char_count_excluding_newline()]` - pub fn char_at(&self, desired_x: f32) -> usize { - for (i, char_x_bounds) in self.x_offsets.windows(2).enumerate() { - let char_center_x = 0.5 * (char_x_bounds[0] + char_x_bounds[1]); - if desired_x < char_center_x { - return i; - } - } - self.char_count_excluding_newline() - } -} - // ---------------------------------------------------------------------------- // const REPLACEMENT_CHAR: char = '\u{25A1}'; // □ white square Replaces a missing or unsupported Unicode character. @@ -551,55 +400,3 @@ fn allocate_glyph( uv_rect, }) } - -#[test] -fn test_text_layout() { - let pixels_per_point = 1.0; - let typeface_data = include_bytes!("../../fonts/ProggyClean.ttf"); - let atlas = TextureAtlas::new(512, 16); - let atlas = Arc::new(Mutex::new(atlas)); - let font = Font::new(atlas, typeface_data, 13.0, pixels_per_point); - - let galley = font.layout_multiline("".to_owned(), 1024.0); - assert_eq!(galley.lines.len(), 1); - assert_eq!(galley.lines[0].ends_with_newline, false); - assert_eq!(galley.lines[0].x_offsets, vec![0.0]); - - let galley = font.layout_multiline("\n".to_owned(), 1024.0); - assert_eq!(galley.lines.len(), 2); - assert_eq!(galley.lines[0].ends_with_newline, true); - assert_eq!(galley.lines[1].ends_with_newline, false); - assert_eq!(galley.lines[1].x_offsets, vec![0.0]); - - let galley = font.layout_multiline("\n\n".to_owned(), 1024.0); - assert_eq!(galley.lines.len(), 3); - assert_eq!(galley.lines[0].ends_with_newline, true); - assert_eq!(galley.lines[1].ends_with_newline, true); - assert_eq!(galley.lines[2].ends_with_newline, false); - assert_eq!(galley.lines[2].x_offsets, vec![0.0]); - - let galley = font.layout_multiline(" ".to_owned(), 1024.0); - assert_eq!(galley.lines.len(), 1); - assert_eq!(galley.lines[0].ends_with_newline, false); - - let galley = font.layout_multiline("One line".to_owned(), 1024.0); - assert_eq!(galley.lines.len(), 1); - assert_eq!(galley.lines[0].ends_with_newline, false); - - let galley = font.layout_multiline("First line\n".to_owned(), 1024.0); - assert_eq!(galley.lines.len(), 2); - assert_eq!(galley.lines[0].ends_with_newline, true); - assert_eq!(galley.lines[1].ends_with_newline, false); - assert_eq!(galley.lines[1].x_offsets, vec![0.0]); - - // Test wrapping: - let galley = font.layout_multiline("line wrap".to_owned(), 10.0); - assert_eq!(galley.lines.len(), 2); - assert_eq!(galley.lines[0].ends_with_newline, false); - assert_eq!(galley.lines[1].ends_with_newline, false); - - let galley = font.layout_multiline("line\nwrap".to_owned(), 10.0); - assert_eq!(galley.lines.len(), 2); - assert_eq!(galley.lines[0].ends_with_newline, true); - assert_eq!(galley.lines[1].ends_with_newline, false); -} diff --git a/egui/src/paint/galley.rs b/egui/src/paint/galley.rs new file mode 100644 index 00000000..01c14494 --- /dev/null +++ b/egui/src/paint/galley.rs @@ -0,0 +1,816 @@ +use crate::math::{vec2, NumExt, Vec2}; + +/// Character cursor +#[derive(Clone, Copy, Debug, Default)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct CCursor { + /// Character offset (NOT byte offset!). + pub index: usize, + + /// If this cursors sits right at the border of a wrapped line (NOT `\n`), + /// do we prefer the next line? + /// For instance, consider this text, word wrapped: + /// ``` text + /// Hello_ + /// world! + /// ``` + /// + /// The offset `6` is both the end of the first line + /// and the start of the second line. + /// The `prefer_next_line` selects which. + pub prefer_next_line: bool, +} + +impl CCursor { + pub fn new(index: usize) -> Self { + Self { + index, + prefer_next_line: false, + } + } +} + +/// Two `CCursor`s are considered equal if they refer to the same character boundary, +/// even if one prefers the start of the next line. +impl PartialEq for CCursor { + fn eq(&self, other: &CCursor) -> bool { + self.index == other.index + } +} + +impl std::ops::Add for CCursor { + type Output = CCursor; + fn add(self, rhs: usize) -> Self::Output { + CCursor { + index: self.index.saturating_add(rhs), + prefer_next_line: self.prefer_next_line, + } + } +} + +impl std::ops::Sub for CCursor { + type Output = CCursor; + fn sub(self, rhs: usize) -> Self::Output { + CCursor { + index: self.index.saturating_sub(rhs), + prefer_next_line: self.prefer_next_line, + } + } +} + +/// Line Cursor +#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct LCursor { + /// 0 is first line, and so on. + /// Note that a single paragraph can span multiple lines. + /// (a paragraph is text separated by `\n`). + pub line: usize, + + /// Character based (NOT bytes). + /// It is fine if this points to something beyond the end of the current line. + /// When moving up/down it may again be within the next line. + pub column: usize, +} + +/// Paragraph Cursor +#[derive(Clone, Copy, Debug, Default)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct PCursor { + /// 0 is first paragraph, and so on. + /// Note that a single paragraph can span multiple lines. + /// (a paragraph is text separated by `\n`). + pub paragraph: usize, + + /// Character based (NOT bytes). + /// It is fine if this points to something beyond the end of the current line. + /// When moving up/down it may again be within the next line. + pub offset: usize, + + /// If this cursors sits right at the border of a wrapped line (NOT `\n`), + /// do we prefer the next line? + /// For instance, consider this text, word wrapped: + /// ``` text + /// Hello_ + /// world! + /// ``` + /// + /// The offset `6` is both the end of the first line + /// and the start of the second line. + /// The `prefer_next_line` selects which. + pub prefer_next_line: bool, +} + +/// Two `PCursor`s are considered equal if they refer to the same character boundary, +/// even if one prefers the start of the next line. +impl PartialEq for PCursor { + fn eq(&self, other: &PCursor) -> bool { + self.paragraph == other.paragraph && self.offset == other.offset + } +} + +/// All different types of cursors together. +/// They all point to the same place, but in their own different ways. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct Cursor { + pub ccursor: CCursor, + pub lcursor: LCursor, + pub pcursor: PCursor, +} + +/// A collection of text locked into place. +#[derive(Clone, Debug, Default)] +pub struct Galley { + /// The full text, including any an all `\n`. + pub text: String, + + /// Lines of text, from top to bottom. + /// The number of chars in all lines sum up to text.chars().count(). + /// Note that each paragraph (pieces of text separated with `\n`) + /// can be split up into multiple lines. + pub lines: Vec, + + // Optimization: calculated once and reused. + pub size: Vec2, +} + +// TODO: should this maybe be renamed `Row` to avoid confusion with lines as 'that which is broken by \n'. +/// A typeset piece of text on a single line. +#[derive(Clone, Debug)] +pub struct Line { + /// The start of each character, probably starting at zero. + /// The last element is the end of the last character. + /// This is never empty. + /// Unit: points. + /// + /// `x_offsets.len() + (ends_with_newline as usize) == text.chars().count() + 1` + pub x_offsets: Vec, + + /// Top of the line, offset within the Galley. + /// Unit: points. + pub y_min: f32, + + /// Bottom of the line, offset within the Galley. + /// Unit: points. + pub y_max: f32, + + /// If true, this Line came from a paragraph ending with a `\n`. + /// The `\n` itself is omitted from `x_offsets`. + /// A `\n` in the input text always creates a new `Line` below it, + /// so that text that ends with `\n` has an empty `Line` last. + /// This also implies that the last `Line` in a `Galley` always has `ends_with_newline == false`. + pub ends_with_newline: bool, +} + +impl Line { + pub fn sanity_check(&self) { + assert!(!self.x_offsets.is_empty()); + } + + /// Excludes the implicit `\n` after the `Line`, if any. + pub fn char_count_excluding_newline(&self) -> usize { + assert!(!self.x_offsets.is_empty()); + self.x_offsets.len() - 1 + } + + /// Includes the implicit `\n` after the `Line`, if any. + pub fn char_count_including_newline(&self) -> usize { + self.char_count_excluding_newline() + (self.ends_with_newline as usize) + } + + pub fn min_x(&self) -> f32 { + *self.x_offsets.first().unwrap() + } + + pub fn max_x(&self) -> f32 { + *self.x_offsets.last().unwrap() + } + + /// Closest char at the desired x coordinate. + /// Returns something in the range `[0, char_count_excluding_newline()]`. + pub fn char_at(&self, desired_x: f32) -> usize { + for (i, char_x_bounds) in self.x_offsets.windows(2).enumerate() { + let char_center_x = 0.5 * (char_x_bounds[0] + char_x_bounds[1]); + if desired_x < char_center_x { + return i; + } + } + self.char_count_excluding_newline() + } +} + +impl Galley { + pub fn sanity_check(&self) { + let mut char_count = 0; + for line in &self.lines { + line.sanity_check(); + char_count += line.char_count_including_newline(); + } + assert_eq!(char_count, self.text.chars().count()); + if let Some(last_line) = self.lines.last() { + debug_assert!( + !last_line.ends_with_newline, + "If the text ends with '\\n', there would be an empty Line last.\n\ + Galley: {:#?}", + self + ); + } + } +} + +/// ## Physical positions +impl Galley { + pub fn last_pos(&self) -> Vec2 { + if let Some(last) = self.lines.last() { + vec2(last.max_x(), last.y_min) + } else { + vec2(0.0, 0.0) // Empty galley + } + } + + pub fn pos_from_pcursor(&self, pcursor: PCursor) -> Vec2 { + let mut it = PCursor::default(); + + for line in &self.lines { + if it.paragraph == pcursor.paragraph { + // Right paragraph, but is it the right line in the paragraph? + + if it.offset <= pcursor.offset + && pcursor.offset <= it.offset + line.char_count_excluding_newline() + { + let column = pcursor.offset - it.offset; + let column = column.at_most(line.char_count_excluding_newline()); + + let select_next_line_instead = pcursor.prefer_next_line + && !line.ends_with_newline + && column == line.char_count_excluding_newline(); + if !select_next_line_instead { + return vec2(line.x_offsets[column], line.y_min); + } + } + } + + if line.ends_with_newline { + it.paragraph += 1; + it.offset = 0; + } else { + it.offset += line.char_count_including_newline(); + } + } + + self.last_pos() + } + + pub fn pos_from_cursor(&self, cursor: &Cursor) -> Vec2 { + // self.pos_from_lcursor(cursor.lcursor) + self.pos_from_pcursor(cursor.pcursor) // The one TextEdit stores + } + + /// Cursor at the given position within the galley + pub fn cursor_at(&self, pos: Vec2) -> Cursor { + let mut best_y_dist = f32::INFINITY; + let mut cursor = Cursor::default(); + + let mut ccursor_index = 0; + let mut pcursor_it = PCursor::default(); + + for (line_nr, line) in self.lines.iter().enumerate() { + let y_dist = (line.y_min - pos.y).abs().min((line.y_max - pos.y).abs()); + if y_dist < best_y_dist { + best_y_dist = y_dist; + let column = line.char_at(pos.x); + cursor = Cursor { + ccursor: CCursor { + index: ccursor_index + column, + prefer_next_line: column == 0, + }, + lcursor: LCursor { + line: line_nr, + column, + }, + pcursor: PCursor { + paragraph: pcursor_it.paragraph, + offset: pcursor_it.offset + column, + prefer_next_line: column == 0, + }, + } + } + ccursor_index += line.char_count_including_newline(); + if line.ends_with_newline { + pcursor_it.paragraph += 1; + pcursor_it.offset = 0; + } else { + pcursor_it.offset += line.char_count_including_newline(); + } + } + cursor + } +} + +/// ## Cursor positions +impl Galley { + /// Cursor to one-past last character. + pub fn end(&self) -> Cursor { + if self.lines.is_empty() { + return Default::default(); + } + let mut ccursor = CCursor { + index: 0, + prefer_next_line: true, + }; + let mut pcursor = PCursor { + paragraph: 0, + offset: 0, + prefer_next_line: true, + }; + for line in &self.lines { + let line_char_count = line.char_count_including_newline(); + ccursor.index += line_char_count; + if line.ends_with_newline { + pcursor.paragraph += 1; + pcursor.offset = 0; + } else { + pcursor.offset += line_char_count; + } + } + Cursor { + ccursor, + lcursor: self.end_lcursor(), + pcursor, + } + } + + pub fn end_lcursor(&self) -> LCursor { + if let Some(last_line) = self.lines.last() { + debug_assert!(!last_line.ends_with_newline); + LCursor { + line: self.lines.len() - 1, + column: last_line.char_count_excluding_newline(), + } + } else { + Default::default() + } + } +} + +/// ## Cursor conversions +impl Galley { + // TODO: return identical cursor, or clamp? + pub fn from_ccursor(&self, ccursor: CCursor) -> Cursor { + let prefer_next_line = ccursor.prefer_next_line; + let mut ccursor_it = CCursor { + index: 0, + prefer_next_line, + }; + let mut pcursor_it = PCursor { + paragraph: 0, + offset: 0, + prefer_next_line, + }; + + for (line_nr, line) in self.lines.iter().enumerate() { + let line_char_count = line.char_count_excluding_newline(); + + if ccursor_it.index <= ccursor.index + && ccursor.index <= ccursor_it.index + line_char_count + { + let column = ccursor.index - ccursor_it.index; + + let select_next_line_instead = prefer_next_line + && !line.ends_with_newline + && column == line.char_count_excluding_newline(); + if !select_next_line_instead { + pcursor_it.offset += column; + return Cursor { + ccursor, + lcursor: LCursor { + line: line_nr, + column, + }, + pcursor: pcursor_it, + }; + } + } + ccursor_it.index += line.char_count_including_newline(); + if line.ends_with_newline { + pcursor_it.paragraph += 1; + pcursor_it.offset = 0; + } else { + pcursor_it.offset += line.char_count_including_newline(); + } + } + debug_assert_eq!(ccursor_it, self.end().ccursor); + Cursor { + ccursor: ccursor_it, // clamp + lcursor: self.end_lcursor(), + pcursor: pcursor_it, + } + } + + // TODO: return identical cursor, or clamp? + pub fn from_lcursor(&self, lcursor: LCursor) -> Cursor { + if lcursor.line >= self.lines.len() { + return self.end(); + } + + let prefer_next_line = lcursor.column == 0; + let mut ccursor_it = CCursor { + index: 0, + prefer_next_line, + }; + let mut pcursor_it = PCursor { + paragraph: 0, + offset: 0, + prefer_next_line, + }; + + for (line_nr, line) in self.lines.iter().enumerate() { + if line_nr == lcursor.line { + let column = lcursor.column.at_most(line.char_count_excluding_newline()); + + let select_next_line_instead = prefer_next_line + && !line.ends_with_newline + && column == line.char_count_excluding_newline(); + + if !select_next_line_instead { + ccursor_it.index += column; + pcursor_it.offset += column; + return Cursor { + ccursor: ccursor_it, + lcursor, + pcursor: pcursor_it, + }; + } + } + ccursor_it.index += line.char_count_including_newline(); + if line.ends_with_newline { + pcursor_it.paragraph += 1; + pcursor_it.offset = 0; + } else { + pcursor_it.offset += line.char_count_including_newline(); + } + } + Cursor { + ccursor: ccursor_it, + lcursor: self.end_lcursor(), + pcursor: pcursor_it, + } + } + + // TODO: return identical cursor, or clamp? + pub fn from_pcursor(&self, pcursor: PCursor) -> Cursor { + let prefer_next_line = pcursor.prefer_next_line; + let mut ccursor_it = CCursor { + index: 0, + prefer_next_line, + }; + let mut pcursor_it = PCursor { + paragraph: 0, + offset: 0, + prefer_next_line, + }; + + for (line_nr, line) in self.lines.iter().enumerate() { + if pcursor_it.paragraph == pcursor.paragraph { + // Right paragraph, but is it the right line in the paragraph? + + if pcursor_it.offset <= pcursor.offset + && pcursor.offset <= pcursor_it.offset + line.char_count_excluding_newline() + { + let column = pcursor.offset - pcursor_it.offset; + let column = column.at_most(line.char_count_excluding_newline()); + + let select_next_line_instead = pcursor.prefer_next_line + && !line.ends_with_newline + && column == line.char_count_excluding_newline(); + if !select_next_line_instead { + ccursor_it.index += column; + return Cursor { + ccursor: ccursor_it, + lcursor: LCursor { + line: line_nr, + column, + }, + pcursor, + }; + } + } + } + + ccursor_it.index += line.char_count_including_newline(); + if line.ends_with_newline { + pcursor_it.paragraph += 1; + pcursor_it.offset = 0; + } else { + pcursor_it.offset += line.char_count_including_newline(); + } + } + Cursor { + ccursor: ccursor_it, + lcursor: self.end_lcursor(), + pcursor, + } + } +} + +/// ## Cursor positions +impl Galley { + pub fn cursor_left_one_character(&self, cursor: &Cursor) -> Cursor { + if cursor.ccursor.index == 0 { + Default::default() + } else { + self.from_ccursor(cursor.ccursor - 1) + } + } + + pub fn cursor_right_one_character(&self, cursor: &Cursor) -> Cursor { + self.from_ccursor(cursor.ccursor + 1) + } + + pub fn cursor_up_one_line(&self, cursor: &Cursor) -> Cursor { + if cursor.lcursor.line == 0 { + Cursor::default() + } else { + let x = self.pos_from_cursor(cursor).x; + let line = cursor.lcursor.line - 1; + let column = self.lines[line].char_at(x).max(cursor.lcursor.column); + self.from_lcursor(LCursor { line, column }) + } + } + + pub fn cursor_down_one_line(&self, cursor: &Cursor) -> Cursor { + if cursor.lcursor.line + 1 < self.lines.len() { + let x = self.pos_from_cursor(cursor).x; + let line = cursor.lcursor.line + 1; + let column = self.lines[line].char_at(x).max(cursor.lcursor.column); + self.from_lcursor(LCursor { line, column }) + } else { + self.end() + } + } + + pub fn cursor_begin_of_line(&self, cursor: &Cursor) -> Cursor { + self.from_lcursor(LCursor { + line: cursor.lcursor.line, + column: 0, + }) + } + + pub fn cursor_end_of_line(&self, cursor: &Cursor) -> Cursor { + self.from_lcursor(LCursor { + line: cursor.lcursor.line, + column: self.lines[cursor.lcursor.line].char_count_excluding_newline(), + }) + } +} + +// ---------------------------------------------------------------------------- + +#[test] +fn test_text_layout() { + use crate::mutex::Mutex; + use crate::paint::{font::Font, *}; + + let pixels_per_point = 1.0; + let typeface_data = include_bytes!("../../fonts/ProggyClean.ttf"); + let atlas = TextureAtlas::new(512, 16); + let atlas = std::sync::Arc::new(Mutex::new(atlas)); + let font = Font::new(atlas, typeface_data, 13.0, pixels_per_point); + + let galley = font.layout_multiline("".to_owned(), 1024.0); + assert_eq!(galley.lines.len(), 1); + assert_eq!(galley.lines[0].ends_with_newline, false); + assert_eq!(galley.lines[0].x_offsets, vec![0.0]); + + let galley = font.layout_multiline("\n".to_owned(), 1024.0); + assert_eq!(galley.lines.len(), 2); + assert_eq!(galley.lines[0].ends_with_newline, true); + assert_eq!(galley.lines[1].ends_with_newline, false); + assert_eq!(galley.lines[1].x_offsets, vec![0.0]); + + let galley = font.layout_multiline("\n\n".to_owned(), 1024.0); + assert_eq!(galley.lines.len(), 3); + assert_eq!(galley.lines[0].ends_with_newline, true); + assert_eq!(galley.lines[1].ends_with_newline, true); + assert_eq!(galley.lines[2].ends_with_newline, false); + assert_eq!(galley.lines[2].x_offsets, vec![0.0]); + + let galley = font.layout_multiline(" ".to_owned(), 1024.0); + assert_eq!(galley.lines.len(), 1); + assert_eq!(galley.lines[0].ends_with_newline, false); + + let galley = font.layout_multiline("One line".to_owned(), 1024.0); + assert_eq!(galley.lines.len(), 1); + assert_eq!(galley.lines[0].ends_with_newline, false); + + let galley = font.layout_multiline("First line\n".to_owned(), 1024.0); + assert_eq!(galley.lines.len(), 2); + assert_eq!(galley.lines[0].ends_with_newline, true); + assert_eq!(galley.lines[1].ends_with_newline, false); + assert_eq!(galley.lines[1].x_offsets, vec![0.0]); + + let galley = font.layout_multiline("line\nbreak".to_owned(), 10.0); + assert_eq!(galley.lines.len(), 2); + assert_eq!(galley.lines[0].ends_with_newline, true); + assert_eq!(galley.lines[1].ends_with_newline, false); + + // Test wrapping: + let galley = font.layout_multiline("line wrap".to_owned(), 10.0); + assert_eq!(galley.lines.len(), 2); + assert_eq!(galley.lines[0].ends_with_newline, false); + assert_eq!(galley.lines[1].ends_with_newline, false); + + { + // Test wrapping: + let galley = font.layout_multiline("Line wrap.\nNew paragraph.".to_owned(), 10.0); + assert_eq!(galley.lines.len(), 4); + assert_eq!(galley.lines[0].ends_with_newline, false); + assert_eq!( + galley.lines[0].char_count_excluding_newline(), + "Line ".len() + ); + assert_eq!( + galley.lines[0].char_count_including_newline(), + "Line ".len() + ); + assert_eq!(galley.lines[1].ends_with_newline, true); + assert_eq!( + galley.lines[1].char_count_excluding_newline(), + "wrap.".len() + ); + assert_eq!( + galley.lines[1].char_count_including_newline(), + "wrap.\n".len() + ); + assert_eq!(galley.lines[2].ends_with_newline, false); + assert_eq!(galley.lines[3].ends_with_newline, false); + + let cursor = Cursor::default(); + assert_eq!(cursor, galley.from_ccursor(cursor.ccursor)); + assert_eq!(cursor, galley.from_lcursor(cursor.lcursor)); + assert_eq!(cursor, galley.from_pcursor(cursor.pcursor)); + + let cursor = galley.end(); + assert_eq!(cursor, galley.from_ccursor(cursor.ccursor)); + assert_eq!(cursor, galley.from_lcursor(cursor.lcursor)); + assert_eq!(cursor, galley.from_pcursor(cursor.pcursor)); + assert_eq!( + cursor, + Cursor { + ccursor: CCursor::new(25), + lcursor: LCursor { + line: 3, + column: 10 + }, + pcursor: PCursor { + paragraph: 1, + offset: 14, + prefer_next_line: false, + } + } + ); + + let cursor = galley.from_ccursor(CCursor::new(1)); + assert_eq!(cursor.lcursor, LCursor { line: 0, column: 1 }); + assert_eq!( + cursor.pcursor, + PCursor { + paragraph: 0, + offset: 1, + prefer_next_line: false, + } + ); + assert_eq!(cursor, galley.from_ccursor(cursor.ccursor)); + assert_eq!(cursor, galley.from_lcursor(cursor.lcursor)); + assert_eq!(cursor, galley.from_pcursor(cursor.pcursor)); + + let cursor = galley.from_pcursor(PCursor { + paragraph: 1, + offset: 2, + prefer_next_line: false, + }); + assert_eq!(cursor.lcursor, LCursor { line: 2, column: 2 }); + assert_eq!(cursor, galley.from_ccursor(cursor.ccursor)); + assert_eq!(cursor, galley.from_lcursor(cursor.lcursor)); + assert_eq!(cursor, galley.from_pcursor(cursor.pcursor)); + + let cursor = galley.from_pcursor(PCursor { + paragraph: 1, + offset: 6, + prefer_next_line: false, + }); + assert_eq!(cursor.lcursor, LCursor { line: 3, column: 2 }); + assert_eq!(cursor, galley.from_ccursor(cursor.ccursor)); + assert_eq!(cursor, galley.from_lcursor(cursor.lcursor)); + assert_eq!(cursor, galley.from_pcursor(cursor.pcursor)); + + // On the border between two lines within the same paragraph: + let cursor = galley.from_lcursor(LCursor { line: 0, column: 5 }); + assert_eq!( + cursor, + Cursor { + ccursor: CCursor::new(5), + lcursor: LCursor { line: 0, column: 5 }, + pcursor: PCursor { + paragraph: 0, + offset: 5, + prefer_next_line: false, + } + } + ); + assert_eq!(cursor, galley.from_lcursor(cursor.lcursor)); + + let cursor = galley.from_lcursor(LCursor { line: 1, column: 0 }); + assert_eq!( + cursor, + Cursor { + ccursor: CCursor::new(5), + lcursor: LCursor { line: 1, column: 0 }, + pcursor: PCursor { + paragraph: 0, + offset: 5, + prefer_next_line: false, + } + } + ); + assert_eq!(cursor, galley.from_lcursor(cursor.lcursor)); + } + + { + // Test cursor movement: + let galley = font.layout_multiline("Line wrap.\nNew paragraph.".to_owned(), 10.0); + assert_eq!(galley.lines.len(), 4); + assert_eq!(galley.lines[0].ends_with_newline, false); + assert_eq!(galley.lines[1].ends_with_newline, true); + assert_eq!(galley.lines[2].ends_with_newline, false); + assert_eq!(galley.lines[3].ends_with_newline, false); + + let cursor = Cursor::default(); + + assert_eq!(galley.cursor_up_one_line(&cursor), cursor); + assert_eq!(galley.cursor_begin_of_line(&cursor), cursor); + + assert_eq!( + galley.cursor_end_of_line(&cursor), + Cursor { + ccursor: CCursor::new(5), + lcursor: LCursor { line: 0, column: 5 }, + pcursor: PCursor { + paragraph: 0, + offset: 5, + prefer_next_line: false, + } + } + ); + + assert_eq!( + galley.cursor_down_one_line(&cursor), + Cursor { + ccursor: CCursor::new(5), + lcursor: LCursor { line: 1, column: 0 }, + pcursor: PCursor { + paragraph: 0, + offset: 5, + prefer_next_line: false, + } + } + ); + + let cursor = Cursor::default(); + assert_eq!( + galley.cursor_down_one_line(&galley.cursor_down_one_line(&cursor)), + Cursor { + ccursor: CCursor::new(11), + lcursor: LCursor { line: 2, column: 0 }, + pcursor: PCursor { + paragraph: 1, + offset: 0, + prefer_next_line: false, + } + } + ); + + let cursor = galley.end(); + assert_eq!(galley.cursor_down_one_line(&cursor), cursor); + + let cursor = galley.end(); + assert!(galley.cursor_up_one_line(&galley.end()) != cursor); + + assert_eq!( + galley.cursor_up_one_line(&galley.end()), + Cursor { + ccursor: CCursor::new(15), + lcursor: LCursor { + line: 2, + column: 10 + }, + pcursor: PCursor { + paragraph: 1, + offset: 4, + prefer_next_line: false, + } + } + ); + } +} diff --git a/egui/src/paint/mod.rs b/egui/src/paint/mod.rs index 6c87d7e8..b62aad3d 100644 --- a/egui/src/paint/mod.rs +++ b/egui/src/paint/mod.rs @@ -6,6 +6,7 @@ pub mod color; pub mod command; pub mod font; pub mod fonts; +mod galley; pub mod stats; pub mod tessellator; mod texture_atlas; @@ -14,9 +15,10 @@ pub use { color::{Rgba, Srgba}, command::{PaintCmd, Stroke}, fonts::{FontDefinitions, FontFamily, Fonts, TextStyle}, + galley::*, stats::PaintStats, tessellator::{ PaintJob, PaintJobs, TesselationOptions, TextureId, Triangles, Vertex, WHITE_UV, }, - texture_atlas::Texture, + texture_atlas::{Texture, TextureAtlas}, }; diff --git a/egui/src/paint/stats.rs b/egui/src/paint/stats.rs index f6fdbfdd..c0586d95 100644 --- a/egui/src/paint/stats.rs +++ b/egui/src/paint/stats.rs @@ -66,7 +66,7 @@ impl AllocInfo { } } - pub fn from_galley(galley: &font::Galley) -> Self { + pub fn from_galley(galley: &Galley) -> Self { Self::from_slice(galley.text.as_bytes()) + Self::from_slice(&galley.lines) } diff --git a/egui/src/painter.rs b/egui/src/painter.rs index 2b3232ca..67fec0ef 100644 --- a/egui/src/painter.rs +++ b/egui/src/painter.rs @@ -5,7 +5,7 @@ use crate::{ color, layers::PaintCmdIdx, math::{Pos2, Rect, Vec2}, - paint::{font, Fonts, PaintCmd, Stroke, TextStyle}, + paint::{Fonts, Galley, PaintCmd, Stroke, TextStyle}, Context, LayerId, Srgba, }; @@ -278,7 +278,7 @@ impl Painter { } /// Paint text that has already been layed out in a `Galley`. - pub fn galley(&self, pos: Pos2, galley: font::Galley, text_style: TextStyle, color: Srgba) { + pub fn galley(&self, pos: Pos2, galley: Galley, text_style: TextStyle, color: Srgba) { self.add(PaintCmd::Text { pos, galley, diff --git a/egui/src/widgets/mod.rs b/egui/src/widgets/mod.rs index 0bac8eb2..8438f50f 100644 --- a/egui/src/widgets/mod.rs +++ b/egui/src/widgets/mod.rs @@ -78,7 +78,7 @@ impl Label { self } - pub fn layout(&self, ui: &Ui) -> font::Galley { + pub fn layout(&self, ui: &Ui) -> Galley { let max_width = ui.available().width(); // Prevent word-wrapping after a single letter, and other silly shit: // TODO: general "don't force labels and similar to wrap so early" @@ -86,7 +86,7 @@ impl Label { self.layout_width(ui, max_width) } - pub fn layout_width(&self, ui: &Ui, max_width: f32) -> font::Galley { + pub fn layout_width(&self, ui: &Ui, max_width: f32) -> Galley { let text_style = self.text_style_or_default(ui.style()); let font = &ui.fonts()[text_style]; if self.multiline { @@ -109,7 +109,7 @@ impl Label { // TODO: a paint method for painting anywhere in a ui. // This should be the easiest method of putting text anywhere. - pub fn paint_galley(&self, ui: &mut Ui, pos: Pos2, galley: font::Galley) { + pub fn paint_galley(&self, ui: &mut Ui, pos: Pos2, galley: Galley) { let text_style = self.text_style_or_default(ui.style()); let text_color = self .text_color diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index 0159b0bf..a38e5d4e 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -2,12 +2,26 @@ use crate::{paint::*, *}; #[derive(Clone, Copy, Debug, Default)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(default))] pub(crate) struct State { - /// Character based, NOT bytes. - /// TODO: store as line + row - pub cursor: Option, + /// We store as PCursor (paragraph number, and character offset within that paragraph). + /// This is so what if we resize the `TextEdit` region, and text wrapping changes, + /// we keep the same byte character offset from the beginning of the text, + /// even though the number of lines changes + /// (each paragraph can be several lines, due to word wrapping). + /// The column (character offset) should be able to extend beyond the last word so that we can + /// go down and still end up on the same column when we return. + pcursor: Option, } +// struct PCursorPair { +// /// Where the selection started (e.g. a drag started). +// begin: PCursor, +// /// The end of the selection. When moving with e.g. shift+arrows, this is what moves. +// /// Note that this may be BEFORE the `begin`. +// end: PCursor, +// } + /// A text region that the user can edit the contents of. /// /// Example: @@ -167,7 +181,7 @@ impl<'t> Widget for TextEdit<'t> { if response.clicked && enabled { ui.memory().request_kb_focus(id); if let Some(mouse_pos) = ui.input().mouse.pos { - state.cursor = Some(galley.char_at(mouse_pos - response.rect.min).char_idx); + state.pcursor = Some(galley.cursor_at(mouse_pos - response.rect.min).pcursor); } } else if ui.input().mouse.click || (ui.input().mouse.pressed && !response.hovered) { // User clicked somewhere else @@ -182,19 +196,29 @@ impl<'t> Widget for TextEdit<'t> { } if ui.memory().has_kb_focus(id) && enabled { - let mut cursor = state.cursor.unwrap_or_else(|| text.chars().count()); - cursor = clamp(cursor, 0..=text.chars().count()); + let mut cursor = state + .pcursor + .map(|pcursor| galley.from_pcursor(pcursor)) + .unwrap_or_else(|| galley.end()); for event in &ui.input().events { - match event { + let did_mutate_text = match event { Event::Copy | Event::Cut => { // TODO: cut ui.ctx().output().copied_text = text.clone(); + None } Event::Text(text_to_insert) => { - // newlines are handled by `Key::Enter`. - if text_to_insert != "\n" && text_to_insert != "\r" { - insert_text(&mut cursor, text, text_to_insert); + // Newlines are handled by `Key::Enter`. + if !text_to_insert.is_empty() + && text_to_insert != "\n" + && text_to_insert != "\r" + { + let mut ccursor = cursor.ccursor; + insert_text(&mut ccursor, text, text_to_insert); + Some(ccursor) + } else { + None } } Event::Key { @@ -202,7 +226,9 @@ impl<'t> Widget for TextEdit<'t> { pressed: true, } => { if multiline { - insert_text(&mut cursor, text, "\n"); + let mut ccursor = cursor.ccursor; + insert_text(&mut ccursor, text, "\n"); + Some(ccursor) } else { // Common to end input with enter ui.memory().surrender_kb_focus(id); @@ -217,22 +243,25 @@ impl<'t> Widget for TextEdit<'t> { break; } Event::Key { key, pressed: true } => { - on_key_press(&mut cursor, text, *key); + on_key_press(&mut cursor, text, &galley, *key) } - _ => {} + Event::Key { .. } => None, + }; + + if let Some(new_ccursor) = did_mutate_text { + // Layout again to avoid frame delay, and to keep `text` and `galley` in sync. + let font = &ui.fonts()[text_style]; + galley = if multiline { + font.layout_multiline(text.clone(), available_width) + } else { + font.layout_single_line(text.clone()) + }; + + // Set cursor using new galley: + cursor = galley.from_ccursor(new_ccursor); } } - state.cursor = Some(cursor); - - // layout again to avoid frame delay: - let font = &ui.fonts()[text_style]; - galley = if multiline { - font.layout_multiline(text.clone(), available_width) - } else { - font.layout_single_line(text.clone()) - }; - - // dbg!(&galley); + state.pcursor = Some(cursor.pcursor); } let painter = ui.painter(); @@ -259,8 +288,8 @@ impl<'t> Widget for TextEdit<'t> { }; if show_cursor { - if let Some(cursor) = state.cursor { - let cursor_pos = response.rect.min + galley.char_start_pos(cursor); + if let Some(pcursor) = state.pcursor { + let cursor_pos = response.rect.min + galley.pos_from_pcursor(pcursor); painter.line_segment( [cursor_pos, cursor_pos + vec2(0.0, line_spacing)], (ui.style().visuals.text_cursor_width, color::WHITE), @@ -282,114 +311,79 @@ impl<'t> Widget for TextEdit<'t> { } } -fn insert_text(cursor: &mut usize, text: &mut String, text_to_insert: &str) { - // eprintln!("insert_text {:?}", text_to_insert); - +fn insert_text(ccursor: &mut CCursor, text: &mut String, text_to_insert: &str) { let mut char_it = text.chars(); let mut new_text = String::with_capacity(text.capacity()); - for _ in 0..*cursor { + for _ in 0..ccursor.index { let c = char_it.next().unwrap(); new_text.push(c); } - *cursor += text_to_insert.chars().count(); + ccursor.index += text_to_insert.chars().count(); new_text += text_to_insert; new_text.extend(char_it); *text = new_text; } -fn on_key_press(cursor: &mut usize, text: &mut String, key: Key) { +/// Returns `Some(new_cursor)` if we did mutate `text`. +fn on_key_press( + cursor: &mut Cursor, + text: &mut String, + galley: &Galley, + key: Key, +) -> Option { // eprintln!("on_key_press before: '{}', cursor at {}", text, cursor); match key { - Key::Backspace if *cursor > 0 => { - *cursor -= 1; - + Key::Backspace if cursor.ccursor.index > 0 => { + *cursor = galley.from_ccursor(cursor.ccursor - 1); let mut char_it = text.chars(); let mut new_text = String::with_capacity(text.capacity()); - for _ in 0..*cursor { + for _ in 0..cursor.ccursor.index { new_text.push(char_it.next().unwrap()) } new_text.extend(char_it.skip(1)); *text = new_text; + Some(cursor.ccursor) } Key::Delete => { let mut char_it = text.chars(); let mut new_text = String::with_capacity(text.capacity()); - for _ in 0..*cursor { + for _ in 0..cursor.ccursor.index { new_text.push(char_it.next().unwrap()) } new_text.extend(char_it.skip(1)); *text = new_text; + Some(cursor.ccursor) } - Key::Enter => {} // handled earlier + Key::Enter => unreachable!("Should have been handled earlier"), + Key::Home => { - // To start of paragraph: - let pos = line_col_from_char_idx(text, *cursor); - *cursor = char_idx_from_line_col(text, (pos.0, 0)); + // To start of line: + *cursor = galley.cursor_begin_of_line(cursor); + None } Key::End => { - // To end of paragraph: - let pos = line_col_from_char_idx(text, *cursor); - let line = line_from_number(text, pos.0); - *cursor = char_idx_from_line_col(text, (pos.0, line.chars().count())); + *cursor = galley.cursor_end_of_line(cursor); + None } - Key::Left if *cursor > 0 => { - *cursor -= 1; + Key::Left => { + *cursor = galley.cursor_left_one_character(cursor); + None } Key::Right => { - *cursor = (*cursor + 1).min(text.chars().count()); + *cursor = galley.cursor_right_one_character(cursor); + None } Key::Up => { - let mut pos = line_col_from_char_idx(text, *cursor); - pos.0 = pos.0.saturating_sub(1); - *cursor = char_idx_from_line_col(text, pos); + *cursor = galley.cursor_up_one_line(cursor); + None } Key::Down => { - let mut pos = line_col_from_char_idx(text, *cursor); - pos.0 += 1; - *cursor = char_idx_from_line_col(text, pos); + *cursor = galley.cursor_down_one_line(cursor); + None } - _ => {} + _ => None, } // eprintln!("on_key_press after: '{}', cursor at {}\n", text, cursor); } - -fn line_col_from_char_idx(s: &str, char_idx: usize) -> (usize, usize) { - let mut char_count = 0; - - let mut last_line_nr = 0; - let mut last_line = s; - for (line_nr, line) in s.split('\n').enumerate() { - let line_width = line.chars().count(); - if char_idx <= char_count + line_width { - return (line_nr, char_idx - char_count); - } - char_count += line_width + 1; - last_line_nr = line_nr; - last_line = line; - } - - // safe fallback: - (last_line_nr, last_line.chars().count()) -} - -fn char_idx_from_line_col(s: &str, pos: (usize, usize)) -> usize { - let mut char_count = 0; - for (line_nr, line) in s.split('\n').enumerate() { - if line_nr == pos.0 { - return char_count + pos.1.min(line.chars().count()); - } - char_count += line.chars().count() + 1; - } - char_count -} - -fn line_from_number(s: &str, desired_line_number: usize) -> &str { - for (line_nr, line) in s.split('\n').enumerate() { - if line_nr == desired_line_number { - return line; - } - } - s -}