feat: llimphi standalone — framework UI soberano extraído del monorepo
Motor gráfico Llimphi como workspace independiente: bucle Elm (input→update→view→layout→raster→present) sobre wgpu+vello+taffy+parley. Núcleo (hal/raster/layout/text/ui/theme/surface/motion/icons) + ~40 widgets + módulos, sin dependencias al resto del monorepo. cargo check --workspace pasa (64 crates). Puerta de entrada: cargo run -p llimphi-ui --example counter. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "llimphi-text"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
# vello directo (no llimphi-raster): el motor de texto sólo necesita
|
||||
# Scene/peniko/kurbo para construir y pintar layouts — nada del Renderer ni
|
||||
# de llimphi-hal. Eso mantiene llimphi-text (y quien lo use: el compositor)
|
||||
# libre de winit, condición para correr sobre el framebuffer de wawa.
|
||||
[dependencies]
|
||||
vello = { workspace = true }
|
||||
parley = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
llimphi-raster = { path = "../llimphi-raster" }
|
||||
llimphi-hal = { path = "../llimphi-hal" }
|
||||
pollster = { workspace = true }
|
||||
|
||||
[[example]]
|
||||
name = "hello_text"
|
||||
path = "examples/hello_text.rs"
|
||||
@@ -0,0 +1,9 @@
|
||||
# llimphi-text
|
||||
|
||||
> Shaping + fonts de [llimphi](../README.md).
|
||||
|
||||
Capa de tipografía. Fontdue para subset minimal; HarfBuzz cuando se requiere shaping complejo (árabe, devanagari, ligaduras). Cache de glyphs rasterizados; medición precisa para layout (`measure(text, font, size) → (w, h)`).
|
||||
|
||||
## Deps
|
||||
|
||||
- `fontdue`, `harfbuzz_rs` (feature)
|
||||
@@ -0,0 +1,9 @@
|
||||
# llimphi-text
|
||||
|
||||
> Shaping + fonts of [llimphi](../README.md).
|
||||
|
||||
Typography layer. Fontdue for minimal subset; HarfBuzz when complex shaping is required (Arabic, Devanagari, ligatures). Cache of rasterized glyphs; precise measurement for layout (`measure(text, font, size) → (w, h)`).
|
||||
|
||||
## Deps
|
||||
|
||||
- `fontdue`, `harfbuzz_rs` (feature)
|
||||
Binary file not shown.
@@ -0,0 +1,167 @@
|
||||
//! Texto via parley sobre vello: párrafo wrappeable + shaping (kerning,
|
||||
//! ligatures, bidi, fallback CJK/emoji).
|
||||
//!
|
||||
//! Corre con: `cargo run -p llimphi-text --example hello_text --release`.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use llimphi_hal::winit::application::ApplicationHandler;
|
||||
use llimphi_hal::winit::dpi::LogicalSize;
|
||||
use llimphi_hal::winit::event::WindowEvent;
|
||||
use llimphi_hal::winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||
use llimphi_hal::winit::window::{Window, WindowAttributes, WindowId};
|
||||
use llimphi_hal::{Hal, Surface, WinitSurface};
|
||||
use llimphi_text::peniko::{color::palette, Color};
|
||||
use llimphi_text::{draw_block, Alignment, TextBlock, Typesetter};
|
||||
|
||||
const PARRAFO: &str = "Llimphi pinta vector preciso sobre el silicio: \
|
||||
geometrías exactas, sin cajas negras. شكراً 你好 — el shaping de parley \
|
||||
maneja kerning, ligaduras y fallback CJK/Arabic en la misma línea.";
|
||||
|
||||
struct State {
|
||||
window: Arc<Window>,
|
||||
hal: Hal,
|
||||
surface: WinitSurface,
|
||||
renderer: llimphi_raster::Renderer,
|
||||
scene: llimphi_raster::vello::Scene,
|
||||
typesetter: Typesetter,
|
||||
}
|
||||
|
||||
struct App {
|
||||
state: Option<State>,
|
||||
}
|
||||
|
||||
impl ApplicationHandler for App {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if self.state.is_some() {
|
||||
return;
|
||||
}
|
||||
let window = event_loop
|
||||
.create_window(
|
||||
WindowAttributes::default()
|
||||
.with_title("llimphi · hello_text")
|
||||
.with_inner_size(LogicalSize::new(960u32, 540u32)),
|
||||
)
|
||||
.expect("create window");
|
||||
let window = Arc::new(window);
|
||||
let hal = pollster::block_on(Hal::new(None)).expect("hal");
|
||||
let surface = WinitSurface::new(&hal, window.clone()).expect("surface");
|
||||
let renderer = llimphi_raster::Renderer::new(&hal).expect("renderer");
|
||||
let typesetter = Typesetter::new();
|
||||
window.request_redraw();
|
||||
self.state = Some(State {
|
||||
window,
|
||||
hal,
|
||||
surface,
|
||||
renderer,
|
||||
scene: llimphi_raster::vello::Scene::new(),
|
||||
typesetter,
|
||||
});
|
||||
}
|
||||
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
_id: WindowId,
|
||||
event: WindowEvent,
|
||||
) {
|
||||
let Some(state) = self.state.as_mut() else {
|
||||
return;
|
||||
};
|
||||
match event {
|
||||
WindowEvent::CloseRequested => event_loop.exit(),
|
||||
WindowEvent::Resized(size) => {
|
||||
state.surface.resize(size.width, size.height);
|
||||
state.window.request_redraw();
|
||||
}
|
||||
WindowEvent::RedrawRequested => {
|
||||
let frame = match state.surface.acquire() {
|
||||
Ok(f) => f,
|
||||
Err(_) => {
|
||||
let (w, h) = state.surface.size();
|
||||
state.surface.resize(w, h);
|
||||
state.window.request_redraw();
|
||||
return;
|
||||
}
|
||||
};
|
||||
let (w, _h) = frame.size();
|
||||
let margin_x = 64.0_f64;
|
||||
let margin_y = 64.0_f64;
|
||||
let inner_w = (w as f32 - 2.0 * margin_x as f32).max(100.0);
|
||||
state.scene.reset();
|
||||
|
||||
// Título centrado
|
||||
draw_block(
|
||||
&mut state.scene,
|
||||
&mut state.typesetter,
|
||||
&TextBlock {
|
||||
text: "Llimphi",
|
||||
size_px: 96.0,
|
||||
color: Color::from_rgba8(220, 230, 240, 255),
|
||||
origin: (margin_x, margin_y),
|
||||
max_width: Some(inner_w),
|
||||
alignment: Alignment::Center,
|
||||
line_height: 1.0,
|
||||
|
||||
italic: false,
|
||||
font_family: None,
|
||||
},
|
||||
);
|
||||
|
||||
// Subtítulo centrado
|
||||
draw_block(
|
||||
&mut state.scene,
|
||||
&mut state.typesetter,
|
||||
&TextBlock {
|
||||
text: "motor gráfico soberano · parley + vello",
|
||||
size_px: 20.0,
|
||||
color: Color::from_rgba8(140, 160, 180, 255),
|
||||
origin: (margin_x, margin_y + 110.0),
|
||||
max_width: Some(inner_w),
|
||||
alignment: Alignment::Center,
|
||||
line_height: 1.0,
|
||||
|
||||
italic: false,
|
||||
font_family: None,
|
||||
},
|
||||
);
|
||||
|
||||
// Párrafo justificado con wrap
|
||||
draw_block(
|
||||
&mut state.scene,
|
||||
&mut state.typesetter,
|
||||
&TextBlock {
|
||||
text: PARRAFO,
|
||||
size_px: 22.0,
|
||||
color: Color::from_rgba8(200, 210, 220, 255),
|
||||
origin: (margin_x, margin_y + 170.0),
|
||||
max_width: Some(inner_w),
|
||||
alignment: Alignment::Justify,
|
||||
line_height: 1.4,
|
||||
|
||||
italic: false,
|
||||
font_family: None,
|
||||
},
|
||||
);
|
||||
|
||||
if let Err(e) = state.renderer.render(
|
||||
&state.hal,
|
||||
&state.scene,
|
||||
&frame,
|
||||
palette::css::BLACK,
|
||||
) {
|
||||
eprintln!("render error: {e}");
|
||||
}
|
||||
state.surface.present(frame, &state.hal);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let event_loop = EventLoop::new().expect("event loop");
|
||||
event_loop.set_control_flow(ControlFlow::Wait);
|
||||
let mut app = App { state: None };
|
||||
event_loop.run_app(&mut app).expect("run app");
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
//! llimphi-text — Texto sobre vello vía parley.
|
||||
//!
|
||||
//! parley hace shaping completo (bidi, ligatures, kerning), line break y
|
||||
//! alineación; fontique resuelve fuentes del sistema con fallback CJK/emoji.
|
||||
//! Aquí lo envolvemos en una API mínima centrada en el caso común: un
|
||||
//! bloque de texto con color uniforme, ancho máximo opcional y alineación.
|
||||
|
||||
use vello::peniko::{Brush, Color};
|
||||
|
||||
pub use parley;
|
||||
pub use vello;
|
||||
pub use vello::peniko;
|
||||
|
||||
/// Estado compartido del motor de texto. Una instancia por proceso es lo
|
||||
/// recomendado: `FontContext` cachea la base de fuentes y `LayoutContext`
|
||||
/// reutiliza allocaciones entre layouts.
|
||||
pub struct Typesetter {
|
||||
font_cx: parley::FontContext,
|
||||
layout_cx: parley::LayoutContext<()>,
|
||||
/// Contexto separado para layouts multicolor (`Brush` por rango). El
|
||||
/// brush genérico de parley no puede ser `()` y `RunBrush` a la vez en
|
||||
/// el mismo `LayoutContext`, así que mantenemos uno por sabor.
|
||||
runs_cx: parley::LayoutContext<RunBrush>,
|
||||
}
|
||||
|
||||
impl Default for Typesetter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// DejaVu Sans embebida como **fallback universal de símbolos**. El motor
|
||||
/// confía en las fuentes del sistema vía fontique, pero muchas instalaciones
|
||||
/// (p. ej. solo Liberation/Adwaita) carecen de glyphs para flechas (`→`),
|
||||
/// formas geométricas (`● ▶`), dingbats (`✓ ✗ ✎`), avisos (`⚠`) o astro
|
||||
/// (`♈ ☉ ☽`) — y entonces parley pinta el "tofu" (□). DejaVu cubre todo ese
|
||||
/// rango; la registramos y la enganchamos al fallback del script `Common`
|
||||
/// (`Zyyy`), que es donde Unicode clasifica esos símbolos. Así cualquier app
|
||||
/// Llimphi deja de mostrar cuadrados sin tocar una línea de su código.
|
||||
/// Licencia: Bitstream Vera + Arev (libre, redistribuible).
|
||||
const DEJAVU_SANS: &[u8] = include_bytes!("../assets/DejaVuSans.ttf");
|
||||
|
||||
impl Typesetter {
|
||||
pub fn new() -> Self {
|
||||
let mut font_cx = parley::FontContext::new();
|
||||
Self::install_symbol_fallback(&mut font_cx);
|
||||
Self {
|
||||
font_cx,
|
||||
layout_cx: parley::LayoutContext::new(),
|
||||
runs_cx: parley::LayoutContext::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Registra DejaVu Sans y la apila como último recurso para los símbolos
|
||||
/// del script `Common` (flechas, geométricos, dingbats, astro…). Ver la
|
||||
/// nota de [`DEJAVU_SANS`]. Best-effort: si algo falla, el texto sigue
|
||||
/// funcionando con las fuentes del sistema (solo reaparecería el tofu).
|
||||
fn install_symbol_fallback(font_cx: &mut parley::FontContext) {
|
||||
use parley::fontique::Blob;
|
||||
let blob = Blob::new(std::sync::Arc::new(DEJAVU_SANS));
|
||||
let registered = font_cx.collection.register_fonts(blob, None);
|
||||
if let Some((family_id, _)) = registered.first() {
|
||||
// `Zyyy` (Common) es el script de la inmensa mayoría de los
|
||||
// símbolos que daban tofu; lo apilamos al final del fallback.
|
||||
font_cx
|
||||
.collection
|
||||
.append_fallbacks("Zyyy", std::iter::once(*family_id));
|
||||
}
|
||||
}
|
||||
|
||||
/// Acceso al `FontContext` por si se necesita registrar fuentes extra
|
||||
/// o cambiar la stack de fallback.
|
||||
pub fn font_context_mut(&mut self) -> &mut parley::FontContext {
|
||||
&mut self.font_cx
|
||||
}
|
||||
|
||||
/// Construye y resuelve un `parley::Layout`. Aplica `font_size`,
|
||||
/// `line_height` (multiplicador del font_size), `max_width` (line
|
||||
/// break), y `alignment`. `italic`=true selecciona la variante
|
||||
/// italic/oblique de la fuente activa (vía `parley::FontStyle`).
|
||||
pub fn layout(
|
||||
&mut self,
|
||||
text: &str,
|
||||
size_px: f32,
|
||||
max_width: Option<f32>,
|
||||
alignment: Alignment,
|
||||
line_height: f32,
|
||||
italic: bool,
|
||||
font_family: Option<&str>,
|
||||
) -> parley::Layout<()> {
|
||||
let mut builder =
|
||||
self.layout_cx
|
||||
.ranged_builder(&mut self.font_cx, text, 1.0, true);
|
||||
builder.push_default(parley::StyleProperty::FontSize(size_px));
|
||||
builder.push_default(parley::StyleProperty::LineHeight(line_height));
|
||||
if italic {
|
||||
builder.push_default(parley::StyleProperty::FontStyle(
|
||||
parley::FontStyle::Italic,
|
||||
));
|
||||
}
|
||||
if let Some(ff) = font_family {
|
||||
// parley::FontStack::Source acepta CSS-like syntax
|
||||
// (`"Helvetica", sans-serif`).
|
||||
builder.push_default(parley::StyleProperty::FontStack(
|
||||
parley::FontStack::Source(std::borrow::Cow::Borrowed(ff)),
|
||||
));
|
||||
}
|
||||
let mut layout = builder.build(text);
|
||||
layout.break_all_lines(max_width);
|
||||
layout.align(
|
||||
max_width,
|
||||
alignment.into(),
|
||||
parley::AlignmentOptions::default(),
|
||||
);
|
||||
layout
|
||||
}
|
||||
|
||||
/// Construye un layout **multicolor** en una sola pasada de shaping:
|
||||
/// `default_color` cubre todo el texto y cada `(start_byte, end_byte,
|
||||
/// color)` lo sobreescribe en su rango (offsets en **bytes**, no chars —
|
||||
/// la convención de parley). Pensado para syntax highlighting: shapear
|
||||
/// la línea entera una vez con un color por token, en vez de un layout
|
||||
/// por token. Sin wrap (`max_width = None`); el caller posiciona la línea.
|
||||
pub fn layout_runs(
|
||||
&mut self,
|
||||
text: &str,
|
||||
size_px: f32,
|
||||
default_color: Color,
|
||||
runs: &[(usize, usize, Color)],
|
||||
alignment: Alignment,
|
||||
line_height: f32,
|
||||
) -> parley::Layout<RunBrush> {
|
||||
let mut builder = self
|
||||
.runs_cx
|
||||
.ranged_builder(&mut self.font_cx, text, 1.0, true);
|
||||
builder.push_default(parley::StyleProperty::FontSize(size_px));
|
||||
builder.push_default(parley::StyleProperty::LineHeight(line_height));
|
||||
builder.push_default(parley::StyleProperty::Brush(RunBrush(default_color)));
|
||||
let len = text.len();
|
||||
for &(start, end, color) in runs {
|
||||
if start < end && end <= len {
|
||||
builder.push(parley::StyleProperty::Brush(RunBrush(color)), start..end);
|
||||
}
|
||||
}
|
||||
let mut layout = builder.build(text);
|
||||
layout.break_all_lines(None);
|
||||
layout.align(None, alignment.into(), parley::AlignmentOptions::default());
|
||||
layout
|
||||
}
|
||||
}
|
||||
|
||||
/// Brush por-run para texto multicolor. Newtype sobre [`Color`] porque
|
||||
/// parley exige que el brush genérico implemente `Default` (que `Color` no
|
||||
/// garantiza); aquí proveemos uno explícito (negro opaco) que nunca se ve
|
||||
/// en la práctica: todo run lleva su color o el `default_color` del bloque.
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
pub struct RunBrush(pub Color);
|
||||
|
||||
impl Default for RunBrush {
|
||||
fn default() -> Self {
|
||||
RunBrush(Color::from_rgba8(0, 0, 0, 255))
|
||||
}
|
||||
}
|
||||
|
||||
/// Alineación horizontal del bloque dentro de su ancho máximo.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Alignment {
|
||||
Start,
|
||||
Center,
|
||||
End,
|
||||
Justify,
|
||||
}
|
||||
|
||||
impl From<Alignment> for parley::Alignment {
|
||||
fn from(a: Alignment) -> Self {
|
||||
match a {
|
||||
Alignment::Start => parley::Alignment::Start,
|
||||
Alignment::Center => parley::Alignment::Middle,
|
||||
Alignment::End => parley::Alignment::End,
|
||||
Alignment::Justify => parley::Alignment::Justified,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Especificación de un bloque de texto a rasterizar.
|
||||
pub struct TextBlock<'a> {
|
||||
pub text: &'a str,
|
||||
pub size_px: f32,
|
||||
pub color: Color,
|
||||
/// Esquina superior-izquierda del bloque (no el baseline — parley se
|
||||
/// encarga del baseline internamente).
|
||||
pub origin: (f64, f64),
|
||||
pub max_width: Option<f32>,
|
||||
pub alignment: Alignment,
|
||||
/// Múltiplo del font_size (1.0 = compacto, 1.3 = cómodo).
|
||||
pub line_height: f32,
|
||||
/// `true` → fuerza variante italic/oblique en la fuente activa.
|
||||
pub italic: bool,
|
||||
/// CSS-style `font-family` string. `None` = sans-serif default.
|
||||
pub font_family: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> TextBlock<'a> {
|
||||
/// Constructor simple para una línea sin wrap.
|
||||
pub fn simple(text: &'a str, size_px: f32, color: Color, origin: (f64, f64)) -> Self {
|
||||
Self {
|
||||
text,
|
||||
size_px,
|
||||
color,
|
||||
origin,
|
||||
max_width: None,
|
||||
alignment: Alignment::Start,
|
||||
line_height: 1.0,
|
||||
italic: false,
|
||||
font_family: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Medidas resultantes de un layout.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Measurement {
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
/// Construye el layout (shaping + line break + alineación) listo para medir
|
||||
/// y/o pintar. Usá esta API cuando necesitás el alto **antes** de elegir el
|
||||
/// origen (p. ej. centrado vertical) y no querés repetir el shaping en el
|
||||
/// `draw`: medís sobre el layout retornado y luego lo pasás a
|
||||
/// [`draw_layout`].
|
||||
pub fn layout_block(ts: &mut Typesetter, block: &TextBlock<'_>) -> parley::Layout<()> {
|
||||
ts.layout(
|
||||
block.text,
|
||||
block.size_px,
|
||||
block.max_width,
|
||||
block.alignment,
|
||||
block.line_height,
|
||||
block.italic,
|
||||
block.font_family.as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Devuelve las medidas de un layout ya resuelto. Equivalente conceptual a
|
||||
/// `(layout.width(), layout.height())` pero envuelto en [`Measurement`].
|
||||
pub fn measurement(layout: &parley::Layout<()>) -> Measurement {
|
||||
Measurement {
|
||||
width: layout.width(),
|
||||
height: layout.height(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Pinta un layout ya resuelto en `scene` con `color` y un offset `origin`
|
||||
/// (esquina superior-izquierda del bloque). No alloca: los glifos van
|
||||
/// directo del iterador de parley al builder de vello.
|
||||
pub fn draw_layout(
|
||||
scene: &mut vello::Scene,
|
||||
layout: &parley::Layout<()>,
|
||||
color: Color,
|
||||
origin: (f64, f64),
|
||||
) {
|
||||
draw_layout_xf(scene, layout, color, vello::kurbo::Affine::translate(origin));
|
||||
}
|
||||
|
||||
/// Igual que [`draw_layout`] pero con una **afín completa** en vez de sólo un
|
||||
/// desplazamiento: permite pintar texto girado/escalado (p. ej. dentro de un
|
||||
/// marco rotado en una presentación espacial). El origen del layout (0,0) es el
|
||||
/// que mapea `transform`; las posiciones de glifo se aplican en ese espacio.
|
||||
pub fn draw_layout_xf(
|
||||
scene: &mut vello::Scene,
|
||||
layout: &parley::Layout<()>,
|
||||
color: Color,
|
||||
transform: vello::kurbo::Affine,
|
||||
) {
|
||||
draw_layout_brush_xf(scene, layout, &Brush::Solid(color), transform);
|
||||
}
|
||||
|
||||
/// Igual que [`draw_layout_xf`] pero con un [`Brush`] arbitrario en vez de un
|
||||
/// color sólido: permite rellenar los glifos con un gradiente o una imagen
|
||||
/// (p. ej. CSS `background-clip: text`). El brush se interpreta en el espacio
|
||||
/// **local** del layout (origen 0,0) y `transform` lo lleva al lugar final —
|
||||
/// así un gradiente construido en coords (0,0)-(w,h) queda alineado con los
|
||||
/// glifos. Para texto normal usá [`draw_layout_xf`] (solid = máxima compat).
|
||||
pub fn draw_layout_brush_xf(
|
||||
scene: &mut vello::Scene,
|
||||
layout: &parley::Layout<()>,
|
||||
brush: &Brush,
|
||||
transform: vello::kurbo::Affine,
|
||||
) {
|
||||
for line in layout.lines() {
|
||||
for item in line.items() {
|
||||
if let parley::PositionedLayoutItem::GlyphRun(glyph_run) = item {
|
||||
let run = glyph_run.run();
|
||||
let font = run.font().clone();
|
||||
let font_size = run.font_size();
|
||||
scene
|
||||
.draw_glyphs(&font)
|
||||
.font_size(font_size)
|
||||
.brush(brush)
|
||||
.transform(transform)
|
||||
.draw(
|
||||
peniko::Fill::NonZero,
|
||||
glyph_run.positioned_glyphs().map(|g| vello::Glyph {
|
||||
id: g.id as u32,
|
||||
x: g.x,
|
||||
y: g.y,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pinta un layout **multicolor** ([`Typesetter::layout_runs`]): cada
|
||||
/// `glyph_run` usa el color de su propio brush ([`RunBrush`]) en vez de un
|
||||
/// color uniforme. `origin` es la esquina superior-izquierda del bloque.
|
||||
pub fn draw_layout_runs(
|
||||
scene: &mut vello::Scene,
|
||||
layout: &parley::Layout<RunBrush>,
|
||||
origin: (f64, f64),
|
||||
) {
|
||||
let transform = vello::kurbo::Affine::translate(origin);
|
||||
for line in layout.lines() {
|
||||
for item in line.items() {
|
||||
if let parley::PositionedLayoutItem::GlyphRun(glyph_run) = item {
|
||||
let brush = Brush::Solid(glyph_run.style().brush.0);
|
||||
let run = glyph_run.run();
|
||||
let font = run.font().clone();
|
||||
let font_size = run.font_size();
|
||||
scene
|
||||
.draw_glyphs(&font)
|
||||
.font_size(font_size)
|
||||
.brush(&brush)
|
||||
.transform(transform)
|
||||
.draw(
|
||||
peniko::Fill::NonZero,
|
||||
glyph_run.positioned_glyphs().map(|g| vello::Glyph {
|
||||
id: g.id as u32,
|
||||
x: g.x,
|
||||
y: g.y,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mide sin pintar. Atajo de [`layout_block`] + [`measurement`] para
|
||||
/// llamadores que sólo necesitan el bounding box.
|
||||
pub fn measure(ts: &mut Typesetter, block: &TextBlock<'_>) -> Measurement {
|
||||
measurement(&layout_block(ts, block))
|
||||
}
|
||||
|
||||
/// Rasteriza el bloque en `scene` haciendo shaping una sola vez. Equivale a
|
||||
/// `layout_block` + `draw_layout` con `block.origin`.
|
||||
pub fn draw_block(scene: &mut vello::Scene, ts: &mut Typesetter, block: &TextBlock<'_>) {
|
||||
let layout = layout_block(ts, block);
|
||||
draw_layout(scene, &layout, block.color, block.origin);
|
||||
}
|
||||
Reference in New Issue
Block a user