Prune commentary and stale Rust port notes
This commit is contained in:
1 parent
3ae034a47b
commit
1e6d3b1edd
84 files changed
+334
-5648
No files matched your search
+244
-409
@@ -12,40 +12,13 @@ use swash::{
|
||||
zeno::{Format, Vector},
|
||||
};
|
||||
|
||||
/// The icon font iris ships: the Nerd Fonts Symbols **Mono** subset built
|
||||
/// by `iris/core/build-icon-font.sh`, holding only the codepoints
|
||||
/// `crate::icon` names (992 bytes for three glyphs today).
|
||||
///
|
||||
/// This is the one font bundled here, and it is not a text font: body and
|
||||
/// monospace text still come from the platform's own collection
|
||||
/// (decided 2026-09-07). An icon is the opposite case -- a small,
|
||||
/// closed set of codepoints no system font is guaranteed to have -- which
|
||||
/// is the same division the Compose app makes.
|
||||
const NERD_ICONS: &[u8] = include_bytes!("../../assets/fonts/nerd_icons.ttf");
|
||||
|
||||
/// What starting up found about text rendering, for the on-screen
|
||||
/// Diagnostics page and the one startup log line (RUST.md's P0 box, "log
|
||||
/// once at startup ... the number of font families found, the default
|
||||
/// family resolved"). Built once by `TextData::font_diagnostics` --
|
||||
/// `Default::default` still exists for callers (tests, examples) that
|
||||
/// don't need the report.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FontDiagnostics {
|
||||
/// `Collection::family_names().count()` after registering the bundled
|
||||
/// fonts -- system families plus the two bundled ones.
|
||||
pub families_found: usize,
|
||||
/// The family `GenericFamily::SansSerif` resolves to first -- the
|
||||
/// bundled "Noto Sans" unless registration itself failed.
|
||||
pub default_family: Option<String>,
|
||||
/// The family `GenericFamily::Monospace` resolves to first.
|
||||
pub default_mono_family: Option<String>,
|
||||
/// One resolved family name per style axis this crate actually uses
|
||||
/// (`SpanStyle::bold`/`italic`), so a report can say plainly whether a
|
||||
/// bold/italic request is landing on a real face rather than being
|
||||
/// silently absorbed by whatever the sans-serif default resolves to
|
||||
/// for every weight (RUST.md's P0 box, "bold words render as blank
|
||||
/// gaps" -- a family that resolves but has no distinct bold face is
|
||||
/// exactly what produced that).
|
||||
pub regular_resolved: Option<String>,
|
||||
pub bold_resolved: Option<String>,
|
||||
pub italic_resolved: Option<String>,
|
||||
@@ -57,8 +30,6 @@ pub struct FontDiagnostics {
|
||||
pub icon_family: Option<String>,
|
||||
}
|
||||
|
||||
/// Everything text needs that outlives one string: the font collection, the
|
||||
/// layout scratch space, the glyph rasteriser and the atlas they fill.
|
||||
pub struct TextData {
|
||||
pub font_cx: FontContext,
|
||||
pub layout_cx: LayoutContext<UiColor>,
|
||||
@@ -84,19 +55,6 @@ pub struct TextData {
|
||||
}
|
||||
|
||||
impl Default for TextData {
|
||||
/// Text comes entirely from the platform's own font collection --
|
||||
/// `FontContext::new()` builds a `fontique::Collection` with
|
||||
/// `CollectionOptions::system_fonts` on by default, which is real
|
||||
/// discovery on both targets this crate ships on: Android's backend
|
||||
/// parses `/system/fonts` and `/system/etc/fonts.xml` and maps
|
||||
/// `SansSerif`/`SystemUi` to `["Roboto Flex", "Roboto", "Noto Sans"]`
|
||||
/// and `Monospace` to the platform's `"monospace"` alias; the desktop
|
||||
/// build's backend is fontconfig. No font is bundled or registered
|
||||
/// here -- see the 2026-09-07 decision for why (matching what
|
||||
/// the Compose app does: it takes body/monospace text from
|
||||
/// `FontFamily.Default`/`FontFamily.Monospace`, i.e. Android's Roboto
|
||||
/// and its platform monospace face, and ships no text font of its own,
|
||||
/// only its committed Nerd Fonts icon subset for fixed glyphs).
|
||||
fn default() -> Self {
|
||||
let mut font_cx = FontContext::new();
|
||||
patch_android_monospace(&mut font_cx);
|
||||
@@ -112,16 +70,6 @@ impl Default for TextData {
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers the bundled icon font as an ordinary named family and
|
||||
/// answers the name it registered under -- read back from the collection
|
||||
/// rather than written down here, so the name cannot drift from the file
|
||||
/// (`build-icon-font.sh` takes whatever face the Nerd Fonts release
|
||||
/// ships).
|
||||
///
|
||||
/// A *named* family rather than a generic one: nothing should fall back
|
||||
/// to it for ordinary text, and nothing should fall back out of it for an
|
||||
/// icon -- a system face that happens to have one of these codepoints
|
||||
/// would draw somebody else's picture.
|
||||
fn register_icon_font(font_cx: &mut FontContext) -> Option<String> {
|
||||
let blob = Blob::new(Arc::new(NERD_ICONS));
|
||||
let id = font_cx
|
||||
@@ -133,24 +81,6 @@ fn register_icon_font(font_cx: &mut FontContext) -> Option<String> {
|
||||
font_cx.collection.family_name(id).map(str::to_string)
|
||||
}
|
||||
|
||||
/// Works around `fontique` 0.11.1's Android backend never resolving
|
||||
/// `GenericFamily::Monospace` (confirmed against
|
||||
/// `fontique-0.11.1/src/backend/android.rs`'s `SystemFonts::new`, and still
|
||||
/// present on `linebender/parley`'s `main` as of 2026-09-07, so there is no
|
||||
/// released fix to bump to yet -- see the 2026-09-07 decision,
|
||||
/// "Platform fonts," for the full account). Two bugs stack, not one:
|
||||
/// `DEFAULT_GENERIC_FAMILIES` looks up the name `"monospace"` *before*
|
||||
/// `fonts.xml` is parsed into that same name map, and even after parsing,
|
||||
/// AOSP's `fonts.xml` names it with a `<family name="monospace">` element
|
||||
/// (not an `<alias>`) whose `<font>` children the backend's own parser
|
||||
/// does not read (a `TODO` in that match arm) -- so the name gets a
|
||||
/// `FamilyId` with no font data behind it, and `family_by_name("monospace")`
|
||||
/// comes back empty too. Confirmed on this checkout's emulator: `adb pull
|
||||
/// /system/etc/fonts.xml` shows
|
||||
/// `<family name="monospace"><font weight="400"
|
||||
/// style="normal">DroidSansMono.ttf</font></family>` with no matching
|
||||
/// alias.
|
||||
///
|
||||
/// So this reads `fonts.xml` itself (already on-device, already the
|
||||
/// authority Compose's own `Typeface.MONOSPACE` resolves through) for the
|
||||
/// filename that declaration names, then finds which of fontique's
|
||||
@@ -204,13 +134,6 @@ fn patch_android_monospace(font_cx: &mut FontContext) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads the font filename `fonts.xml` names for its `"monospace"` family
|
||||
/// (e.g. `"DroidSansMono.ttf"`), by plain substring search rather than a
|
||||
/// real XML parser -- a new dependency for one well-known, stable AOSP file
|
||||
/// whose structure fontique itself already parses with a full parser one
|
||||
/// module over. Not a general XML reader; assumes the file has exactly one
|
||||
/// `<family name="monospace">` element with at least one `<font>` child,
|
||||
/// which is the format on every AOSP `fonts.xml` this was checked against.
|
||||
#[cfg(target_os = "android")]
|
||||
fn android_monospace_font_filename() -> Option<String> {
|
||||
let android_root = std::env::var("ANDROID_ROOT").unwrap_or_else(|_| "/system".to_string());
|
||||
@@ -231,9 +154,6 @@ fn android_monospace_font_filename() -> Option<String> {
|
||||
fn patch_android_monospace(_font_cx: &mut FontContext) {}
|
||||
|
||||
impl TextData {
|
||||
/// [`Family::Icons`] as the name the bundled font actually registered
|
||||
/// under; everything else unchanged.
|
||||
///
|
||||
/// Cloned rather than borrowed because the caller needs it while the
|
||||
/// layout builder holds `&mut self` -- a `String` per shaped icon run,
|
||||
/// paid only when the layout is rebuilt.
|
||||
@@ -247,9 +167,6 @@ impl TextData {
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the startup report -- see `FontDiagnostics`. Queries the
|
||||
/// collection directly (`fontique::Query`) rather than shaping a real
|
||||
/// string, since all that's needed is which family each axis lands on.
|
||||
pub fn font_diagnostics(&mut self) -> FontDiagnostics {
|
||||
use parley::fontique::{Attributes, FontWidth, QueryStatus};
|
||||
let families_found = self.font_cx.collection.family_names().count();
|
||||
@@ -268,11 +185,6 @@ impl TextData {
|
||||
let default_mono_family = default_mono_family_id
|
||||
.and_then(|id| self.font_cx.collection.family_name(id).map(str::to_string));
|
||||
|
||||
// Resolves the family a (generic family, weight, style) query lands
|
||||
// on, without holding the `Query`'s borrow of `collection` across
|
||||
// the `family_name` lookup that needs it back -- the `FamilyId` is
|
||||
// captured out of the closure first, then looked up once `query`
|
||||
// (and its borrow) has been dropped.
|
||||
let mut resolve_family =
|
||||
|generic: GenericFamily, weight: FontWeight, style: FontStyle| -> Option<String> {
|
||||
let mut family_id = None;
|
||||
@@ -327,285 +239,7 @@ impl TextData {
|
||||
icon_family: self.icon_family.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Which family to ask for. Kept as an owned name rather than parley's
|
||||
/// borrowed `FontFamily<'_>` so that a widget can hold one without a lifetime.
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum Family {
|
||||
SansSerif,
|
||||
Serif,
|
||||
Monospace,
|
||||
/// The bundled icon font -- see [`crate::icon`] for what is in it.
|
||||
/// Named as an intention rather than as a font name because only
|
||||
/// [`TextData`] knows what the file registered as; it resolves this
|
||||
/// during shaping ([`TextData::resolve_family`]).
|
||||
Icons,
|
||||
Named(String),
|
||||
}
|
||||
|
||||
impl Family {
|
||||
fn family(&self) -> FontFamily<'_> {
|
||||
let name = match self {
|
||||
Self::SansSerif => FontFamilyName::Generic(GenericFamily::SansSerif),
|
||||
Self::Serif => FontFamilyName::Generic(GenericFamily::Serif),
|
||||
Self::Monospace => FontFamilyName::Generic(GenericFamily::Monospace),
|
||||
// Only reachable if `resolve_family` did not run, which no
|
||||
// shaping path allows -- and sans-serif is the honest answer
|
||||
// for a build whose icon font failed to register: the reader
|
||||
// gets the platform's own tofu rather than a wrong picture.
|
||||
Self::Icons => FontFamilyName::Generic(GenericFamily::SansSerif),
|
||||
Self::Named(name) => FontFamilyName::Named(name.as_str().into()),
|
||||
};
|
||||
FontFamily::Single(name)
|
||||
}
|
||||
}
|
||||
|
||||
/// One styled run inside a `TextBuffer`, overriding `TextAttrs`' base style
|
||||
/// over `range` (a byte range into the buffer's text). Every field is
|
||||
/// optional so a span only says what it changes -- e.g. a link span sets
|
||||
/// `color` and `underline` and leaves weight/family at the paragraph's own
|
||||
/// default. This is I5's answer to RUST.md's inline-rich-text ceiling
|
||||
/// (`masonry/src/widgets/text_area.rs`'s `StyleSet` is one style for the
|
||||
/// whole editor, with `// TODO: RichTextInput` beside it): parley's own
|
||||
/// `RangedBuilder::push` already takes a style and a range, so per-span
|
||||
/// bold/italic/monospace/colour/underline only needed plumbing this struct
|
||||
/// through to it and giving each glyph its own colour at draw time (see
|
||||
/// `PlacedGlyph::color` and `TextData::place` below) instead of the one
|
||||
/// `RenderedText::color` every glyph used to share.
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct SpanStyle {
|
||||
pub range: Range<usize>,
|
||||
pub color: Option<UiColor>,
|
||||
pub family: Option<Family>,
|
||||
/// Overrides `TextAttrs::font_size` for just this range -- what lets a
|
||||
/// heading inside a transcript row's single `TextEdit` be bigger than
|
||||
/// the paragraph text around it, so a whole markdown-folded row (block
|
||||
/// and inline styling both) can stay one selectable text buffer instead
|
||||
/// of one widget per block.
|
||||
pub font_size: Option<f32>,
|
||||
pub bold: bool,
|
||||
pub italic: bool,
|
||||
pub underline: bool,
|
||||
}
|
||||
|
||||
impl SpanStyle {
|
||||
pub fn new(range: Range<usize>) -> Self {
|
||||
Self {
|
||||
range,
|
||||
color: None,
|
||||
family: None,
|
||||
font_size: None,
|
||||
bold: false,
|
||||
italic: false,
|
||||
underline: false,
|
||||
}
|
||||
}
|
||||
pub fn color(mut self, color: UiColor) -> Self {
|
||||
self.color = Some(color);
|
||||
self
|
||||
}
|
||||
pub fn family(mut self, family: Family) -> Self {
|
||||
self.family = Some(family);
|
||||
self
|
||||
}
|
||||
pub fn font_size(mut self, size: f32) -> Self {
|
||||
self.font_size = Some(size);
|
||||
self
|
||||
}
|
||||
pub fn bold(mut self) -> Self {
|
||||
self.bold = true;
|
||||
self
|
||||
}
|
||||
pub fn italic(mut self) -> Self {
|
||||
self.italic = true;
|
||||
self
|
||||
}
|
||||
pub fn underline(mut self) -> Self {
|
||||
self.underline = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct TextAttrs {
|
||||
pub color: UiColor,
|
||||
pub font_size: f32,
|
||||
pub line_height: f32,
|
||||
pub family: Family,
|
||||
pub wrap: bool,
|
||||
/// inner alignment of text region (within where it's drawn)
|
||||
pub align: RegionAlign,
|
||||
}
|
||||
|
||||
pub const LINE_HEIGHT_MULT: f32 = 1.1;
|
||||
|
||||
impl Default for TextAttrs {
|
||||
fn default() -> Self {
|
||||
let size = 16.0;
|
||||
Self {
|
||||
color: UiColor::WHITE,
|
||||
font_size: size,
|
||||
line_height: size * LINE_HEIGHT_MULT,
|
||||
family: Family::SansSerif,
|
||||
wrap: false,
|
||||
align: Align::CENTER_LEFT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A string together with its laid-out form.
|
||||
///
|
||||
/// The text and the layout live in one place because parley's `Layout` borrows
|
||||
/// nothing but is only meaningful against the string it was built from: keeping
|
||||
/// them apart is how they get out of step.
|
||||
pub struct TextBuffer {
|
||||
text: String,
|
||||
layout: Layout<UiColor>,
|
||||
spans: Vec<SpanStyle>,
|
||||
/// What the current layout was built for, so `shape` can decline to redo
|
||||
/// work that would come out the same. Spans are not part of this key --
|
||||
/// `set_spans` forces `shaped` to `None` directly, the same way `edit`
|
||||
/// does, since spans change far less often than a naive equality check
|
||||
/// on the whole `Vec` would cost to compute every frame.
|
||||
shaped: Option<(TextAttrs, Option<f32>, f32)>,
|
||||
}
|
||||
|
||||
impl TextBuffer {
|
||||
pub fn new(text: impl Into<String>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
layout: Layout::new(),
|
||||
spans: Vec::new(),
|
||||
shaped: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace this buffer's per-range style overrides (I5's rich text --
|
||||
/// see `SpanStyle`). Invalidates the layout unconditionally, mirroring
|
||||
/// `set_text`.
|
||||
pub fn set_spans(&mut self, spans: Vec<SpanStyle>) {
|
||||
self.spans = spans;
|
||||
self.shaped = None;
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self::new("")
|
||||
}
|
||||
|
||||
pub fn text(&self) -> &str {
|
||||
&self.text
|
||||
}
|
||||
|
||||
pub fn layout(&self) -> &Layout<UiColor> {
|
||||
&self.layout
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.text.is_empty()
|
||||
}
|
||||
|
||||
pub fn set_text(&mut self, text: impl Into<String>) {
|
||||
let text = text.into();
|
||||
if text != self.text {
|
||||
self.text = text;
|
||||
self.shaped = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Edit the string in place; invalidates the layout unconditionally, since
|
||||
/// the caller is assumed to have changed something.
|
||||
pub fn edit(&mut self) -> &mut String {
|
||||
self.shaped = None;
|
||||
&mut self.text
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Vec2 {
|
||||
Vec2::new(self.layout.width(), self.layout.height())
|
||||
}
|
||||
|
||||
/// Lay the text out, unless it is already laid out for these
|
||||
/// attributes, this width and this density.
|
||||
///
|
||||
/// **`attrs.font_size`/`line_height` and every span's own `font_size`
|
||||
/// are density-independent (dp) units, multiplied by `density` here --
|
||||
/// the one place text crosses from the widget tree's dp sizes into the
|
||||
/// physical pixels the shaper and rasteriser (`TextData::place`) both
|
||||
/// then work in.** This is what makes glyphs sharp on a dense display:
|
||||
/// before this existed, `font_size` was already a physical-pixel value
|
||||
/// (RUST.md's P0 box's global-scale stopgap resolved density by
|
||||
/// stretching the whole rendered frame afterward instead), so a glyph
|
||||
/// was rasterised small and then upscaled by whatever the display's
|
||||
/// scale factor was -- exactly the blur Iris's report described.
|
||||
/// Multiplying here instead means the font size hitting `ScaleContext`
|
||||
/// in `place` below is already the display's real physical size, so
|
||||
/// the atlas holds a bitmap at the resolution it is actually shown at.
|
||||
/// `GlyphKey.size` already keys on that resolved `font_size`
|
||||
/// (`(font_size * 16.0).round()`), so a cache entry is naturally per
|
||||
/// physical size with no change needed there.
|
||||
pub fn shape(
|
||||
&mut self,
|
||||
data: &mut TextData,
|
||||
attrs: &TextAttrs,
|
||||
width: Option<f32>,
|
||||
density: f32,
|
||||
) {
|
||||
if self.shaped.as_ref() == Some(&(attrs.clone(), width, density)) {
|
||||
return;
|
||||
}
|
||||
// Resolved before the builder borrows `data`: `Family::Icons`
|
||||
// names an intention, and the name behind it lives on `TextData`.
|
||||
let base_family = data.resolve_family(&attrs.family);
|
||||
let span_families: Vec<Option<Family>> = self
|
||||
.spans
|
||||
.iter()
|
||||
.map(|span| span.family.as_ref().map(|f| data.resolve_family(f)))
|
||||
.collect();
|
||||
let mut builder = data
|
||||
.layout_cx
|
||||
.ranged_builder(&mut data.font_cx, &self.text, 1.0, true);
|
||||
builder.push_default(StyleProperty::FontFamily(base_family.family()));
|
||||
builder.push_default(StyleProperty::FontSize(attrs.font_size * density));
|
||||
builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute(
|
||||
attrs.line_height * density,
|
||||
)));
|
||||
builder.push_default(StyleProperty::Brush(attrs.color));
|
||||
for (span, family) in self.spans.iter().zip(&span_families) {
|
||||
let range = span.range.clone();
|
||||
if let Some(color) = span.color {
|
||||
builder.push(StyleProperty::Brush(color), range.clone());
|
||||
}
|
||||
if let Some(family) = family {
|
||||
builder.push(StyleProperty::FontFamily(family.family()), range.clone());
|
||||
}
|
||||
if let Some(size) = span.font_size {
|
||||
builder.push(StyleProperty::FontSize(size * density), range.clone());
|
||||
}
|
||||
if span.bold {
|
||||
builder.push(StyleProperty::FontWeight(FontWeight::BOLD), range.clone());
|
||||
}
|
||||
if span.italic {
|
||||
builder.push(StyleProperty::FontStyle(FontStyle::Italic), range.clone());
|
||||
}
|
||||
if span.underline {
|
||||
builder.push(StyleProperty::Underline(true), range.clone());
|
||||
}
|
||||
}
|
||||
builder.build_into(&mut self.layout, &self.text);
|
||||
self.layout.break_all_lines(width);
|
||||
self.layout
|
||||
.align(Alignment::Start, AlignmentOptions::default());
|
||||
self.shaped = Some((attrs.clone(), width, density));
|
||||
}
|
||||
}
|
||||
|
||||
impl TextData {
|
||||
/// Rasterise whatever of `buffer` is not in the atlas yet, and return where
|
||||
/// each glyph goes relative to the text's top-left.
|
||||
///
|
||||
/// Nothing is uploaded for a glyph already in the atlas, which is the point
|
||||
/// of having one: a resize re-runs this and touches the GPU only if the new
|
||||
/// width brought genuinely new glyphs into view.
|
||||
pub fn place(&mut self, buffer: &TextBuffer, textures: &mut Textures) -> Vec<PlacedGlyph> {
|
||||
let mut placed = Vec::new();
|
||||
for line in buffer.layout.lines() {
|
||||
@@ -622,8 +256,6 @@ impl TextData {
|
||||
continue;
|
||||
};
|
||||
let coords_hash = hash_coords(coords);
|
||||
// `font.data.id()` rather than the pointer, so the same font
|
||||
// loaded twice is still one set of entries.
|
||||
let font_id = font.data.id();
|
||||
|
||||
for glyph in run.positioned_glyphs() {
|
||||
@@ -676,41 +308,7 @@ impl TextData {
|
||||
}
|
||||
placed
|
||||
}
|
||||
}
|
||||
|
||||
fn hash_coords(coords: &[i16]) -> u64 {
|
||||
// FxHash over the coordinates; they are short and change rarely.
|
||||
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
|
||||
for c in coords {
|
||||
h ^= *c as u16 as u64;
|
||||
h = h.wrapping_mul(0x1000_0000_01b3);
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
/// A laid-out string, ready to draw: where each glyph goes and how big the
|
||||
/// whole thing is.
|
||||
///
|
||||
/// Cheap to clone and to keep, which is the point -- a widget holds one across
|
||||
/// frames and re-emits its quads without going near the rasteriser. `color`
|
||||
/// is the buffer's *base* colour (`TextAttrs::color`) for a caller that wants
|
||||
/// it as a whole (e.g. tinting a cursor to match); the colour each glyph is
|
||||
/// actually drawn in is `PlacedGlyph::color`, which a `SpanStyle` can
|
||||
/// override per range.
|
||||
#[derive(Clone)]
|
||||
pub struct RenderedText {
|
||||
pub glyphs: std::sync::Arc<Vec<PlacedGlyph>>,
|
||||
pub size: Vec2,
|
||||
pub color: UiColor,
|
||||
/// The [`GlyphAtlas::generation`] the glyphs above were placed against.
|
||||
/// A holder must re-render rather than re-emit these quads once the
|
||||
/// atlas has moved on (`GlyphAtlas::clear`'s doc says what happens
|
||||
/// otherwise); `Painter::glyphs` debug-asserts it.
|
||||
pub generation: u64,
|
||||
}
|
||||
|
||||
impl TextData {
|
||||
/// Lay out and place in one step, which is what a widget wants.
|
||||
pub fn render(
|
||||
&mut self,
|
||||
buffer: &mut TextBuffer,
|
||||
@@ -730,16 +328,255 @@ impl TextData {
|
||||
}
|
||||
}
|
||||
|
||||
/// Which family to ask for. Kept as an owned name rather than parley's
|
||||
/// borrowed `FontFamily<'_>` so that a widget can hold one without a lifetime.
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum Family {
|
||||
SansSerif,
|
||||
Serif,
|
||||
Monospace,
|
||||
/// The bundled icon font -- see [`crate::icon`] for what is in it.
|
||||
/// Named as an intention rather than as a font name because only
|
||||
/// [`TextData`] knows what the file registered as; it resolves this
|
||||
/// during shaping ([`TextData::resolve_family`]).
|
||||
Icons,
|
||||
Named(String),
|
||||
}
|
||||
|
||||
impl Family {
|
||||
fn family(&self) -> FontFamily<'_> {
|
||||
let name = match self {
|
||||
Self::SansSerif => FontFamilyName::Generic(GenericFamily::SansSerif),
|
||||
Self::Serif => FontFamilyName::Generic(GenericFamily::Serif),
|
||||
Self::Monospace => FontFamilyName::Generic(GenericFamily::Monospace),
|
||||
Self::Icons => FontFamilyName::Generic(GenericFamily::SansSerif),
|
||||
Self::Named(name) => FontFamilyName::Named(name.as_str().into()),
|
||||
};
|
||||
FontFamily::Single(name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct SpanStyle {
|
||||
pub range: Range<usize>,
|
||||
pub color: Option<UiColor>,
|
||||
pub family: Option<Family>,
|
||||
pub font_size: Option<f32>,
|
||||
pub bold: bool,
|
||||
pub italic: bool,
|
||||
pub underline: bool,
|
||||
}
|
||||
|
||||
impl SpanStyle {
|
||||
pub fn new(range: Range<usize>) -> Self {
|
||||
Self {
|
||||
range,
|
||||
color: None,
|
||||
family: None,
|
||||
font_size: None,
|
||||
bold: false,
|
||||
italic: false,
|
||||
underline: false,
|
||||
}
|
||||
}
|
||||
pub fn color(mut self, color: UiColor) -> Self {
|
||||
self.color = Some(color);
|
||||
self
|
||||
}
|
||||
pub fn family(mut self, family: Family) -> Self {
|
||||
self.family = Some(family);
|
||||
self
|
||||
}
|
||||
pub fn font_size(mut self, size: f32) -> Self {
|
||||
self.font_size = Some(size);
|
||||
self
|
||||
}
|
||||
pub fn bold(mut self) -> Self {
|
||||
self.bold = true;
|
||||
self
|
||||
}
|
||||
pub fn italic(mut self) -> Self {
|
||||
self.italic = true;
|
||||
self
|
||||
}
|
||||
pub fn underline(mut self) -> Self {
|
||||
self.underline = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct TextAttrs {
|
||||
pub color: UiColor,
|
||||
pub font_size: f32,
|
||||
pub line_height: f32,
|
||||
pub family: Family,
|
||||
pub wrap: bool,
|
||||
pub align: RegionAlign,
|
||||
}
|
||||
|
||||
pub const LINE_HEIGHT_MULT: f32 = 1.1;
|
||||
|
||||
impl Default for TextAttrs {
|
||||
fn default() -> Self {
|
||||
let size = 16.0;
|
||||
Self {
|
||||
color: UiColor::WHITE,
|
||||
font_size: size,
|
||||
line_height: size * LINE_HEIGHT_MULT,
|
||||
family: Family::SansSerif,
|
||||
wrap: false,
|
||||
align: Align::CENTER_LEFT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The text and the layout live in one place because parley's `Layout` borrows
|
||||
/// nothing but is only meaningful against the string it was built from: keeping
|
||||
/// them apart is how they get out of step.
|
||||
pub struct TextBuffer {
|
||||
text: String,
|
||||
layout: Layout<UiColor>,
|
||||
spans: Vec<SpanStyle>,
|
||||
shaped: Option<(TextAttrs, Option<f32>, f32)>,
|
||||
}
|
||||
|
||||
impl TextBuffer {
|
||||
pub fn new(text: impl Into<String>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
layout: Layout::new(),
|
||||
spans: Vec::new(),
|
||||
shaped: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_spans(&mut self, spans: Vec<SpanStyle>) {
|
||||
self.spans = spans;
|
||||
self.shaped = None;
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self::new("")
|
||||
}
|
||||
|
||||
pub fn text(&self) -> &str {
|
||||
&self.text
|
||||
}
|
||||
|
||||
pub fn layout(&self) -> &Layout<UiColor> {
|
||||
&self.layout
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.text.is_empty()
|
||||
}
|
||||
|
||||
pub fn set_text(&mut self, text: impl Into<String>) {
|
||||
let text = text.into();
|
||||
if text != self.text {
|
||||
self.text = text;
|
||||
self.shaped = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Edit the string in place; invalidates the layout unconditionally, since
|
||||
/// the caller is assumed to have changed something.
|
||||
pub fn edit(&mut self) -> &mut String {
|
||||
self.shaped = None;
|
||||
&mut self.text
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Vec2 {
|
||||
Vec2::new(self.layout.width(), self.layout.height())
|
||||
}
|
||||
|
||||
pub fn shape(
|
||||
&mut self,
|
||||
data: &mut TextData,
|
||||
attrs: &TextAttrs,
|
||||
width: Option<f32>,
|
||||
density: f32,
|
||||
) {
|
||||
if self.shaped.as_ref() == Some(&(attrs.clone(), width, density)) {
|
||||
return;
|
||||
}
|
||||
let base_family = data.resolve_family(&attrs.family);
|
||||
let span_families: Vec<Option<Family>> = self
|
||||
.spans
|
||||
.iter()
|
||||
.map(|span| span.family.as_ref().map(|f| data.resolve_family(f)))
|
||||
.collect();
|
||||
let mut builder = data
|
||||
.layout_cx
|
||||
.ranged_builder(&mut data.font_cx, &self.text, 1.0, true);
|
||||
builder.push_default(StyleProperty::FontFamily(base_family.family()));
|
||||
builder.push_default(StyleProperty::FontSize(attrs.font_size * density));
|
||||
builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute(
|
||||
attrs.line_height * density,
|
||||
)));
|
||||
builder.push_default(StyleProperty::Brush(attrs.color));
|
||||
for (span, family) in self.spans.iter().zip(&span_families) {
|
||||
let range = span.range.clone();
|
||||
if let Some(color) = span.color {
|
||||
builder.push(StyleProperty::Brush(color), range.clone());
|
||||
}
|
||||
if let Some(family) = family {
|
||||
builder.push(StyleProperty::FontFamily(family.family()), range.clone());
|
||||
}
|
||||
if let Some(size) = span.font_size {
|
||||
builder.push(StyleProperty::FontSize(size * density), range.clone());
|
||||
}
|
||||
if span.bold {
|
||||
builder.push(StyleProperty::FontWeight(FontWeight::BOLD), range.clone());
|
||||
}
|
||||
if span.italic {
|
||||
builder.push(StyleProperty::FontStyle(FontStyle::Italic), range.clone());
|
||||
}
|
||||
if span.underline {
|
||||
builder.push(StyleProperty::Underline(true), range.clone());
|
||||
}
|
||||
}
|
||||
builder.build_into(&mut self.layout, &self.text);
|
||||
self.layout.break_all_lines(width);
|
||||
self.layout
|
||||
.align(Alignment::Start, AlignmentOptions::default());
|
||||
self.shaped = Some((attrs.clone(), width, density));
|
||||
}
|
||||
}
|
||||
|
||||
fn hash_coords(coords: &[i16]) -> u64 {
|
||||
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
|
||||
for c in coords {
|
||||
h ^= *c as u16 as u64;
|
||||
h = h.wrapping_mul(0x1000_0000_01b3);
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
/// Cheap to clone and to keep, which is the point -- a widget holds one across
|
||||
/// frames and re-emits its quads without going near the rasteriser. `color`
|
||||
/// is the buffer's *base* colour (`TextAttrs::color`) for a caller that wants
|
||||
/// it as a whole (e.g. tinting a cursor to match); the colour each glyph is
|
||||
/// actually drawn in is `PlacedGlyph::color`, which a `SpanStyle` can
|
||||
/// override per range.
|
||||
#[derive(Clone)]
|
||||
pub struct RenderedText {
|
||||
pub glyphs: std::sync::Arc<Vec<PlacedGlyph>>,
|
||||
pub size: Vec2,
|
||||
pub color: UiColor,
|
||||
/// The [`GlyphAtlas::generation`] the glyphs above were placed against.
|
||||
/// A holder must re-render rather than re-emit these quads once the
|
||||
/// atlas has moved on (`GlyphAtlas::clear`'s doc says what happens
|
||||
/// otherwise); `Painter::glyphs` debug-asserts it.
|
||||
pub generation: u64,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::icon;
|
||||
|
||||
/// Every codepoint `icon` names is actually in the subset the script
|
||||
/// built. This is the failure `build-icon-font.sh`'s own comment warns
|
||||
/// about -- a constant added on one side and not the other is a glyph
|
||||
/// that silently isn't there -- and it is invisible at runtime,
|
||||
/// because a missing glyph draws as nothing rather than as an error.
|
||||
#[test]
|
||||
fn every_icon_is_in_the_bundled_font() {
|
||||
let font = FontRef::from_index(NERD_ICONS, 0).expect("the bundled icon font parses");
|
||||
@@ -762,8 +599,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The font registers, so `Family::Icons` resolves to a real family
|
||||
/// rather than falling through to sans-serif and drawing tofu.
|
||||
#[test]
|
||||
fn the_icon_family_registers_and_resolves() {
|
||||
let data = TextData::default();
|
||||
|
||||
Reference in new issue
Block a user