mirror of
https://github.com/emilk/egui.git
synced 2026-08-31 05:40:03 -04:00
Merge branch 'main' into lucas/malmal/main
This commit is contained in:
@@ -11,7 +11,7 @@ readme = "README.md"
|
||||
repository = "https://github.com/emilk/egui/tree/main/crates/epaint"
|
||||
categories = ["graphics", "gui"]
|
||||
keywords = ["graphics", "gui", "egui"]
|
||||
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
||||
include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -48,7 +48,7 @@ mint = ["emath/mint"]
|
||||
rayon = ["dep:rayon"]
|
||||
|
||||
## Allow serialization using [`serde`](https://docs.rs/serde).
|
||||
serde = ["dep:serde", "ahash/serde", "emath/serde", "ecolor/serde"]
|
||||
serde = ["dep:serde", "ahash/serde", "emath/serde", "ecolor/serde", "font-types/serde", "smallvec/serde"]
|
||||
|
||||
## Change Vertex layout to be compatible with unity
|
||||
unity = []
|
||||
@@ -62,12 +62,14 @@ emath.workspace = true
|
||||
ecolor.workspace = true
|
||||
|
||||
ahash.workspace = true
|
||||
font-types.workspace = true
|
||||
log.workspace = true
|
||||
nohash-hasher.workspace = true
|
||||
parking_lot.workspace = true # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios.
|
||||
profiling.workspace = true
|
||||
self_cell.workspace = true
|
||||
skrifa.workspace = true
|
||||
smallvec.workspace = true
|
||||
vello_cpu.workspace = true
|
||||
|
||||
#! ### Optional dependencies
|
||||
|
||||
@@ -57,6 +57,7 @@ pub fn adjust_colors(
|
||||
radius: _,
|
||||
fill,
|
||||
stroke,
|
||||
angle: _,
|
||||
})
|
||||
| Shape::Rect(RectShape {
|
||||
rect: _,
|
||||
@@ -67,6 +68,7 @@ pub fn adjust_colors(
|
||||
round_to_pixels: _,
|
||||
blur_width: _,
|
||||
brush: _,
|
||||
angle: _,
|
||||
}) => {
|
||||
adjust_color(fill);
|
||||
adjust_color(&mut stroke.color);
|
||||
|
||||
@@ -10,6 +10,9 @@ pub struct EllipseShape {
|
||||
pub radius: Vec2,
|
||||
pub fill: Color32,
|
||||
pub stroke: Stroke,
|
||||
|
||||
/// Rotate ellipse by this many radians clockwise around its center.
|
||||
pub angle: f32,
|
||||
}
|
||||
|
||||
impl EllipseShape {
|
||||
@@ -20,6 +23,7 @@ impl EllipseShape {
|
||||
radius,
|
||||
fill: fill_color.into(),
|
||||
stroke: Default::default(),
|
||||
angle: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,18 +34,38 @@ impl EllipseShape {
|
||||
radius,
|
||||
fill: Default::default(),
|
||||
stroke: stroke.into(),
|
||||
angle: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the rotation of the ellipse (in radians, clockwise).
|
||||
/// The ellipse rotates around its center.
|
||||
#[inline]
|
||||
pub fn with_angle(mut self, angle: f32) -> Self {
|
||||
self.angle = angle;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the rotation of the ellipse (in radians, clockwise) around a custom pivot point.
|
||||
#[inline]
|
||||
pub fn with_angle_and_pivot(mut self, angle: f32, pivot: Pos2) -> Self {
|
||||
self.angle = angle;
|
||||
let rot = emath::Rot2::from_angle(angle);
|
||||
self.center = pivot + rot * (self.center - pivot);
|
||||
self
|
||||
}
|
||||
|
||||
/// The visual bounding rectangle (includes stroke width)
|
||||
pub fn visual_bounding_rect(&self) -> Rect {
|
||||
if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() {
|
||||
Rect::NOTHING
|
||||
} else {
|
||||
Rect::from_center_size(
|
||||
self.center,
|
||||
let rect = Rect::from_center_size(
|
||||
Pos2::ZERO,
|
||||
self.radius * 2.0 + Vec2::splat(self.stroke.width),
|
||||
)
|
||||
);
|
||||
rect.rotate_bb(emath::Rot2::from_angle(self.angle))
|
||||
.translate(self.center.to_vec2())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,13 +54,16 @@ pub struct RectShape {
|
||||
/// Since most rectangles do not have a texture, this is optional and in an `Arc`,
|
||||
/// so that [`RectShape`] is kept small..
|
||||
pub brush: Option<Arc<Brush>>,
|
||||
|
||||
/// Rotate rectangle by this many radians clockwise around its center.
|
||||
pub angle: f32,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rect_shape_size() {
|
||||
assert_eq!(
|
||||
std::mem::size_of::<RectShape>(),
|
||||
48,
|
||||
56,
|
||||
"RectShape changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it."
|
||||
);
|
||||
assert!(
|
||||
@@ -88,6 +91,7 @@ impl RectShape {
|
||||
round_to_pixels: None,
|
||||
blur_width: 0.0,
|
||||
brush: Default::default(),
|
||||
angle: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +161,25 @@ impl RectShape {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the rotation of the rectangle (in radians, clockwise).
|
||||
/// The rectangle rotates around its center.
|
||||
#[inline]
|
||||
pub fn with_angle(mut self, angle: f32) -> Self {
|
||||
self.angle = angle;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the rotation of the rectangle (in radians, clockwise) around a custom pivot point.
|
||||
#[inline]
|
||||
pub fn with_angle_and_pivot(mut self, angle: f32, pivot: Pos2) -> Self {
|
||||
self.angle = angle;
|
||||
let rot = emath::Rot2::from_angle(angle);
|
||||
let center = self.rect.center();
|
||||
let new_center = pivot + rot * (center - pivot);
|
||||
self.rect = self.rect.translate(new_center - center);
|
||||
self
|
||||
}
|
||||
|
||||
/// The visual bounding rectangle (includes stroke width)
|
||||
#[inline]
|
||||
pub fn visual_bounding_rect(&self) -> Rect {
|
||||
@@ -168,7 +191,17 @@ impl RectShape {
|
||||
StrokeKind::Middle => self.stroke.width / 2.0,
|
||||
StrokeKind::Outside => self.stroke.width,
|
||||
};
|
||||
self.rect.expand(expand + self.blur_width / 2.0)
|
||||
let expanded = self.rect.expand(expand + self.blur_width / 2.0);
|
||||
if self.angle == 0.0 {
|
||||
expanded
|
||||
} else {
|
||||
// Rotate around the rectangle's center and compute bounding box
|
||||
let center = self.rect.center();
|
||||
let rect_relative = Rect::from_center_size(Pos2::ZERO, expanded.size());
|
||||
rect_relative
|
||||
.rotate_bb(emath::Rot2::from_angle(self.angle))
|
||||
.translate(center.to_vec2())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1546,6 +1546,7 @@ impl Tessellator {
|
||||
radius,
|
||||
fill,
|
||||
stroke,
|
||||
angle,
|
||||
} = shape;
|
||||
|
||||
if radius.x <= 0.0 || radius.y <= 0.0 {
|
||||
@@ -1596,6 +1597,14 @@ impl Tessellator {
|
||||
points.push(center + Vec2::new(0.0, -radius.y));
|
||||
points.extend(quarter.iter().rev().map(|p| center + Vec2::new(p.x, -p.y)));
|
||||
|
||||
// Apply rotation if angle is non-zero
|
||||
if angle != 0.0 {
|
||||
let rot = emath::Rot2::from_angle(angle);
|
||||
for point in &mut points {
|
||||
*point = center + rot * (*point - center);
|
||||
}
|
||||
}
|
||||
|
||||
let path_stroke = PathStroke::from(stroke).outside();
|
||||
self.scratchpad_path.clear();
|
||||
self.scratchpad_path.add_line_loop(&points);
|
||||
@@ -1773,6 +1782,7 @@ impl Tessellator {
|
||||
round_to_pixels,
|
||||
mut blur_width,
|
||||
brush: _, // brush is extracted on its own, because it is not Copy
|
||||
angle,
|
||||
} = *rect_shape;
|
||||
|
||||
let mut corner_radius = CornerRadiusF32::from(corner_radius);
|
||||
@@ -1940,6 +1950,16 @@ impl Tessellator {
|
||||
let path = &mut self.scratchpad_path;
|
||||
path.clear();
|
||||
path::rounded_rectangle(&mut self.scratchpad_points, rect, corner_radius);
|
||||
|
||||
// Apply rotation if angle is non-zero
|
||||
if angle != 0.0 {
|
||||
let rot = emath::Rot2::from_angle(angle);
|
||||
let center = rect.center();
|
||||
for point in &mut self.scratchpad_points {
|
||||
*point = center + rot * (*point - center);
|
||||
}
|
||||
}
|
||||
|
||||
path.add_line_loop(&self.scratchpad_points);
|
||||
|
||||
let path_stroke = PathStroke::from(stroke).with_kind(stroke_kind);
|
||||
|
||||
@@ -12,7 +12,7 @@ use vello_cpu::{color, kurbo};
|
||||
use crate::{
|
||||
TextOptions, TextureAtlas,
|
||||
text::{
|
||||
FontTweak,
|
||||
FontTweak, VariationCoords,
|
||||
fonts::{Blob, CachedFamily, FontFaceKey},
|
||||
},
|
||||
};
|
||||
@@ -145,8 +145,8 @@ struct GlyphCacheKey(u64);
|
||||
impl nohash_hasher::IsEnabled for GlyphCacheKey {}
|
||||
|
||||
impl GlyphCacheKey {
|
||||
fn new(glyph_id: skrifa::GlyphId, metrics: &ScaledMetrics, bin: SubpixelBin) -> Self {
|
||||
let ScaledMetrics {
|
||||
fn new(glyph_id: skrifa::GlyphId, metrics: &StyledMetrics, bin: SubpixelBin) -> Self {
|
||||
let StyledMetrics {
|
||||
pixels_per_point,
|
||||
px_scale_factor,
|
||||
..
|
||||
@@ -197,10 +197,10 @@ impl FontCell {
|
||||
fn allocate_glyph_uncached(
|
||||
&mut self,
|
||||
atlas: &mut TextureAtlas,
|
||||
metrics: &ScaledMetrics,
|
||||
metrics: &StyledMetrics,
|
||||
glyph_info: &GlyphInfo,
|
||||
bin: SubpixelBin,
|
||||
location: &skrifa::instance::Location,
|
||||
location: skrifa::instance::LocationRef<'_>,
|
||||
) -> Option<GlyphAllocation> {
|
||||
let glyph_id = glyph_info.id?;
|
||||
|
||||
@@ -337,8 +337,6 @@ pub struct FontFace {
|
||||
font: FontCell,
|
||||
tweak: FontTweak,
|
||||
|
||||
/// Variable font location (for weight axis, etc.)
|
||||
location: skrifa::instance::Location,
|
||||
glyph_info_cache: ahash::HashMap<char, GlyphInfo>,
|
||||
glyph_alloc_cache: ahash::HashMap<GlyphCacheKey, GlyphAllocation>,
|
||||
|
||||
@@ -355,7 +353,6 @@ impl FontFace {
|
||||
font_data: Blob,
|
||||
index: u32,
|
||||
tweak: FontTweak,
|
||||
preferred_weight: Option<u16>,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let font = FontCell::try_new(font_data, |font_data| {
|
||||
let skrifa_font =
|
||||
@@ -401,44 +398,10 @@ impl FontFace {
|
||||
})
|
||||
})?;
|
||||
|
||||
// Use preferred_weight if provided, otherwise try to read from the OS/2 table or fvar default
|
||||
let weight = preferred_weight.or_else(|| {
|
||||
// First try OS/2 table
|
||||
if let Some(w) = font
|
||||
.borrow_dependent()
|
||||
.skrifa
|
||||
.os2()
|
||||
.ok()
|
||||
.map(|os2| os2.us_weight_class())
|
||||
{
|
||||
return Some(w);
|
||||
}
|
||||
// If no OS/2 or preferred_weight, try to get default from variable font's fvar table
|
||||
font.borrow_dependent()
|
||||
.skrifa
|
||||
.axes()
|
||||
.iter()
|
||||
.find(|axis| axis.tag() == skrifa::raw::types::Tag::new(b"wght"))
|
||||
.map(|axis| axis.default_value() as u16)
|
||||
});
|
||||
|
||||
// Create location for variable font with weight axis
|
||||
// If weight is provided (either from preferred_weight, OS/2, or fvar default), use it
|
||||
// Otherwise fall back to Location::default() which uses all axis defaults
|
||||
let location = if let Some(w) = weight {
|
||||
font.borrow_dependent()
|
||||
.skrifa
|
||||
.axes()
|
||||
.location([("wght", w as f32)])
|
||||
} else {
|
||||
skrifa::instance::Location::default()
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
font,
|
||||
tweak,
|
||||
location,
|
||||
glyph_info_cache: Default::default(),
|
||||
glyph_alloc_cache: Default::default(),
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@@ -544,7 +507,7 @@ impl FontFace {
|
||||
#[inline]
|
||||
pub(super) fn pair_kerning_pixels(
|
||||
&self,
|
||||
metrics: &ScaledMetrics,
|
||||
metrics: &StyledMetrics,
|
||||
last_glyph_id: skrifa::GlyphId,
|
||||
glyph_id: skrifa::GlyphId,
|
||||
) -> f32 {
|
||||
@@ -566,7 +529,7 @@ impl FontFace {
|
||||
#[inline]
|
||||
pub fn pair_kerning(
|
||||
&self,
|
||||
metrics: &ScaledMetrics,
|
||||
metrics: &StyledMetrics,
|
||||
last_glyph_id: skrifa::GlyphId,
|
||||
glyph_id: skrifa::GlyphId,
|
||||
) -> f32 {
|
||||
@@ -574,7 +537,12 @@ impl FontFace {
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn scaled_metrics(&self, pixels_per_point: f32, font_size: f32) -> ScaledMetrics {
|
||||
pub fn styled_metrics(
|
||||
&self,
|
||||
pixels_per_point: f32,
|
||||
font_size: f32,
|
||||
coords: &VariationCoords,
|
||||
) -> StyledMetrics {
|
||||
let pt_scale_factor = self.font.px_scale_factor(font_size * self.tweak.scale);
|
||||
let font_data = self.font.borrow_dependent();
|
||||
let ascent = (font_data.metrics.ascent * pt_scale_factor).round_ui();
|
||||
@@ -588,20 +556,32 @@ impl FontFace {
|
||||
+ self.tweak.y_offset)
|
||||
.round_ui();
|
||||
|
||||
ScaledMetrics {
|
||||
let axes = font_data.skrifa.axes();
|
||||
// Override the default coordinates with ones specified via FontTweak, then the ones specified directly via the
|
||||
// argument (probably from TextFormat).
|
||||
let settings = self
|
||||
.tweak
|
||||
.coords
|
||||
.as_ref()
|
||||
.iter()
|
||||
.chain(coords.as_ref().iter());
|
||||
let location = axes.location(settings);
|
||||
|
||||
StyledMetrics {
|
||||
pixels_per_point,
|
||||
px_scale_factor,
|
||||
scale,
|
||||
y_offset_in_points,
|
||||
ascent,
|
||||
row_height: ascent - descent + line_gap,
|
||||
location,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn allocate_glyph(
|
||||
&mut self,
|
||||
atlas: &mut TextureAtlas,
|
||||
metrics: &ScaledMetrics,
|
||||
metrics: &StyledMetrics,
|
||||
glyph_info: GlyphInfo,
|
||||
chr: char,
|
||||
h_pos: f32,
|
||||
@@ -653,7 +633,7 @@ impl FontFace {
|
||||
// Allocate the glyph
|
||||
let allocation = self
|
||||
.font
|
||||
.allocate_glyph_uncached(atlas, metrics, &glyph_info, bin, &self.location)
|
||||
.allocate_glyph_uncached(atlas, metrics, &glyph_info, bin, (&metrics.location).into())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Insert into cache
|
||||
@@ -711,12 +691,17 @@ impl Font<'_> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn scaled_metrics(&self, pixels_per_point: f32, font_size: f32) -> ScaledMetrics {
|
||||
pub fn styled_metrics(
|
||||
&self,
|
||||
pixels_per_point: f32,
|
||||
font_size: f32,
|
||||
coords: &VariationCoords,
|
||||
) -> StyledMetrics {
|
||||
self.cached_family
|
||||
.fonts
|
||||
.first()
|
||||
.and_then(|key| self.fonts_by_id.get(key))
|
||||
.map(|font_face| font_face.scaled_metrics(pixels_per_point, font_size))
|
||||
.map(|font_face| font_face.styled_metrics(pixels_per_point, font_size, coords))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -759,8 +744,8 @@ impl Font<'_> {
|
||||
}
|
||||
|
||||
/// Metrics for a font at a specific screen-space scale.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Default)]
|
||||
pub struct ScaledMetrics {
|
||||
#[derive(Clone, Debug, PartialEq, Default)]
|
||||
pub struct StyledMetrics {
|
||||
/// The DPI part of the screen-space scale.
|
||||
pub pixels_per_point: f32,
|
||||
|
||||
@@ -784,6 +769,9 @@ pub struct ScaledMetrics {
|
||||
///
|
||||
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
|
||||
pub row_height: f32,
|
||||
|
||||
/// Resolved variation coordinates.
|
||||
pub location: skrifa::instance::Location,
|
||||
}
|
||||
|
||||
/// Code points that will always be invisible (zero width).
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::{
|
||||
use crate::{
|
||||
TextureAtlas,
|
||||
text::{
|
||||
Galley, LayoutJob, LayoutSection, TextOptions,
|
||||
Galley, LayoutJob, LayoutSection, TextOptions, VariationCoords,
|
||||
font::{Font, FontFace, GlyphInfo},
|
||||
},
|
||||
};
|
||||
@@ -125,12 +125,6 @@ pub struct FontData {
|
||||
|
||||
/// Extra scale and vertical tweak to apply to all text of this font.
|
||||
pub tweak: FontTweak,
|
||||
|
||||
/// The font weight (100-900), if available.
|
||||
/// Standard values: 100 (Thin), 200 (Extra Light), 300 (Light), 400 (Regular),
|
||||
/// 500 (Medium), 600 (Semi Bold), 700 (Bold), 800 (Extra Bold), 900 (Black).
|
||||
/// `None` if the weight could not be determined.
|
||||
pub weight: Option<u16>,
|
||||
}
|
||||
|
||||
impl FontData {
|
||||
@@ -139,7 +133,6 @@ impl FontData {
|
||||
font: Cow::Borrowed(font),
|
||||
index: 0,
|
||||
tweak: Default::default(),
|
||||
weight: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,43 +141,12 @@ impl FontData {
|
||||
font: Cow::Owned(font),
|
||||
index: 0,
|
||||
tweak: Default::default(),
|
||||
weight: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tweak(self, tweak: FontTweak) -> Self {
|
||||
Self { tweak, ..self }
|
||||
}
|
||||
|
||||
/// Set the font weight (100-900).
|
||||
///
|
||||
/// This is typically read automatically from the font file when loaded,
|
||||
/// but can be overridden manually if needed.
|
||||
///
|
||||
/// Standard weight values:
|
||||
/// - 100: Thin
|
||||
/// - 200: Extra Light
|
||||
/// - 300: Light
|
||||
/// - 400: Regular/Normal
|
||||
/// - 500: Medium
|
||||
/// - 600: Semi Bold
|
||||
/// - 700: Bold
|
||||
/// - 800: Extra Bold
|
||||
/// - 900: Black
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use epaint::text::FontData;
|
||||
/// let font_data = FontData::from_static(include_bytes!("../../../epaint_default_fonts/fonts/Ubuntu-Light.ttf"))
|
||||
/// .weight(300); // Override to Light weight
|
||||
/// assert_eq!(font_data.weight, Some(300));
|
||||
/// ```
|
||||
pub fn weight(self, weight: u16) -> Self {
|
||||
Self {
|
||||
weight: Some(weight),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for FontData {
|
||||
@@ -196,7 +158,7 @@ impl AsRef<[u8]> for FontData {
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Extra scale and vertical tweak to apply to all text of a certain font.
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct FontTweak {
|
||||
/// Scale the font's glyphs by this much.
|
||||
@@ -228,6 +190,9 @@ pub struct FontTweak {
|
||||
///
|
||||
/// `None` means use the global setting.
|
||||
pub hinting_override: Option<bool>,
|
||||
|
||||
/// Override the font's default variation coordinates.
|
||||
pub coords: VariationCoords,
|
||||
}
|
||||
|
||||
impl Default for FontTweak {
|
||||
@@ -237,6 +202,7 @@ impl Default for FontTweak {
|
||||
y_offset_factor: 0.0,
|
||||
y_offset: 0.0,
|
||||
hinting_override: None,
|
||||
coords: VariationCoords::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -718,7 +684,12 @@ impl FontsView<'_> {
|
||||
pub fn row_height(&mut self, font_id: &FontId) -> f32 {
|
||||
self.fonts
|
||||
.font(&font_id.family)
|
||||
.scaled_metrics(self.pixels_per_point, font_id.size)
|
||||
.styled_metrics(
|
||||
self.pixels_per_point,
|
||||
font_id.size,
|
||||
// TODO(valadaptive): use font variation coords when calculating row height
|
||||
&VariationCoords::default(),
|
||||
)
|
||||
.row_height
|
||||
}
|
||||
|
||||
@@ -824,15 +795,13 @@ impl FontsImpl {
|
||||
let mut fonts_by_id: nohash_hasher::IntMap<FontFaceKey, FontFace> = Default::default();
|
||||
let mut fonts_by_name: ahash::HashMap<String, FontFaceKey> = Default::default();
|
||||
for (name, font_data) in &definitions.font_data {
|
||||
let tweak = font_data.tweak;
|
||||
let blob = blob_from_font_data(font_data);
|
||||
let font_face = FontFace::new(
|
||||
options,
|
||||
name.clone(),
|
||||
blob,
|
||||
font_data.index,
|
||||
tweak,
|
||||
font_data.weight,
|
||||
font_data.tweak.clone(),
|
||||
)
|
||||
.unwrap_or_else(|err| panic!("Error parsing {name:?} TTF/OTF font file: {err}"));
|
||||
let key = FontFaceKey::new();
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
Color32, Mesh, Stroke, Vertex,
|
||||
stroke::PathStroke,
|
||||
text::{
|
||||
font::{ScaledMetrics, is_cjk, is_cjk_break_allowed},
|
||||
font::{StyledMetrics, is_cjk, is_cjk_break_allowed},
|
||||
fonts::FontFaceKey,
|
||||
},
|
||||
};
|
||||
@@ -114,7 +114,7 @@ pub fn layout(fonts: &mut FontsImpl, pixels_per_point: f32, job: Arc<LayoutJob>)
|
||||
let intrinsic_size = calculate_intrinsic_size(point_scale, &job, ¶graphs);
|
||||
|
||||
let mut elided = false;
|
||||
let mut rows = rows_from_paragraphs(paragraphs, &job, &mut elided);
|
||||
let mut rows = rows_from_paragraphs(paragraphs, &job, pixels_per_point, &mut elided);
|
||||
if elided && let Some(last_placed) = rows.last_mut() {
|
||||
let last_row = Arc::make_mut(&mut last_placed.row);
|
||||
replace_last_glyph_with_overflow_character(fonts, pixels_per_point, &job, last_row);
|
||||
@@ -160,7 +160,7 @@ fn layout_section(
|
||||
} = section;
|
||||
let mut font = fonts.font(&format.font_id.family);
|
||||
let font_size = format.font_id.size;
|
||||
let font_metrics = font.scaled_metrics(pixels_per_point, font_size);
|
||||
let font_metrics = font.styled_metrics(pixels_per_point, font_size, &format.coords);
|
||||
let line_height = section
|
||||
.format
|
||||
.line_height
|
||||
@@ -178,7 +178,7 @@ fn layout_section(
|
||||
|
||||
// Optimization: only recompute `ScaledMetrics` when the concrete `FontImpl` changes.
|
||||
let mut current_font = FontFaceKey::INVALID;
|
||||
let mut current_font_face_metrics = ScaledMetrics::default();
|
||||
let mut current_font_face_metrics = StyledMetrics::default();
|
||||
|
||||
for chr in job.text[byte_range.clone()].chars() {
|
||||
if job.break_on_newline && chr == '\n' {
|
||||
@@ -192,7 +192,9 @@ fn layout_section(
|
||||
current_font = font_id;
|
||||
current_font_face_metrics = font_face
|
||||
.as_ref()
|
||||
.map(|font_face| font_face.scaled_metrics(pixels_per_point, font_size))
|
||||
.map(|font_face| {
|
||||
font_face.styled_metrics(pixels_per_point, font_size, &format.coords)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
@@ -252,11 +254,12 @@ fn calculate_intrinsic_size(
|
||||
) -> Vec2 {
|
||||
let mut intrinsic_size = Vec2::ZERO;
|
||||
for (idx, paragraph) in paragraphs.iter().enumerate() {
|
||||
let width = paragraph
|
||||
.glyphs
|
||||
.last()
|
||||
.map(|l| l.max_x())
|
||||
.unwrap_or_default();
|
||||
// Use the precise cursor position instead of `last_glyph.max_x()`,
|
||||
// because glyph positions are pixel-snapped but the cursor tracks
|
||||
// the exact subpixel advance. This ensures that when two galleys are
|
||||
// placed side-by-side, the gap matches what it would be within a
|
||||
// single galley.
|
||||
let width = paragraph.cursor_x_px / point_scale.pixels_per_point;
|
||||
intrinsic_size.x = f32::max(intrinsic_size.x, width);
|
||||
|
||||
let mut height = paragraph
|
||||
@@ -277,6 +280,7 @@ fn calculate_intrinsic_size(
|
||||
fn rows_from_paragraphs(
|
||||
paragraphs: Vec<Paragraph>,
|
||||
job: &LayoutJob,
|
||||
pixels_per_point: f32,
|
||||
elided: &mut bool,
|
||||
) -> Vec<PlacedRow> {
|
||||
let num_paragraphs = paragraphs.len();
|
||||
@@ -303,8 +307,11 @@ fn rows_from_paragraphs(
|
||||
ends_with_newline: !is_last_paragraph,
|
||||
});
|
||||
} else {
|
||||
let paragraph_max_x = paragraph.glyphs.last().unwrap().max_x();
|
||||
if paragraph_max_x <= job.effective_wrap_width() {
|
||||
// Use precise cursor position for width instead of pixel-snapped
|
||||
// `last_glyph.max_x()`, so that side-by-side galleys have the same
|
||||
// spacing as characters within a single galley.
|
||||
let paragraph_width = paragraph.cursor_x_px / pixels_per_point;
|
||||
if paragraph_width <= job.effective_wrap_width() {
|
||||
// Early-out optimization: the whole paragraph fits on one row.
|
||||
rows.push(PlacedRow {
|
||||
pos: pos2(0.0, f32::NAN),
|
||||
@@ -312,7 +319,7 @@ fn rows_from_paragraphs(
|
||||
section_index_at_start: paragraph.section_index_at_start,
|
||||
glyphs: paragraph.glyphs,
|
||||
visuals: Default::default(),
|
||||
size: vec2(paragraph_max_x, 0.0),
|
||||
size: vec2(paragraph_width, 0.0),
|
||||
}),
|
||||
ends_with_newline: !is_last_paragraph,
|
||||
});
|
||||
@@ -468,7 +475,7 @@ fn replace_last_glyph_with_overflow_character(
|
||||
let mut font_face = font.fonts_by_id.get_mut(&font_id);
|
||||
let font_face_metrics = font_face
|
||||
.as_mut()
|
||||
.map(|f| f.scaled_metrics(pixels_per_point, font_size))
|
||||
.map(|f| f.styled_metrics(pixels_per_point, font_size, §ion.format.coords))
|
||||
.unwrap_or_default();
|
||||
|
||||
let overflow_glyph_x = if let Some(prev_glyph) = row.glyphs.last() {
|
||||
@@ -495,7 +502,9 @@ fn replace_last_glyph_with_overflow_character(
|
||||
let replacement_glyph_width = font_face
|
||||
.as_mut()
|
||||
.and_then(|f| f.glyph_info(overflow_character))
|
||||
.map(|i| i.advance_width_unscaled.0 * font_face_metrics.px_scale_factor)
|
||||
.map(|i| {
|
||||
i.advance_width_unscaled.0 * font_face_metrics.px_scale_factor / pixels_per_point
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// Check if we're within width budget:
|
||||
@@ -517,7 +526,8 @@ fn replace_last_glyph_with_overflow_character(
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let font_metrics = font.scaled_metrics(pixels_per_point, font_size);
|
||||
let font_metrics =
|
||||
font.styled_metrics(pixels_per_point, font_size, §ion.format.coords);
|
||||
let line_height = section
|
||||
.format
|
||||
.line_height
|
||||
@@ -1166,6 +1176,42 @@ mod tests {
|
||||
assert_eq!(row.rect().max.x, row.glyphs.last().unwrap().max_x());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_with_pixels_per_point() {
|
||||
let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
|
||||
|
||||
for pixels_per_point in [
|
||||
0.33, 0.5, 0.67, 1.0, 1.25, 1.33, 1.5, 1.75, 2.0, 3.0, 4.0, 5.0,
|
||||
] {
|
||||
for ch in ['W', 'A', 'n', 't', 'i'] {
|
||||
let target_width = 50.0;
|
||||
let text = (0..20).map(|_| ch).collect::<String>();
|
||||
|
||||
let mut job = LayoutJob::single_section(text, TextFormat::default());
|
||||
job.wrap.max_width = target_width;
|
||||
job.wrap.max_rows = 1;
|
||||
let elided_galley = layout(&mut fonts, pixels_per_point, job.into());
|
||||
assert!(elided_galley.elided);
|
||||
|
||||
let test_galley = layout(
|
||||
&mut fonts,
|
||||
pixels_per_point,
|
||||
Arc::new(LayoutJob::single_section(
|
||||
(0..elided_galley.rows[0].char_count_excluding_newline())
|
||||
.map(|_| ch)
|
||||
.chain(std::iter::once('…'))
|
||||
.collect::<String>(),
|
||||
TextFormat::default(),
|
||||
)),
|
||||
);
|
||||
|
||||
assert!(elided_galley.size().x >= 0.0);
|
||||
assert!(elided_galley.size().x <= target_width);
|
||||
assert!(test_galley.size().x > target_width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_row() {
|
||||
let pixels_per_point = 1.0;
|
||||
@@ -1174,7 +1220,7 @@ mod tests {
|
||||
let font_id = FontId::default();
|
||||
let font_height = fonts
|
||||
.font(&font_id.family)
|
||||
.scaled_metrics(pixels_per_point, font_id.size)
|
||||
.styled_metrics(pixels_per_point, font_id.size, &VariationCoords::default())
|
||||
.row_height;
|
||||
|
||||
let job = LayoutJob::simple(String::new(), font_id, Color32::WHITE, f32::INFINITY);
|
||||
@@ -1207,7 +1253,7 @@ mod tests {
|
||||
let font_id = FontId::default();
|
||||
let font_height = fonts
|
||||
.font(&font_id.family)
|
||||
.scaled_metrics(pixels_per_point, font_id.size)
|
||||
.styled_metrics(pixels_per_point, font_id.size, &VariationCoords::default())
|
||||
.row_height;
|
||||
|
||||
let job = LayoutJob::simple("Hi!\n".to_owned(), font_id, Color32::WHITE, f32::INFINITY);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
use std::{ops::Range, str::FromStr as _};
|
||||
|
||||
use super::{
|
||||
cursor::{CCursor, LayoutCursor},
|
||||
@@ -7,6 +7,8 @@ use super::{
|
||||
};
|
||||
use crate::{Color32, FontId, Mesh, Stroke, text::FontsView};
|
||||
use emath::{Align, GuiRounding as _, NumExt as _, OrderedFloat, Pos2, Rect, Vec2, pos2, vec2};
|
||||
pub use font_types::Tag;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
/// Describes the task of laying out text.
|
||||
///
|
||||
@@ -257,6 +259,107 @@ impl std::hash::Hash for LayoutSection {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Helper trait for all types that can be parsed as a [`font_types::Tag`].
|
||||
pub trait IntoTag {
|
||||
fn into_tag(self) -> font_types::Tag;
|
||||
}
|
||||
|
||||
impl IntoTag for font_types::Tag {
|
||||
#[inline(always)]
|
||||
fn into_tag(self) -> font_types::Tag {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoTag for u32 {
|
||||
#[inline(always)]
|
||||
fn into_tag(self) -> font_types::Tag {
|
||||
font_types::Tag::from_u32(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoTag for [u8; 4] {
|
||||
#[inline(always)]
|
||||
fn into_tag(self) -> font_types::Tag {
|
||||
font_types::Tag::new_checked(&self).expect("Invalid variation axis tag")
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoTag for &[u8; 4] {
|
||||
#[inline(always)]
|
||||
fn into_tag(self) -> font_types::Tag {
|
||||
font_types::Tag::new_checked(self).expect("Invalid variation axis tag")
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoTag for &str {
|
||||
#[inline(always)]
|
||||
fn into_tag(self) -> font_types::Tag {
|
||||
font_types::Tag::from_str(self).expect("Invalid variation axis tag")
|
||||
}
|
||||
}
|
||||
|
||||
/// List of font variation coordinates by axis tag. If more than one coordinate for a given axis is provided, the last
|
||||
/// one added is used.
|
||||
#[derive(Clone, Debug, PartialEq, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct VariationCoords(SmallVec<[(font_types::Tag, f32); 2]>);
|
||||
|
||||
impl VariationCoords {
|
||||
/// Create a list of variation coordinates from a sequence of (tag, value) pairs.
|
||||
///
|
||||
/// ## Example:
|
||||
/// ```
|
||||
/// use epaint::text::VariationCoords;
|
||||
///
|
||||
/// let coords = VariationCoords::new([
|
||||
/// (b"wght", 500.0),
|
||||
/// (b"wdth", 75.0),
|
||||
/// ]);
|
||||
/// ```
|
||||
pub fn new<T: IntoTag>(values: impl IntoIterator<Item = (T, f32)>) -> Self {
|
||||
Self(values.into_iter().map(|(t, c)| (t.into_tag(), c)).collect())
|
||||
}
|
||||
|
||||
/// Add a variation coordinate to the list.
|
||||
#[inline(always)]
|
||||
pub fn push(&mut self, tag: impl IntoTag, coord: f32) {
|
||||
self.0.push((tag.into_tag(), coord));
|
||||
}
|
||||
|
||||
/// Remove the coordinate at the given index.
|
||||
pub fn remove(&mut self, index: usize) {
|
||||
self.0.remove(index);
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.0.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[(font_types::Tag, f32)]> for VariationCoords {
|
||||
#[inline(always)]
|
||||
fn as_ref(&self) -> &[(font_types::Tag, f32)] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsMut<[(font_types::Tag, f32)]> for VariationCoords {
|
||||
fn as_mut(&mut self) -> &mut [(font_types::Tag, f32)] {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for VariationCoords {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.0.len().hash(state);
|
||||
for (tag, coord) in &self.0 {
|
||||
tag.hash(state);
|
||||
OrderedFloat(*coord).hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Formatting option for a section of text.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
@@ -287,6 +390,8 @@ pub struct TextFormat {
|
||||
/// Default: 1.0
|
||||
pub expand_bg: f32,
|
||||
|
||||
pub coords: VariationCoords,
|
||||
|
||||
pub italics: bool,
|
||||
|
||||
pub underline: Stroke,
|
||||
@@ -315,6 +420,7 @@ impl Default for TextFormat {
|
||||
color: Color32::GRAY,
|
||||
background: Color32::TRANSPARENT,
|
||||
expand_bg: 1.0,
|
||||
coords: VariationCoords::default(),
|
||||
italics: false,
|
||||
underline: Stroke::NONE,
|
||||
strikethrough: Stroke::NONE,
|
||||
@@ -333,6 +439,7 @@ impl std::hash::Hash for TextFormat {
|
||||
color,
|
||||
background,
|
||||
expand_bg,
|
||||
coords,
|
||||
italics,
|
||||
underline,
|
||||
strikethrough,
|
||||
@@ -346,6 +453,7 @@ impl std::hash::Hash for TextFormat {
|
||||
color.hash(state);
|
||||
background.hash(state);
|
||||
emath::OrderedFloat(*expand_bg).hash(state);
|
||||
coords.hash(state);
|
||||
italics.hash(state);
|
||||
underline.hash(state);
|
||||
strikethrough.hash(state);
|
||||
@@ -934,12 +1042,12 @@ impl Galley {
|
||||
}
|
||||
|
||||
/// Returns a 0-width Rect.
|
||||
fn pos_from_layout_cursor(&self, layout_cursor: &LayoutCursor) -> Rect {
|
||||
pub fn pos_from_layout_cursor(&self, layout_cursor: &LayoutCursor) -> Rect {
|
||||
let Some(row) = self.rows.get(layout_cursor.row) else {
|
||||
return self.end_pos();
|
||||
};
|
||||
|
||||
let x = row.x_offset(layout_cursor.column);
|
||||
let x = row.x_offset(layout_cursor.column) + row.pos.x - self.rect.left();
|
||||
Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y()))
|
||||
}
|
||||
|
||||
@@ -984,7 +1092,7 @@ impl Galley {
|
||||
if is_pos_within_row || y_dist < best_y_dist {
|
||||
best_y_dist = y_dist;
|
||||
// char_at is `Row` not `PlacedRow` relative which means we have to subtract the pos.
|
||||
let column = row.char_at(pos.x - row.pos.x);
|
||||
let column = row.char_at(pos.x - row.pos.x + self.rect.left());
|
||||
let prefer_next_row = column < row.char_count_excluding_newline();
|
||||
cursor = CCursor {
|
||||
index: ccursor_index + column,
|
||||
|
||||
Reference in New Issue
Block a user