use std::sync::Arc;
use gpui::{
div, font, point, prelude::*, px, rgb, rgba, size, App, Application, Bounds, Context, Element,
ElementId, Entity, Font, FontWeight, GlobalElementId, InspectorElementId, LayoutId, Pixels,
Render, Style, TextAlign, TextRun, TextSystem, Window, WindowBounds, WindowOptions,
WrappedLine,
};
use rhythm_gpui::{
RhythmBlockMetrics, RhythmFont, RhythmFontSpec, RhythmGrid, RhythmLineMetrics, RhythmStyled,
};
struct FontSet {
heading: RhythmFont,
body: RhythmFont,
bold: RhythmFont,
mono: RhythmFont,
cjk: RhythmFont,
emoji: RhythmFont,
}
impl FontSet {
fn resolve(text_system: &TextSystem, grid: RhythmGrid) -> Self {
let mut heading = font("Georgia");
heading.weight = FontWeight::BOLD;
let mut bold = font("Georgia");
bold.weight = FontWeight::BOLD;
let body_spec = RhythmFontSpec::new(font("Georgia"), px(16.), 3, grid);
let runs = [
RhythmFontSpec::new(bold, px(16.), 3, grid),
RhythmFontSpec::new(font("Menlo"), px(16.), 3, grid),
RhythmFontSpec::new(font("PingFang SC"), px(16.), 3, grid),
RhythmFontSpec::new(font("Apple Color Emoji"), px(16.), 3, grid),
];
Self {
heading: grid.font(text_system, heading, px(28.), 5),
body: body_spec.resolve_covering(text_system, &runs),
bold: runs[0].resolve(text_system),
mono: runs[1].resolve(text_system),
cjk: runs[2].resolve(text_system),
emoji: runs[3].resolve(text_system),
}
}
}
struct Span(&'static str, Font, u32);
struct Paragraph {
spans: Vec<Span>,
font_size: Pixels,
line_rhythms: u32,
top: i32,
bottom: i32,
}
struct CachedBlock {
line: WrappedLine,
metrics: RhythmLineMetrics,
block: RhythmBlockMetrics,
top_row: i32,
}
struct DocLayout {
grid: RhythmGrid,
width: Pixels,
blocks: Vec<CachedBlock>,
}
struct DirectPaint {
grid: RhythmGrid,
set: FontSet,
cache: Option<Arc<DocLayout>>,
}
const MARGIN: f32 = 48.;
impl DirectPaint {
fn paragraphs(&self) -> Vec<Paragraph> {
let set = &self.set;
let ink = 0x24292f;
let code = 0x0550ae;
let script = 0x953800;
vec![
Paragraph {
spans: vec![Span("Direct paint", set.heading.font().clone(), ink)],
font_size: set.heading.font_size(),
line_rhythms: set.heading.metrics().line_rhythms(),
top: 6,
bottom: 0,
},
Paragraph {
spans: vec![Span(
"Every line below is a cached WrappedLine painted at a computed origin — \
no element tree, no Taffy nodes. Resize the window: paragraphs reshape \
only when the wrap width changes, and every baseline keeps its \
appointment with the grid.",
set.body.font().clone(),
ink,
)],
font_size: set.body.font_size(),
line_rhythms: set.body.metrics().line_rhythms(),
top: 6,
bottom: 0,
},
Paragraph {
spans: vec![
Span("Explicit runs: ", set.body.font().clone(), ink),
Span("bold weight", set.bold.font().clone(), ink),
Span(", ", set.body.font().clone(), ink),
Span("inline_code()", set.mono.font().clone(), code),
Span(", ", set.body.font().clone(), ink),
Span("汉字混排", set.cjk.font().clone(), script),
Span(" and ", set.body.font().clone(), ink),
Span("😀", set.emoji.font().clone(), ink),
Span(" share one shaped baseline.", set.body.font().clone(), ink),
],
font_size: set.body.font_size(),
line_rhythms: set.body.metrics().line_rhythms(),
top: 6,
bottom: 0,
},
Paragraph {
spans: vec![Span(
"Glyph fallback: the same 汉字混排 and 😀 in one body-font run — on \
macOS/CoreText the reported line metrics do not grow.",
set.body.font().clone(),
ink,
)],
font_size: set.body.font_size(),
line_rhythms: set.body.metrics().line_rhythms(),
top: 6,
bottom: 3,
},
]
}
fn layout(&self, width: Pixels, window: &Window) -> DocLayout {
let grid = self.grid;
let wrap = px(f32::from(width) - 2. * MARGIN).max(px(64.));
let mut top_row = 0i32;
let mut blocks = Vec::new();
for para in self.paragraphs() {
let text: String = para.spans.iter().map(|Span(s, ..)| *s).collect();
let runs: Vec<TextRun> = para
.spans
.iter()
.map(|Span(s, span_font, color)| TextRun {
len: s.len(),
font: span_font.clone(),
color: rgb(*color).into(),
background_color: None,
underline: None,
strikethrough: None,
})
.collect();
let lines = window
.text_system()
.shape_text(text.into(), para.font_size, &runs, Some(wrap), None)
.expect("shape direct-paint paragraph");
for line in lines {
let metrics = grid.line_metrics(line.ascent(), line.descent(), para.line_rhythms);
let block = RhythmBlockMetrics::new(metrics, para.top, para.bottom);
let visual_lines = line.wrap_boundaries.len() as u32 + 1;
let block_rows = block.rows(visual_lines);
blocks.push(CachedBlock {
line,
metrics,
block,
top_row,
});
top_row = top_row
.checked_add(block_rows)
.expect("direct-paint document exceeds i32 grid rows");
}
}
DocLayout {
grid,
width,
blocks,
}
}
}
struct DocumentElement {
doc: Entity<DirectPaint>,
}
impl IntoElement for DocumentElement {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for DocumentElement {
type RequestLayoutState = ();
type PrepaintState = Arc<DocLayout>;
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut style = Style::default();
style.size.width = gpui::relative(1.).into();
style.size.height = gpui::relative(1.).into();
(window.request_layout(style, [], cx), ())
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
let stale = self
.doc
.read(cx)
.cache
.as_ref()
.is_none_or(|c| c.width != bounds.size.width);
if stale {
let layout = Arc::new(self.doc.read(cx).layout(bounds.size.width, window));
self.doc.update(cx, |doc, _| doc.cache = Some(layout));
}
self.doc.read(cx).cache.clone().expect("cache just filled")
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
layout: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
for cached in &layout.blocks {
let target = layout.grid.height(cached.top_row) + cached.block.first_baseline_px();
let origin_y = cached.metrics.paint_origin_for_px(target);
let origin = point(bounds.origin.x + px(MARGIN), bounds.origin.y + origin_y);
cached
.line
.paint(
origin,
cached.metrics.line_height_px(),
TextAlign::Left,
None,
window,
cx,
)
.expect("paint cached direct-paint line");
}
}
}
impl Render for DirectPaint {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.bg(gpui::white())
.child(DocumentElement { doc: cx.entity() })
.rhythm_debug_overlay(self.grid.overlay(rgba(0x0969da33)), true)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let grid = RhythmGrid::new(px(8.));
let view = DirectPaint {
grid,
set: FontSet::resolve(cx.text_system(), grid),
cache: None,
};
let bounds = Bounds::centered(None, size(px(760.), px(560.)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| view),
)
.unwrap();
cx.activate(true);
});
}