@@ -339,6 +339,260 @@ fn synthesize_text_style(src: &CssStyle, font_size: f32, default_family: &str) -
339339#[ allow( dead_code) ]
340340fn _line_height_unused ( _: Option < & LineHeight > ) { }
341341
342+ // ─────────────────────────────────────────────────────────────────────────────
343+ // Terminal intrinsic measurer
344+ // ─────────────────────────────────────────────────────────────────────────────
345+
346+ use crate :: terminal:: {
347+ Terminal , CHROME_HEIGHT , FONT_SIZE as TERM_FONT_SIZE , LINE_HEIGHT as TERM_LINE_HEIGHT ,
348+ PADDING as TERM_PADDING ,
349+ } ;
350+
351+ /// Intrinsic measurer for [`Terminal`].
352+ ///
353+ /// Natural size formula (matches the painter exactly):
354+ /// - `line_height = ceil(font_size × TERM_LINE_HEIGHT / TERM_FONT_SIZE)`
355+ /// - `height = chrome_height + 2 × TERM_PADDING + n_lines × line_height`
356+ /// - `width` = widest line text (prefix + content) + 2 × TERM_PADDING
357+ ///
358+ /// If the Skia font fails to load, returns (0, 0) so layout falls back to
359+ /// whatever container constraints supply.
360+ pub struct TerminalIntrinsic {
361+ line_height : f32 ,
362+ n_lines : usize ,
363+ chrome_height : f32 ,
364+ padding : f32 ,
365+ /// Maximum measured text width across all lines (including prefix).
366+ max_line_width : f32 ,
367+ }
368+
369+ impl TerminalIntrinsic {
370+ pub fn from_terminal ( t : & Terminal ) -> Self {
371+ let font_size = t. style . font_size_px_or ( TERM_FONT_SIZE ) ;
372+ let line_height = ( font_size * TERM_LINE_HEIGHT / TERM_FONT_SIZE ) . ceil ( ) ;
373+ let chrome_height = if t. show_chrome { CHROME_HEIGHT } else { 0.0 } ;
374+
375+ // Measure each line (prefix + text) with the same Skia font the painter uses.
376+ let max_line_width = Self :: measure_max_width ( t, font_size) ;
377+
378+ Self {
379+ line_height,
380+ n_lines : t. lines . len ( ) ,
381+ chrome_height,
382+ padding : TERM_PADDING ,
383+ max_line_width,
384+ }
385+ }
386+
387+ fn measure_max_width ( t : & Terminal , font_size : f32 ) -> f32 {
388+ let font_style = skia_safe:: FontStyle :: normal ( ) ;
389+ let Ok ( typeface) = typeface_with_fallback ( "SF Mono" , font_style) else {
390+ // Font unavailable (CI without fonts); return 0 — the layout will
391+ // be width-unconstrained and the container drives the size.
392+ return 0.0 ;
393+ } ;
394+ let font = Font :: from_typeface ( typeface, font_size) ;
395+ let emoji_font = emoji_typeface ( ) . map ( |tf| Font :: from_typeface ( tf, font_size) ) ;
396+
397+ t. lines
398+ . iter ( )
399+ . map ( |line| {
400+ let prefix = match line. line_type {
401+ crate :: terminal:: TerminalLineType :: Prompt => "$ " ,
402+ _ => "" ,
403+ } ;
404+ let full = format ! ( "{}{}" , prefix, line. text) ;
405+ measure_text_with_fallback ( & full, & font, & emoji_font, 0.0 )
406+ } )
407+ . fold ( 0.0f32 , f32:: max)
408+ }
409+ }
410+
411+ impl IntrinsicMeasure for TerminalIntrinsic {
412+ fn measure (
413+ & self ,
414+ known : ( Option < f32 > , Option < f32 > ) ,
415+ _available : ( AvailableSpace , AvailableSpace ) ,
416+ ) -> ( f32 , f32 ) {
417+ let w = known. 0 . unwrap_or ( self . max_line_width + self . padding * 2.0 ) ;
418+ let h = known. 1 . unwrap_or (
419+ self . chrome_height + self . padding * 2.0 + self . n_lines as f32 * self . line_height ,
420+ ) ;
421+ ( w, h)
422+ }
423+ }
424+
425+ // ─────────────────────────────────────────────────────────────────────────────
426+ // Table intrinsic measurer
427+ // ─────────────────────────────────────────────────────────────────────────────
428+
429+ use crate :: table:: {
430+ Table , DEFAULT_CELL_PADDING , DEFAULT_FONT_SIZE as TABLE_FONT_SIZE , DEFAULT_ROW_HEIGHT_RATIO ,
431+ } ;
432+
433+ /// Intrinsic measurer for [`Table`].
434+ ///
435+ /// Natural size formula (matches the painter exactly):
436+ /// - `row_height = font_size × DEFAULT_ROW_HEIGHT_RATIO`
437+ /// - `height = (1 + row_count) × row_height` (header + data rows)
438+ /// - `width`: if `column_widths` are provided, their sum; otherwise each
439+ /// column gets `max(header_text_width + 2 × cell_padding, min_col_width)`.
440+ pub struct TableIntrinsic {
441+ row_height : f32 ,
442+ row_count : usize , // data rows only; header adds 1
443+ total_width : f32 ,
444+ }
445+
446+ impl TableIntrinsic {
447+ pub fn from_table ( t : & Table ) -> Self {
448+ let font_size = t. style . font_size_px_or ( TABLE_FONT_SIZE ) ;
449+ let row_height = font_size * DEFAULT_ROW_HEIGHT_RATIO ;
450+
451+ let total_width = Self :: compute_width ( t, font_size) ;
452+
453+ Self {
454+ row_height,
455+ row_count : t. rows . len ( ) ,
456+ total_width,
457+ }
458+ }
459+
460+ fn compute_width ( t : & Table , font_size : f32 ) -> f32 {
461+ // Explicit column widths provided → sum them.
462+ if let Some ( widths) = & t. column_widths {
463+ if !widths. is_empty ( ) {
464+ return widths. iter ( ) . sum ( ) ;
465+ }
466+ }
467+
468+ // Measure each header with the bold font; add 2× cell_padding per column.
469+ let font_style = skia_safe:: FontStyle :: bold ( ) ;
470+ let family = t. style . font_family . as_deref ( ) . unwrap_or ( "Inter" ) ;
471+ let Ok ( typeface) = typeface_with_fallback ( family, font_style) else {
472+ // Font unavailable: fall back to col_count × a reasonable minimum.
473+ let col_count = t. headers . len ( ) . max ( 1 ) as f32 ;
474+ return col_count * ( TABLE_FONT_SIZE * 8.0 + DEFAULT_CELL_PADDING * 2.0 ) ;
475+ } ;
476+ let font = Font :: from_typeface ( typeface, font_size) ;
477+ let emoji_font = emoji_typeface ( ) . map ( |tf| Font :: from_typeface ( tf, font_size) ) ;
478+ let cell_padding = t. cell_padding ;
479+
480+ // Also consider data cell widths to size columns appropriately.
481+ let col_count = t. headers . len ( ) . max ( 1 ) ;
482+ let mut col_widths: Vec < f32 > = vec ! [ 0.0 ; col_count] ;
483+
484+ for ( i, header) in t. headers . iter ( ) . enumerate ( ) {
485+ let w = measure_text_with_fallback ( header, & font, & emoji_font, 0.0 ) ;
486+ col_widths[ i] = col_widths[ i] . max ( w + cell_padding * 2.0 ) ;
487+ }
488+ for row in & t. rows {
489+ for ( i, cell) in row. iter ( ) . enumerate ( ) {
490+ if i >= col_count {
491+ break ;
492+ }
493+ let w = measure_text_with_fallback ( cell, & font, & emoji_font, 0.0 ) ;
494+ col_widths[ i] = col_widths[ i] . max ( w + cell_padding * 2.0 ) ;
495+ }
496+ }
497+
498+ col_widths. iter ( ) . sum ( )
499+ }
500+ }
501+
502+ impl IntrinsicMeasure for TableIntrinsic {
503+ fn measure (
504+ & self ,
505+ known : ( Option < f32 > , Option < f32 > ) ,
506+ _available : ( AvailableSpace , AvailableSpace ) ,
507+ ) -> ( f32 , f32 ) {
508+ let w = known. 0 . unwrap_or ( self . total_width ) ;
509+ let h = known
510+ . 1
511+ . unwrap_or ( ( 1 + self . row_count ) as f32 * self . row_height ) ;
512+ ( w, h)
513+ }
514+ }
515+
516+ // ─────────────────────────────────────────────────────────────────────────────
517+ // Codeblock intrinsic measurer
518+ // ─────────────────────────────────────────────────────────────────────────────
519+
520+ use crate :: codeblock:: dimensions:: compute_code_dimensions;
521+ use crate :: codeblock:: highlight:: resolve_monospace_font;
522+ use crate :: codeblock:: Codeblock ;
523+ use rustmotion_core:: css:: style:: { FontWeight as CssFontWeight2 , FontWeightKw as CssFontWeightKw2 } ;
524+ use rustmotion_core:: schema:: FontWeight ;
525+
526+ /// Intrinsic measurer for [`Codeblock`].
527+ ///
528+ /// Reuses `compute_code_dimensions` (same function as the painter) to derive:
529+ /// - `width = max_line_width + gutter_width + pad_left + pad_right`
530+ /// - `height = line_count × line_height + pad_top + pad_bottom + chrome_height`
531+ ///
532+ /// Computed once at construction from the initial `code` string. If a state
533+ /// transition widens the content at paint time, `auto_scroll` handles vertical
534+ /// overflow without needing the intrinsic to re-run.
535+ pub struct CodeblockIntrinsic {
536+ natural_width : f32 ,
537+ natural_height : f32 ,
538+ }
539+
540+ impl CodeblockIntrinsic {
541+ pub fn from_codeblock ( c : & Codeblock ) -> Self {
542+ let font_family = c. style . font_family_or ( "JetBrains Mono" ) ;
543+ let font_size = c. style . font_size_px_or ( 14.0 ) ;
544+ let font_weight = match & c. style . font_weight {
545+ Some ( CssFontWeight2 :: Keyword ( CssFontWeightKw2 :: Bold | CssFontWeightKw2 :: Bolder ) ) => {
546+ FontWeight :: Bold
547+ }
548+ Some ( CssFontWeight2 :: Number ( n) ) if * n >= 600 => FontWeight :: Bold ,
549+ Some ( CssFontWeight2 :: Number ( n) ) => FontWeight :: Weight ( * n) ,
550+ _ => FontWeight :: Normal ,
551+ } ;
552+
553+ let Some ( font) = resolve_monospace_font ( font_family, font_size, font_weight) else {
554+ return Self {
555+ natural_width : 0.0 ,
556+ natural_height : 0.0 ,
557+ } ;
558+ } ;
559+
560+ let padding = {
561+ let ( t, r, b, l) = c. style . padding_px ( ) ;
562+ if t == 0.0 && r == 0.0 && b == 0.0 && l == 0.0 {
563+ ( 16.0 , 16.0 , 16.0 , 16.0 )
564+ } else {
565+ ( t, r, b, l)
566+ }
567+ } ;
568+
569+ let chrome_height = if c. chrome . as_ref ( ) . is_some_and ( |ch| ch. enabled ) {
570+ 36.0
571+ } else {
572+ 0.0
573+ } ;
574+
575+ let dims = compute_code_dimensions ( & c. code , & font, padding, chrome_height, c) ;
576+
577+ Self {
578+ natural_width : dims. total_width ,
579+ natural_height : dims. total_height ,
580+ }
581+ }
582+ }
583+
584+ impl IntrinsicMeasure for CodeblockIntrinsic {
585+ fn measure (
586+ & self ,
587+ known : ( Option < f32 > , Option < f32 > ) ,
588+ _available : ( AvailableSpace , AvailableSpace ) ,
589+ ) -> ( f32 , f32 ) {
590+ let w = known. 0 . unwrap_or ( self . natural_width ) ;
591+ let h = known. 1 . unwrap_or ( self . natural_height ) ;
592+ ( w, h)
593+ }
594+ }
595+
342596#[ cfg( test) ]
343597mod tests {
344598 use super :: * ;
0 commit comments