Introduce meticulous cursors for text galleys
This commit is contained in:
parent
e1077c98b7
commit
9ab00b8e50
9 changed files with 916 additions and 307 deletions
|
@ -592,7 +592,7 @@ fn paint_frame_interaction(
|
||||||
|
|
||||||
struct TitleBar {
|
struct TitleBar {
|
||||||
title_label: Label,
|
title_label: Label,
|
||||||
title_galley: font::Galley,
|
title_galley: Galley,
|
||||||
title_rect: Rect,
|
title_rect: Rect,
|
||||||
rect: Rect,
|
rect: Rect,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use {
|
use {
|
||||||
super::{font::Galley, fonts::TextStyle, Fonts, Srgba, Triangles},
|
super::{fonts::TextStyle, Fonts, Galley, Srgba, Triangles},
|
||||||
crate::{
|
crate::{
|
||||||
align::{anchor_rect, Align},
|
align::{anchor_rect, Align},
|
||||||
math::{Pos2, Rect},
|
math::{Pos2, Rect},
|
||||||
|
|
|
@ -9,162 +9,11 @@ use {
|
||||||
use crate::{
|
use crate::{
|
||||||
math::{vec2, Vec2},
|
math::{vec2, Vec2},
|
||||||
mutex::Mutex,
|
mutex::Mutex,
|
||||||
|
paint::{Galley, Line},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::texture_atlas::TextureAtlas;
|
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<Line>,
|
|
||||||
|
|
||||||
// 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<f32>,
|
|
||||||
|
|
||||||
/// 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.
|
// const REPLACEMENT_CHAR: char = '\u{25A1}'; // □ white square Replaces a missing or unsupported Unicode character.
|
||||||
|
@ -551,55 +400,3 @@ fn allocate_glyph(
|
||||||
uv_rect,
|
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);
|
|
||||||
}
|
|
||||||
|
|
816
egui/src/paint/galley.rs
Normal file
816
egui/src/paint/galley.rs
Normal file
|
@ -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<usize> 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<usize> 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<Line>,
|
||||||
|
|
||||||
|
// 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<f32>,
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ pub mod color;
|
||||||
pub mod command;
|
pub mod command;
|
||||||
pub mod font;
|
pub mod font;
|
||||||
pub mod fonts;
|
pub mod fonts;
|
||||||
|
mod galley;
|
||||||
pub mod stats;
|
pub mod stats;
|
||||||
pub mod tessellator;
|
pub mod tessellator;
|
||||||
mod texture_atlas;
|
mod texture_atlas;
|
||||||
|
@ -14,9 +15,10 @@ pub use {
|
||||||
color::{Rgba, Srgba},
|
color::{Rgba, Srgba},
|
||||||
command::{PaintCmd, Stroke},
|
command::{PaintCmd, Stroke},
|
||||||
fonts::{FontDefinitions, FontFamily, Fonts, TextStyle},
|
fonts::{FontDefinitions, FontFamily, Fonts, TextStyle},
|
||||||
|
galley::*,
|
||||||
stats::PaintStats,
|
stats::PaintStats,
|
||||||
tessellator::{
|
tessellator::{
|
||||||
PaintJob, PaintJobs, TesselationOptions, TextureId, Triangles, Vertex, WHITE_UV,
|
PaintJob, PaintJobs, TesselationOptions, TextureId, Triangles, Vertex, WHITE_UV,
|
||||||
},
|
},
|
||||||
texture_atlas::Texture,
|
texture_atlas::{Texture, TextureAtlas},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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)
|
Self::from_slice(galley.text.as_bytes()) + Self::from_slice(&galley.lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
||||||
color,
|
color,
|
||||||
layers::PaintCmdIdx,
|
layers::PaintCmdIdx,
|
||||||
math::{Pos2, Rect, Vec2},
|
math::{Pos2, Rect, Vec2},
|
||||||
paint::{font, Fonts, PaintCmd, Stroke, TextStyle},
|
paint::{Fonts, Galley, PaintCmd, Stroke, TextStyle},
|
||||||
Context, LayerId, Srgba,
|
Context, LayerId, Srgba,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -278,7 +278,7 @@ impl Painter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Paint text that has already been layed out in a `Galley`.
|
/// 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 {
|
self.add(PaintCmd::Text {
|
||||||
pos,
|
pos,
|
||||||
galley,
|
galley,
|
||||||
|
|
|
@ -78,7 +78,7 @@ impl Label {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn layout(&self, ui: &Ui) -> font::Galley {
|
pub fn layout(&self, ui: &Ui) -> Galley {
|
||||||
let max_width = ui.available().width();
|
let max_width = ui.available().width();
|
||||||
// Prevent word-wrapping after a single letter, and other silly shit:
|
// Prevent word-wrapping after a single letter, and other silly shit:
|
||||||
// TODO: general "don't force labels and similar to wrap so early"
|
// TODO: general "don't force labels and similar to wrap so early"
|
||||||
|
@ -86,7 +86,7 @@ impl Label {
|
||||||
self.layout_width(ui, max_width)
|
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 text_style = self.text_style_or_default(ui.style());
|
||||||
let font = &ui.fonts()[text_style];
|
let font = &ui.fonts()[text_style];
|
||||||
if self.multiline {
|
if self.multiline {
|
||||||
|
@ -109,7 +109,7 @@ impl Label {
|
||||||
// TODO: a paint method for painting anywhere in a ui.
|
// TODO: a paint method for painting anywhere in a ui.
|
||||||
// This should be the easiest method of putting text anywhere.
|
// 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_style = self.text_style_or_default(ui.style());
|
||||||
let text_color = self
|
let text_color = self
|
||||||
.text_color
|
.text_color
|
||||||
|
|
|
@ -2,12 +2,26 @@ use crate::{paint::*, *};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
#[cfg_attr(feature = "serde", serde(default))]
|
||||||
pub(crate) struct State {
|
pub(crate) struct State {
|
||||||
/// Character based, NOT bytes.
|
/// We store as PCursor (paragraph number, and character offset within that paragraph).
|
||||||
/// TODO: store as line + row
|
/// This is so what if we resize the `TextEdit` region, and text wrapping changes,
|
||||||
pub cursor: Option<usize>,
|
/// 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<PCursor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
/// A text region that the user can edit the contents of.
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
|
@ -167,7 +181,7 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
if response.clicked && enabled {
|
if response.clicked && enabled {
|
||||||
ui.memory().request_kb_focus(id);
|
ui.memory().request_kb_focus(id);
|
||||||
if let Some(mouse_pos) = ui.input().mouse.pos {
|
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) {
|
} else if ui.input().mouse.click || (ui.input().mouse.pressed && !response.hovered) {
|
||||||
// User clicked somewhere else
|
// User clicked somewhere else
|
||||||
|
@ -182,19 +196,29 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.memory().has_kb_focus(id) && enabled {
|
if ui.memory().has_kb_focus(id) && enabled {
|
||||||
let mut cursor = state.cursor.unwrap_or_else(|| text.chars().count());
|
let mut cursor = state
|
||||||
cursor = clamp(cursor, 0..=text.chars().count());
|
.pcursor
|
||||||
|
.map(|pcursor| galley.from_pcursor(pcursor))
|
||||||
|
.unwrap_or_else(|| galley.end());
|
||||||
|
|
||||||
for event in &ui.input().events {
|
for event in &ui.input().events {
|
||||||
match event {
|
let did_mutate_text = match event {
|
||||||
Event::Copy | Event::Cut => {
|
Event::Copy | Event::Cut => {
|
||||||
// TODO: cut
|
// TODO: cut
|
||||||
ui.ctx().output().copied_text = text.clone();
|
ui.ctx().output().copied_text = text.clone();
|
||||||
|
None
|
||||||
}
|
}
|
||||||
Event::Text(text_to_insert) => {
|
Event::Text(text_to_insert) => {
|
||||||
// newlines are handled by `Key::Enter`.
|
// Newlines are handled by `Key::Enter`.
|
||||||
if text_to_insert != "\n" && text_to_insert != "\r" {
|
if !text_to_insert.is_empty()
|
||||||
insert_text(&mut cursor, text, text_to_insert);
|
&& 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 {
|
Event::Key {
|
||||||
|
@ -202,7 +226,9 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
pressed: true,
|
pressed: true,
|
||||||
} => {
|
} => {
|
||||||
if multiline {
|
if multiline {
|
||||||
insert_text(&mut cursor, text, "\n");
|
let mut ccursor = cursor.ccursor;
|
||||||
|
insert_text(&mut ccursor, text, "\n");
|
||||||
|
Some(ccursor)
|
||||||
} else {
|
} else {
|
||||||
// Common to end input with enter
|
// Common to end input with enter
|
||||||
ui.memory().surrender_kb_focus(id);
|
ui.memory().surrender_kb_focus(id);
|
||||||
|
@ -217,22 +243,25 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Event::Key { key, pressed: true } => {
|
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);
|
state.pcursor = Some(cursor.pcursor);
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let painter = ui.painter();
|
let painter = ui.painter();
|
||||||
|
@ -259,8 +288,8 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
};
|
};
|
||||||
|
|
||||||
if show_cursor {
|
if show_cursor {
|
||||||
if let Some(cursor) = state.cursor {
|
if let Some(pcursor) = state.pcursor {
|
||||||
let cursor_pos = response.rect.min + galley.char_start_pos(cursor);
|
let cursor_pos = response.rect.min + galley.pos_from_pcursor(pcursor);
|
||||||
painter.line_segment(
|
painter.line_segment(
|
||||||
[cursor_pos, cursor_pos + vec2(0.0, line_spacing)],
|
[cursor_pos, cursor_pos + vec2(0.0, line_spacing)],
|
||||||
(ui.style().visuals.text_cursor_width, color::WHITE),
|
(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) {
|
fn insert_text(ccursor: &mut CCursor, text: &mut String, text_to_insert: &str) {
|
||||||
// eprintln!("insert_text {:?}", text_to_insert);
|
|
||||||
|
|
||||||
let mut char_it = text.chars();
|
let mut char_it = text.chars();
|
||||||
let mut new_text = String::with_capacity(text.capacity());
|
let mut new_text = String::with_capacity(text.capacity());
|
||||||
for _ in 0..*cursor {
|
for _ in 0..ccursor.index {
|
||||||
let c = char_it.next().unwrap();
|
let c = char_it.next().unwrap();
|
||||||
new_text.push(c);
|
new_text.push(c);
|
||||||
}
|
}
|
||||||
*cursor += text_to_insert.chars().count();
|
ccursor.index += text_to_insert.chars().count();
|
||||||
new_text += text_to_insert;
|
new_text += text_to_insert;
|
||||||
new_text.extend(char_it);
|
new_text.extend(char_it);
|
||||||
*text = new_text;
|
*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<CCursor> {
|
||||||
// eprintln!("on_key_press before: '{}', cursor at {}", text, cursor);
|
// eprintln!("on_key_press before: '{}', cursor at {}", text, cursor);
|
||||||
|
|
||||||
match key {
|
match key {
|
||||||
Key::Backspace if *cursor > 0 => {
|
Key::Backspace if cursor.ccursor.index > 0 => {
|
||||||
*cursor -= 1;
|
*cursor = galley.from_ccursor(cursor.ccursor - 1);
|
||||||
|
|
||||||
let mut char_it = text.chars();
|
let mut char_it = text.chars();
|
||||||
let mut new_text = String::with_capacity(text.capacity());
|
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.push(char_it.next().unwrap())
|
||||||
}
|
}
|
||||||
new_text.extend(char_it.skip(1));
|
new_text.extend(char_it.skip(1));
|
||||||
*text = new_text;
|
*text = new_text;
|
||||||
|
Some(cursor.ccursor)
|
||||||
}
|
}
|
||||||
Key::Delete => {
|
Key::Delete => {
|
||||||
let mut char_it = text.chars();
|
let mut char_it = text.chars();
|
||||||
let mut new_text = String::with_capacity(text.capacity());
|
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.push(char_it.next().unwrap())
|
||||||
}
|
}
|
||||||
new_text.extend(char_it.skip(1));
|
new_text.extend(char_it.skip(1));
|
||||||
*text = new_text;
|
*text = new_text;
|
||||||
|
Some(cursor.ccursor)
|
||||||
}
|
}
|
||||||
Key::Enter => {} // handled earlier
|
Key::Enter => unreachable!("Should have been handled earlier"),
|
||||||
|
|
||||||
Key::Home => {
|
Key::Home => {
|
||||||
// To start of paragraph:
|
// To start of line:
|
||||||
let pos = line_col_from_char_idx(text, *cursor);
|
*cursor = galley.cursor_begin_of_line(cursor);
|
||||||
*cursor = char_idx_from_line_col(text, (pos.0, 0));
|
None
|
||||||
}
|
}
|
||||||
Key::End => {
|
Key::End => {
|
||||||
// To end of paragraph:
|
*cursor = galley.cursor_end_of_line(cursor);
|
||||||
let pos = line_col_from_char_idx(text, *cursor);
|
None
|
||||||
let line = line_from_number(text, pos.0);
|
|
||||||
*cursor = char_idx_from_line_col(text, (pos.0, line.chars().count()));
|
|
||||||
}
|
}
|
||||||
Key::Left if *cursor > 0 => {
|
Key::Left => {
|
||||||
*cursor -= 1;
|
*cursor = galley.cursor_left_one_character(cursor);
|
||||||
|
None
|
||||||
}
|
}
|
||||||
Key::Right => {
|
Key::Right => {
|
||||||
*cursor = (*cursor + 1).min(text.chars().count());
|
*cursor = galley.cursor_right_one_character(cursor);
|
||||||
|
None
|
||||||
}
|
}
|
||||||
Key::Up => {
|
Key::Up => {
|
||||||
let mut pos = line_col_from_char_idx(text, *cursor);
|
*cursor = galley.cursor_up_one_line(cursor);
|
||||||
pos.0 = pos.0.saturating_sub(1);
|
None
|
||||||
*cursor = char_idx_from_line_col(text, pos);
|
|
||||||
}
|
}
|
||||||
Key::Down => {
|
Key::Down => {
|
||||||
let mut pos = line_col_from_char_idx(text, *cursor);
|
*cursor = galley.cursor_down_one_line(cursor);
|
||||||
pos.0 += 1;
|
None
|
||||||
*cursor = char_idx_from_line_col(text, pos);
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => None,
|
||||||
}
|
}
|
||||||
|
|
||||||
// eprintln!("on_key_press after: '{}', cursor at {}\n", text, cursor);
|
// 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
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue