Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add metrics to attributes #257

Merged
merged 6 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions examples/rich-text/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ fn set_buffer_text<'a>(buffer: &mut BorrowedWithFontSystem<'a, Buffer>) {
let comic_attrs = attrs.family(Family::Name("Comic Neue"));

let spans: &[(&str, Attrs)] = &[
("Font size 64 ", attrs.metrics(Metrics::relative(64.0, 1.2))),
("Font size 8 ", attrs.metrics(Metrics::relative(8.0, 1.2))),
("Font size 20 ", attrs.metrics(Metrics::relative(20.0, 1.2))),
("Font size 14 ", attrs.metrics(Metrics::relative(14.0, 1.2))),
(
"Font size 48\n",
attrs.metrics(Metrics::relative(48.0, 1.2)),
),
("B", attrs.weight(Weight::BOLD)),
("old ", attrs),
("I", attrs.style(Style::Italic)),
Expand Down
93 changes: 93 additions & 0 deletions fonts/Inter-LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)

This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org


-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------

PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.

The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.

DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).

"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.

"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.

5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.

TERMINATION
This license becomes null and void if any of the above conditions are
not met.

DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
3 changes: 3 additions & 0 deletions fonts/Inter-Regular.ttf
Git LFS file not shown
39 changes: 38 additions & 1 deletion src/attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use core::ops::Range;
use rangemap::RangeMap;

use crate::CacheKeyFlags;
use crate::{CacheKeyFlags, Metrics};

pub use fontdb::{Family, Stretch, Style, Weight};

Expand Down Expand Up @@ -101,6 +101,32 @@
}
}

/// Metrics, but implementing Eq and Hash using u32 representation of f32
//TODO: what are the edge cases of this?
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct CacheMetrics {
font_size_bits: u32,
line_height_bits: u32,
}

impl From<Metrics> for CacheMetrics {
fn from(metrics: Metrics) -> Self {
Self {
font_size_bits: metrics.font_size.to_bits(),
line_height_bits: metrics.line_height.to_bits(),
}
}
}

impl From<CacheMetrics> for Metrics {
fn from(metrics: CacheMetrics) -> Self {
Self {
font_size: f32::from_bits(metrics.font_size_bits),
line_height: f32::from_bits(metrics.line_height_bits),
}
}
}

/// Text attributes
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Attrs<'a> {
Expand All @@ -112,6 +138,7 @@
pub weight: Weight,
pub metadata: usize,
pub cache_key_flags: CacheKeyFlags,
pub metrics_opt: Option<CacheMetrics>,
}

impl<'a> Attrs<'a> {
Expand All @@ -127,6 +154,7 @@
weight: Weight::NORMAL,
metadata: 0,
cache_key_flags: CacheKeyFlags::empty(),
metrics_opt: None,
}
}

Expand Down Expand Up @@ -172,6 +200,12 @@
self
}

/// Set [`Metrics`], overriding values in buffer
pub fn metrics(mut self, metrics: Metrics) -> Self {
self.metrics_opt = Some(metrics.into());
self
}

/// Check if font matches
pub fn matches(&self, face: &fontdb::FaceInfo) -> bool {
//TODO: smarter way of including emoji
Expand Down Expand Up @@ -219,6 +253,7 @@
pub weight: Weight,
pub metadata: usize,
pub cache_key_flags: CacheKeyFlags,
pub metrics_opt: Option<CacheMetrics>,
}

impl AttrsOwned {
Expand All @@ -231,6 +266,7 @@
weight: attrs.weight,
metadata: attrs.metadata,
cache_key_flags: attrs.cache_key_flags,
metrics_opt: attrs.metrics_opt,
}
}

Expand All @@ -243,6 +279,7 @@
weight: self.weight,
metadata: self.metadata,
cache_key_flags: self.cache_key_flags,
metrics_opt: self.metrics_opt,
}
}
}
Expand Down Expand Up @@ -300,7 +337,7 @@
}

/// Split attributes list at an offset
pub fn split_off(&mut self, index: usize) -> Self {

Check warning on line 340 in src/attrs.rs

View workflow job for this annotation

GitHub Actions / clippy

docs for function which may panic missing `# Panics` section

warning: docs for function which may panic missing `# Panics` section --> src/attrs.rs:340:5 | 340 | pub fn split_off(&mut self, index: usize) -> Self { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | note: first possible panic found here --> src/attrs.rs:356:34 | 356 | let (range, attrs) = self | __________________________________^ 357 | | .spans 358 | | .get_key_value(&key.start) 359 | | .map(|v| (v.0.clone(), v.1.clone())) 360 | | .expect("attrs span not found"); | |_______________________________________________^ = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#missing_panics_doc note: the lint level is defined here --> src/lib.rs:88:9 | 88 | #![warn(clippy::missing_panics_doc)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^
let mut new = Self::new(self.defaults.as_attrs());
let mut removes = Vec::new();

Expand Down
100 changes: 43 additions & 57 deletions src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
pub line_y: f32,
/// Y offset to top of line
pub line_top: f32,
/// Y offset to next line
pub line_height: f32,
/// Width of line
pub line_w: f32,
}
Expand All @@ -35,7 +37,7 @@
/// and `cursor_end` within this run, or None if the cursor range does not intersect this run.
/// This may return widths of zero if `cursor_start == cursor_end`, if the run is empty, or if the
/// region's left start boundary is the same as the cursor's end boundary or vice versa.
pub fn highlight(&self, cursor_start: Cursor, cursor_end: Cursor) -> Option<(f32, f32)> {

Check warning on line 40 in src/buffer.rs

View workflow job for this annotation

GitHub Actions / clippy

docs for function which may panic missing `# Panics` section

warning: docs for function which may panic missing `# Panics` section --> src/buffer.rs:40:5 | 40 | pub fn highlight(&self, cursor_start: Cursor, cursor_end: Cursor) -> Option<(f32, f32)> { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | note: first possible panic found here --> src/buffer.rs:62:25 | 62 | let x_end = x_end.expect("end of cursor not found"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#missing_panics_doc
let mut x_start = None;
let mut x_end = None;
let rtl_factor = if self.rtl { 1. } else { 0. };
Expand Down Expand Up @@ -92,54 +94,25 @@
buffer: &'b Buffer,
line_i: usize,
layout_i: usize,
remaining_len: usize,
total_layout: i32,
line_top: f32,
}

impl<'b> LayoutRunIter<'b> {
pub fn new(buffer: &'b Buffer) -> Self {
let total_layout_lines: usize = buffer
.lines
.iter()
.skip(buffer.scroll.line)
.map(|line| {
line.layout_opt()
.as_ref()
.map(|layout| layout.len())
.unwrap_or_default()
})
.sum();
let top_cropped_layout_lines =
total_layout_lines.saturating_sub(buffer.scroll.layout.try_into().unwrap_or_default());
let maximum_lines = if buffer.metrics.line_height == 0.0 {
0
} else {
(buffer.height / buffer.metrics.line_height) as i32
};
let bottom_cropped_layout_lines =
if top_cropped_layout_lines > maximum_lines.try_into().unwrap_or_default() {
maximum_lines.try_into().unwrap_or_default()
} else {
top_cropped_layout_lines
};

Self {
buffer,
line_i: buffer.scroll.line,
layout_i: 0,
remaining_len: bottom_cropped_layout_lines,
total_layout: 0,
line_top: 0.0,
}
}
}

impl<'b> Iterator for LayoutRunIter<'b> {
type Item = LayoutRun<'b>;

fn size_hint(&self) -> (usize, Option<usize>) {
(self.remaining_len, Some(self.remaining_len))
}

fn next(&mut self) -> Option<Self::Item> {
while let Some(line) = self.buffer.lines.get(self.line_i) {
let shape = line.shape_opt().as_ref()?;
Expand All @@ -153,30 +126,37 @@
continue;
}

let line_top = self
.total_layout
.saturating_sub(self.buffer.scroll.layout)
.saturating_sub(1) as f32
* self.buffer.metrics.line_height;
let mut line_height_opt: Option<f32> = None;
for glyph in layout_line.glyphs.iter() {
if let Some(glyph_line_height) = glyph.line_height_opt {
line_height_opt = match line_height_opt {
Some(line_height) => Some(line_height.max(glyph_line_height)),
None => Some(glyph_line_height),
};
}
}

let line_height = line_height_opt.unwrap_or(self.buffer.metrics.line_height);
let line_top = self.line_top;
let glyph_height = layout_line.max_ascent + layout_line.max_descent;
let centering_offset = (self.buffer.metrics.line_height - glyph_height) / 2.0;
let centering_offset = (line_height - glyph_height) / 2.0;
let line_y = line_top + centering_offset + layout_line.max_ascent;

if line_top + centering_offset > self.buffer.height {
return None;
}

return self.remaining_len.checked_sub(1).map(|num| {
self.remaining_len = num;
LayoutRun {
line_i: self.line_i,
text: line.text(),
rtl: shape.rtl,
glyphs: &layout_line.glyphs,
line_y,
line_top,
line_w: layout_line.w,
}
self.line_top += line_height;

return Some(LayoutRun {
line_i: self.line_i,
text: line.text(),
rtl: shape.rtl,
glyphs: &layout_line.glyphs,
line_y,
line_top,
line_height,
line_w: layout_line.w,
});
}
self.line_i += 1;
Expand All @@ -187,8 +167,6 @@
}
}

impl<'b> ExactSizeIterator for LayoutRunIter<'b> {}

/// Metrics of text
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Metrics {
Expand All @@ -199,13 +177,23 @@
}

impl Metrics {
/// Create metrics with given font size and line height
pub const fn new(font_size: f32, line_height: f32) -> Self {
Self {
font_size,
line_height,
}
}

/// Create metrics with given font size and calculate line height using relative scale
pub fn relative(font_size: f32, line_height_scale: f32) -> Self {
Self {
font_size,
line_height: font_size * line_height_scale,
}
}

/// Scale font size and line height
pub fn scale(self, scale: f32) -> Self {
Self {
font_size: self.font_size * scale,
Expand All @@ -223,7 +211,7 @@
/// A buffer of text that is shaped and laid out
#[derive(Debug)]
pub struct Buffer {
/// [BufferLine]s (or paragraphs) of text in the buffer

Check warning on line 214 in src/buffer.rs

View workflow job for this annotation

GitHub Actions / clippy

item in documentation is missing backticks

warning: item in documentation is missing backticks --> src/buffer.rs:214:10 | 214 | /// [BufferLine]s (or paragraphs) of text in the buffer | ^^^^^^^^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown note: the lint level is defined here --> src/lib.rs:84:9 | 84 | #![warn(clippy::doc_markdown)] | ^^^^^^^^^^^^^^^^^^^^ help: try | 214 | /// [`BufferLine`]s (or paragraphs) of text in the buffer | ~~~~~~~~~~~~
pub lines: Vec<BufferLine>,
metrics: Metrics,
width: f32,
Expand Down Expand Up @@ -328,12 +316,12 @@
}

/// Shape lines until cursor, also scrolling to include cursor in view
pub fn shape_until_cursor(
&mut self,
font_system: &mut FontSystem,
cursor: Cursor,
prune: bool,
) {

Check warning on line 324 in src/buffer.rs

View workflow job for this annotation

GitHub Actions / clippy

docs for function which may panic missing `# Panics` section

warning: docs for function which may panic missing `# Panics` section --> src/buffer.rs:319:5 | 319 | / pub fn shape_until_cursor( 320 | | &mut self, 321 | | font_system: &mut FontSystem, 322 | | cursor: Cursor, 323 | | prune: bool, 324 | | ) { | |_____^ | note: first possible panic found here --> src/buffer.rs:327:29 | 327 | let layout_cursor = self | _____________________________^ 328 | | .layout_cursor(font_system, cursor) 329 | | .expect("shape_until_cursor invalid cursor"); | |________________________________________________________^ = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#missing_panics_doc
let old_scroll = self.scroll;

let layout_cursor = self
Expand Down Expand Up @@ -404,7 +392,7 @@
}

/// Shape lines until scroll
pub fn shape_until_scroll(&mut self, font_system: &mut FontSystem, prune: bool) {

Check warning on line 395 in src/buffer.rs

View workflow job for this annotation

GitHub Actions / clippy

docs for function which may panic missing `# Panics` section

warning: docs for function which may panic missing `# Panics` section --> src/buffer.rs:395:5 | 395 | pub fn shape_until_scroll(&mut self, font_system: &mut FontSystem, prune: bool) { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | note: first possible panic found here --> src/buffer.rs:433:30 | 433 | let layout = self | ______________________________^ 434 | | .line_layout(font_system, line_i) 435 | | .expect("shape_until_scroll invalid line"); | |______________________________________________________________^ = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#missing_panics_doc
let old_scroll = self.scroll;

loop {
Expand Down Expand Up @@ -788,21 +776,19 @@
#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
let instant = std::time::Instant::now();

let font_size = self.metrics.font_size;
let line_height = self.metrics.line_height;

let mut new_cursor_opt = None;

let mut runs = self.layout_runs().peekable();
let mut first_run = true;
while let Some(run) = runs.next() {
let line_y = run.line_y;
let line_top = run.line_top;
let line_height = run.line_height;

if first_run && y < line_y - font_size {
if first_run && y < line_top {
first_run = false;
let new_cursor = Cursor::new(run.line_i, 0);
new_cursor_opt = Some(new_cursor);
} else if y >= line_y - font_size && y < line_y - font_size + line_height {
} else if y >= line_top && y < line_top + line_height {
let mut new_cursor_glyph = run.glyphs.len();
let mut new_cursor_char = 0;
let mut new_cursor_affinity = Affinity::After;
Expand Down Expand Up @@ -1118,7 +1104,7 @@
)?;
}
Motion::Vertical(px) => {
// TODO more efficient
// TODO more efficient, use layout run line height
let lines = px / self.metrics().line_height as i32;
match lines.cmp(&0) {
cmp::Ordering::Less => {
Expand Down
Loading
Loading