1use crate::paths::{
6 maybe_path_for_char, maybe_path_for_cursor_style, path_for_strikeout, path_for_underline, Line,
7};
8use carnelian::color::Color;
9use carnelian::drawing::{FontFace, Glyph, TextGrid};
10use carnelian::render::{
11 BlendMode, Context as RenderContext, Fill, FillRule, Layer, Raster, Style,
12};
13use carnelian::scene::{LayerGroup, SceneOrder};
14use carnelian::Size;
15use euclid::{point2, Rect};
16use rustc_hash::{FxHashMap, FxHashSet};
17use std::collections::hash_map::Entry;
18use std::collections::BTreeSet;
19use std::mem;
20use term_model::ansi::{CursorStyle, TermInfo};
21use term_model::config::Config;
22use term_model::term::cell::Flags;
23use term_model::term::color::Rgb;
24use term_model::term::{RenderableCell, RenderableCellContent, Term};
25
26const SCALE_FACTORS: &[f32] = &[1.0, 1.25, 2.0, 3.0, 4.0];
31
32pub fn get_scale_factor(dpi: &BTreeSet<u32>, actual_dpi: f32) -> f32 {
34 let mut scale_factor = 0;
35 for value in dpi.iter() {
36 if *value as f32 > actual_dpi {
37 break;
38 }
39 scale_factor += 1;
40 }
41 *SCALE_FACTORS.get(scale_factor).unwrap_or(SCALE_FACTORS.last().unwrap())
42}
43
44pub fn cell_size_from_cell_height(font_set: &FontSet, height: f32) -> Size {
46 let rounded_height = height.round();
47
48 let face = &font_set.font.face;
53 let width = face.glyph_index('0').map_or(height / 2.0, |glyph_index| {
54 let ascent = face.ascender() as f32;
55 let descent = face.descender() as f32;
56 let horizontal_advance =
57 face.glyph_hor_advance(glyph_index).expect("glyph_hor_advance") as f32;
58 rounded_height * horizontal_advance / (ascent - descent)
59 });
60
61 Size::new(width.round(), rounded_height)
62}
63
64#[derive(Clone)]
65pub struct FontSet {
66 font: FontFace,
67 bold_font: Option<FontFace>,
68 italic_font: Option<FontFace>,
69 bold_italic_font: Option<FontFace>,
70 fallback_fonts: Vec<FontFace>,
71}
72
73impl FontSet {
74 pub fn new(
75 font: FontFace,
76 bold_font: Option<FontFace>,
77 italic_font: Option<FontFace>,
78 bold_italic_font: Option<FontFace>,
79 fallback_fonts: Vec<FontFace>,
80 ) -> Self {
81 Self { font, bold_font, italic_font, bold_italic_font, fallback_fonts }
82 }
83}
84
85#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
86pub enum LayerContent {
87 Cursor(CursorStyle),
88 Char((char, Flags)),
89}
90
91impl From<RenderableCell> for LayerContent {
94 fn from(cell: RenderableCell) -> Self {
95 match cell.inner {
96 RenderableCellContent::Cursor(cursor_key) => Self::Cursor(cursor_key.style),
97 RenderableCellContent::Chars(chars) => {
98 let flags = cell.flags & (Flags::BOLD_ITALIC | Flags::UNDERLINE | Flags::STRIKEOUT);
99 if chars[0] == '\t' || cell.flags.contains(Flags::HIDDEN) {
101 Self::Char((' ', flags))
102 } else {
103 Self::Char((chars[0], flags))
104 }
105 }
106 }
107 }
108}
109
110#[derive(PartialEq)]
111struct LayerId {
112 content: LayerContent,
113 rgb: Rgb,
114}
115
116fn maybe_raster_for_cursor_style(
117 render_context: &mut RenderContext,
118 cursor_style: CursorStyle,
119 cell_size: &Size,
120) -> Option<Raster> {
121 maybe_path_for_cursor_style(render_context, cursor_style, cell_size).as_ref().map(|p| {
122 let mut raster_builder = render_context.raster_builder().expect("raster_builder");
123 raster_builder.add(p, None);
124 raster_builder.build()
125 })
126}
127
128fn maybe_fallback_glyph_for_char(
129 render_context: &mut RenderContext,
130 c: char,
131 cell_size: &Size,
132) -> Option<Glyph> {
133 maybe_path_for_char(render_context, c, cell_size).as_ref().map(|p| {
134 let mut raster_builder = render_context.raster_builder().expect("raster_builder");
135 raster_builder.add(p, None);
136 let raster = raster_builder.build();
137 let bounding_box = Rect::from_size(*cell_size);
138 Glyph { raster, bounding_box }
139 })
140}
141
142fn maybe_glyph_for_char(
143 context: &mut RenderContext,
144 c: char,
145 flags: Flags,
146 textgrid: &TextGrid,
147 font_set: &FontSet,
148) -> Option<Glyph> {
149 let maybe_bold_italic_font = match flags & Flags::BOLD_ITALIC {
150 Flags::BOLD => font_set.bold_font.as_ref(),
151 Flags::ITALIC => font_set.italic_font.as_ref(),
152 Flags::BOLD_ITALIC => font_set.bold_italic_font.as_ref(),
153 _ => None,
154 };
155 let scale = textgrid.scale;
156 let offset = textgrid.offset;
157
158 for font in maybe_bold_italic_font
167 .iter()
168 .map(|font| *font)
169 .chain(std::iter::once(&font_set.font))
170 .chain(font_set.fallback_fonts.iter())
171 {
172 if let Some(glyph_index) = font.face.glyph_index(c) {
173 let glyph = Glyph::with_scale_and_offset(context, font, scale, offset, glyph_index);
174 return Some(glyph);
175 }
176 }
177
178 maybe_fallback_glyph_for_char(context, c, &textgrid.cell_size)
180}
181
182fn maybe_raster_for_char(
183 context: &mut RenderContext,
184 c: char,
185 flags: Flags,
186 textgrid: &TextGrid,
187 font_set: &FontSet,
188) -> Option<Raster> {
189 let maybe_glyph = maybe_glyph_for_char(context, c, flags, textgrid, font_set);
191
192 let maybe_extra_raster = if flags.intersects(Flags::UNDERLINE | Flags::STRIKEOUT) {
194 let mut raster_builder = context.raster_builder().expect("raster_builder");
195 if flags.contains(Flags::UNDERLINE) {
196 let line_metrics = font_set.font.face.underline_metrics();
198 raster_builder.add(
199 &path_for_underline(
200 &textgrid.cell_size,
201 context,
202 line_metrics.map(|line_metrics| Line::new(line_metrics, textgrid)),
203 ),
204 None,
205 );
206 }
207 if flags.contains(Flags::STRIKEOUT) {
208 let line_metrics = font_set.font.face.strikeout_metrics();
209 raster_builder.add(
210 &path_for_strikeout(
211 &textgrid.cell_size,
212 context,
213 line_metrics.map(|line_metrics| Line::new(line_metrics, textgrid)),
214 ),
215 None,
216 );
217 }
218 Some(raster_builder.build())
219 } else {
220 None
221 };
222
223 match (maybe_glyph, maybe_extra_raster) {
225 (Some(glyph), Some(extra_raster)) => Some(glyph.raster + extra_raster),
226 (Some(glyph), None) => Some(glyph.raster),
227 (None, Some(extra_raster)) => Some(extra_raster),
228 _ => None,
229 }
230}
231
232fn maybe_raster_for_layer_content(
233 render_context: &mut RenderContext,
234 content: &LayerContent,
235 column: usize,
236 row: usize,
237 textgrid: &TextGrid,
238 font_set: &FontSet,
239 raster_cache: &mut FxHashMap<LayerContent, Option<Raster>>,
240) -> Option<Raster> {
241 raster_cache
242 .entry(*content)
243 .or_insert_with(|| match content {
244 LayerContent::Cursor(cursor_style) => {
245 maybe_raster_for_cursor_style(render_context, *cursor_style, &textgrid.cell_size)
246 }
247 LayerContent::Char((c, flags)) => {
248 maybe_raster_for_char(render_context, *c, *flags, textgrid, font_set)
249 }
250 })
251 .as_ref()
252 .map(|r| {
253 let cell_size = &textgrid.cell_size;
254 let cell_position =
255 point2(cell_size.width * column as f32, cell_size.height * row as f32);
256 let raster = r.clone().translate(cell_position.to_vector().to_i32());
257 let empty_raster = {
260 let raster_builder = render_context.raster_builder().unwrap();
261 raster_builder.build()
262 };
263 raster + empty_raster
264 })
265}
266
267fn make_color(term_color: &Rgb) -> Color {
268 Color { r: term_color.r, g: term_color.g, b: term_color.b, a: 0xff }
269}
270
271#[derive(PartialEq, Debug)]
272pub struct RenderableLayer {
273 pub order: usize,
274 pub column: usize,
275 pub row: usize,
276 pub content: LayerContent,
277 pub rgb: Rgb,
278}
279
280pub struct Offset {
281 pub column: usize,
282 pub row: usize,
283}
284
285pub fn renderable_layers<'b, T, C>(
286 term: &'b Term<T>,
287 config: &'b Config<C>,
288 offset: &'b Offset,
289) -> impl Iterator<Item = RenderableLayer> + 'b {
290 let columns = term.cols().0;
291 let stride = columns * 4;
300 term.renderable_cells(config).flat_map(move |cell| {
301 let row = cell.line.0 + offset.row;
302 let cell_order = row * stride + (cell.column.0 + offset.column);
303 let content: LayerContent = cell.into();
304 let order = match content {
305 LayerContent::Cursor(_) => cell_order,
306 LayerContent::Char(_) => cell_order + columns * 2,
307 };
308 if cell.bg_alpha != 0.0 {
309 assert!(cell.bg_alpha == 1.0, "unsupported bg_alpha: {}", cell.bg_alpha);
310 Some(RenderableLayer {
311 order: order,
312 column: cell.column.0,
313 row,
314 content: LayerContent::Cursor(CursorStyle::Block),
315 rgb: cell.bg,
316 })
317 } else {
318 None
319 }
320 .into_iter()
321 .chain(std::iter::once(RenderableLayer {
322 order: order + columns,
323 column: cell.column.0,
324 row,
325 content,
326 rgb: cell.fg,
327 }))
328 })
329}
330
331pub struct Renderer {
332 textgrid: TextGrid,
333 raster_cache: FxHashMap<LayerContent, Option<Raster>>,
334 layers: FxHashMap<SceneOrder, LayerId>,
335 old_layers: FxHashSet<SceneOrder>,
336 new_layers: FxHashSet<SceneOrder>,
337}
338
339impl Renderer {
340 pub fn new(font_set: &FontSet, cell_size: &Size) -> Self {
341 let textgrid = TextGrid::new(&font_set.font, cell_size);
342 let raster_cache = FxHashMap::default();
343 let layers = FxHashMap::default();
344 let old_layers = FxHashSet::default();
345 let new_layers = FxHashSet::default();
346
347 Self { textgrid, raster_cache, layers, old_layers, new_layers }
348 }
349
350 pub fn render<I>(
351 &mut self,
352 layer_group: &mut dyn LayerGroup,
353 render_context: &mut RenderContext,
354 font_set: &FontSet,
355 layers: I,
356 ) where
357 I: IntoIterator<Item = RenderableLayer>,
358 {
359 let raster_cache = &mut self.raster_cache;
360 let textgrid = &self.textgrid;
361
362 for RenderableLayer { order, column, row, content, rgb } in layers.into_iter() {
364 let id = LayerId { content, rgb };
365 let order = SceneOrder::try_from(order).unwrap_or_else(|e| panic!("{}", e));
366
367 self.old_layers.remove(&order);
369
370 match self.layers.entry(order) {
371 Entry::Occupied(entry) => {
372 if *entry.get() != id {
373 let raster = maybe_raster_for_layer_content(
374 render_context,
375 &id.content,
376 column,
377 row,
378 textgrid,
379 font_set,
380 raster_cache,
381 );
382 if let Some(raster) = raster {
383 let value = entry.into_mut();
384 *value = id;
385
386 let did_not_exist = self.new_layers.insert(order);
387 assert!(
388 did_not_exist,
389 "multiple layers with order: {}",
390 order.as_u32()
391 );
392 layer_group.insert(
393 order,
394 Layer {
395 raster,
396 clip: None,
397 style: Style {
398 fill_rule: FillRule::NonZero,
399 fill: Fill::Solid(make_color(&rgb)),
400 blend_mode: BlendMode::Over,
401 },
402 },
403 );
404 } else {
405 entry.remove_entry();
406 layer_group.remove(order);
407 }
408 } else {
409 let did_not_exist = self.new_layers.insert(order);
410 assert!(did_not_exist, "multiple layers with order: {}", order.as_u32());
411 }
412 }
413 Entry::Vacant(entry) => {
414 let raster = maybe_raster_for_layer_content(
415 render_context,
416 &id.content,
417 column,
418 row,
419 textgrid,
420 font_set,
421 raster_cache,
422 );
423 if let Some(raster) = raster {
424 entry.insert(id);
425 let did_not_exist = self.new_layers.insert(order);
426 assert!(did_not_exist, "multiple layers with order: {}", order.as_u32());
427 layer_group.insert(
428 order,
429 Layer {
430 raster,
431 clip: None,
432 style: Style {
433 fill_rule: FillRule::NonZero,
434 fill: Fill::Solid(make_color(&rgb)),
435 blend_mode: BlendMode::Over,
436 },
437 },
438 );
439 }
440 }
441 }
442 }
443
444 for order in self.old_layers.drain() {
446 self.layers.remove(&order);
447 layer_group.remove(order);
448 }
449
450 mem::swap(&mut self.old_layers, &mut self.new_layers);
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458 use anyhow::Error;
459 use carnelian::drawing::DisplayRotation;
460 use carnelian::render::{generic, Context as RenderContext, ContextInner};
461 use euclid::size2;
462 use fuchsia_async as fasync;
463 use std::collections::BTreeMap;
464 use term_model::ansi::Processor;
465 use term_model::clipboard::Clipboard;
466 use term_model::event::{Event, EventListener};
467 use term_model::term::SizeInfo;
468
469 struct TermConfig;
470
471 impl Default for TermConfig {
472 fn default() -> TermConfig {
473 TermConfig
474 }
475 }
476
477 struct EventProxy;
478
479 impl EventListener for EventProxy {
480 fn send_event(&self, _event: Event) {}
481 }
482
483 static FONT_DATA: &'static [u8] = include_bytes!(
486 "../../../../../prebuilt/third_party/fonts/robotomono/RobotoMono-Regular.ttf"
487 );
488 static FONT_SET: std::sync::LazyLock<FontSet> = std::sync::LazyLock::new(|| {
489 FontSet::new(
490 FontFace::new(&FONT_DATA).expect("Failed to create font"),
491 None,
492 None,
493 None,
494 vec![],
495 )
496 });
497
498 struct TestLayerGroup<'a>(&'a mut BTreeMap<SceneOrder, Layer>);
499
500 impl LayerGroup for TestLayerGroup<'_> {
501 fn clear(&mut self) {
502 self.0.clear();
503 }
504 fn insert(&mut self, order: SceneOrder, layer: Layer) {
505 self.0.insert(order, layer);
506 }
507 fn remove(&mut self, order: SceneOrder) {
508 self.0.remove(&order);
509 }
510 }
511
512 #[test]
513 fn check_scale_factors() {
514 let dpi = BTreeSet::from([160, 240, 320]);
515 assert_eq!(get_scale_factor(&dpi, 100.0), 1.0);
516 assert_eq!(get_scale_factor(&dpi, 180.0), 1.25);
517 assert_eq!(get_scale_factor(&dpi, 240.0), 2.0);
518 assert_eq!(get_scale_factor(&dpi, 319.0), 2.0);
519 assert_eq!(get_scale_factor(&dpi, 400.0), 3.0);
520 }
521
522 #[test]
523 fn can_create_renderable_layers() -> Result<(), Error> {
524 let cell_size = Size::new(8.0, 16.0);
525 let size_info = SizeInfo {
526 width: cell_size.width * 2.0,
527 height: cell_size.height,
528 cell_width: cell_size.width,
529 cell_height: cell_size.height,
530 padding_x: 0.0,
531 padding_y: 0.0,
532 dpr: 1.0,
533 };
534 let bg = Rgb { r: 0, g: 0, b: 0 };
535 let fg = Rgb { r: 255, g: 255, b: 255 };
536 let config = {
537 let mut config = Config::<TermConfig>::default();
538 config.colors.primary.background = bg;
539 config.colors.primary.foreground = fg;
540 config
541 };
542 let mut term = Term::new(&config, &size_info, Clipboard::new(), EventProxy {});
543 let mut parser = Processor::new();
544 let mut output = vec![];
545 parser.advance(&mut term, 'A' as u8, &mut output);
546 let offset = Offset { column: 0, row: 0 };
547 let result = renderable_layers(&term, &config, &offset).collect::<Vec<_>>();
548 assert_eq!(
549 result,
550 vec![
551 RenderableLayer {
552 order: 6,
553 column: 0,
554 row: 0,
555 content: LayerContent::Char(('A', Flags::empty())),
556 rgb: fg
557 },
558 RenderableLayer {
559 order: 3,
560 column: 1,
561 row: 0,
562 content: LayerContent::Cursor(CursorStyle::Block),
563 rgb: fg
564 },
565 RenderableLayer {
566 order: 7,
567 column: 1,
568 row: 0,
569 content: LayerContent::Char((' ', Flags::empty())),
570 rgb: bg
571 }
572 ],
573 "unexpected layers"
574 );
575 Ok(())
576 }
577
578 #[fasync::run_singlethreaded(test)]
579 async fn can_render_cell() {
580 let size = size2(64, 64);
581 let forma_context = generic::Forma::new_context_without_token(size, DisplayRotation::Deg0);
582 let mut render_context = RenderContext { inner: ContextInner::Forma(forma_context) };
583 let mut renderer = Renderer::new(&FONT_SET, &Size::new(8.0, 16.0));
584 let layers = vec![
585 RenderableLayer {
586 order: 0,
587 column: 0,
588 row: 0,
589 content: LayerContent::Cursor(CursorStyle::Block),
590 rgb: Rgb { r: 0xff, g: 0xff, b: 0xff },
591 },
592 RenderableLayer {
593 order: 1,
594 column: 0,
595 row: 0,
596 content: LayerContent::Char(('A', Flags::empty())),
597 rgb: Rgb { r: 0, g: 0, b: 0xff },
598 },
599 ];
600 let mut result = BTreeMap::new();
601 let mut layer_group = TestLayerGroup(&mut result);
602 renderer.render(&mut layer_group, &mut render_context, &FONT_SET, layers.into_iter());
603 assert_eq!(result.len(), 2, "expected two layers");
604 }
605}