@@ -57,6 +57,11 @@ import {
5757 routeKeyEvent ,
5858 setMode ,
5959} from "../keybindings/index.js" ;
60+ import {
61+ type ResponsiveBreakpointThresholds ,
62+ normalizeBreakpointThresholds ,
63+ } from "../layout/responsive.js" ;
64+ import type { Rect } from "../layout/types.js" ;
6065import { PERF_ENABLED , perfMarkEnd , perfMarkStart , perfNow , perfRecord } from "../perf/perf.js" ;
6166import type { EventTimeUnwrapState } from "../protocol/types.js" ;
6267import { parseEventBatchV1 } from "../protocol/zrev_v1.js" ;
@@ -71,6 +76,8 @@ import { defaultTheme } from "../theme/defaultTheme.js";
7176import { coerceToLegacyTheme } from "../theme/interop.js" ;
7277import type { Theme } from "../theme/theme.js" ;
7378import type { ThemeDefinition } from "../theme/tokens.js" ;
79+ import type { VNode } from "../widgets/types.js" ;
80+ import { ui } from "../widgets/ui.js" ;
7481import { RawRenderer } from "./rawRenderer.js" ;
7582import {
7683 type RuntimeBreadcrumbAction ,
@@ -95,6 +102,8 @@ type ResolvedAppConfig = Readonly<{
95102 fpsCap : number ;
96103 maxEventBytes : number ;
97104 maxDrawlistBytes : number ;
105+ rootPadding : number ;
106+ breakpointThresholds : ResponsiveBreakpointThresholds ;
98107 useV2Cursor : boolean ;
99108 drawlistValidateParams : boolean ;
100109 drawlistReuseOutputBuffer : boolean ;
@@ -114,6 +123,8 @@ const DEFAULT_CONFIG: ResolvedAppConfig = Object.freeze({
114123 fpsCap : 60 ,
115124 maxEventBytes : 1 << 20 /* 1 MiB */ ,
116125 maxDrawlistBytes : 2 << 20 /* 2 MiB */ ,
126+ rootPadding : 0 ,
127+ breakpointThresholds : normalizeBreakpointThresholds ( undefined ) ,
117128 useV2Cursor : false ,
118129 drawlistValidateParams : true ,
119130 drawlistReuseOutputBuffer : true ,
@@ -232,6 +243,29 @@ async function loadTerminalProfile(backend: RuntimeBackend): Promise<TerminalPro
232243 }
233244}
234245
246+ function buildLayoutDebugOverlay ( rectById : ReadonlyMap < string , Rect > ) : VNode | null {
247+ if ( rectById . size === 0 ) return null ;
248+ const rows = [ ...rectById . entries ( ) ]
249+ . sort ( ( a , b ) => a [ 0 ] . localeCompare ( b [ 0 ] ) )
250+ . slice ( 0 , 18 )
251+ . map ( ( [ id , rect ] ) =>
252+ ui . text ( `${ id } ${ String ( rect . x ) } ,${ String ( rect . y ) } ${ String ( rect . w ) } x${ String ( rect . h ) } ` ) ,
253+ ) ;
254+ const panel = ui . box ( { border : "single" , title : `Layout (${ String ( rectById . size ) } )` , p : 1 } , [
255+ ui . column ( { gap : 0 } , rows ) ,
256+ ] ) ;
257+ return ui . layer ( {
258+ id : "rezi.layout.debug.overlay" ,
259+ zIndex : 2_000_000_000 ,
260+ modal : false ,
261+ backdrop : "none" ,
262+ closeOnEscape : false ,
263+ content : ui . column ( { width : "100%" , height : "100%" , justify : "end" , p : 1 } , [
264+ ui . row ( { width : "100%" , justify : "start" } , [ panel ] ) ,
265+ ] ) ,
266+ } ) ;
267+ }
268+
235269/** Apply defaults to user-provided config, validating all values. */
236270export function resolveAppConfig ( config : AppConfig | undefined ) : ResolvedAppConfig {
237271 if ( ! config ) return DEFAULT_CONFIG ;
@@ -247,6 +281,11 @@ export function resolveAppConfig(config: AppConfig | undefined): ResolvedAppConf
247281 config . maxDrawlistBytes === undefined
248282 ? DEFAULT_CONFIG . maxDrawlistBytes
249283 : requirePositiveInt ( "maxDrawlistBytes" , config . maxDrawlistBytes ) ;
284+ const rootPadding =
285+ config . rootPadding === undefined
286+ ? DEFAULT_CONFIG . rootPadding
287+ : requireNonNegativeInt ( "rootPadding" , config . rootPadding ) ;
288+ const breakpointThresholds = normalizeBreakpointThresholds ( config . breakpoints ) ;
250289 const useV2Cursor = config . useV2Cursor === true ;
251290 const drawlistValidateParams =
252291 config . drawlistValidateParams === undefined
@@ -276,6 +315,8 @@ export function resolveAppConfig(config: AppConfig | undefined): ResolvedAppConf
276315 fpsCap,
277316 maxEventBytes,
278317 maxDrawlistBytes,
318+ rootPadding,
319+ breakpointThresholds,
279320 useV2Cursor,
280321 drawlistValidateParams,
281322 drawlistReuseOutputBuffer,
@@ -445,6 +486,7 @@ export function createApp<S>(opts: CreateAppStateOptions<S> | CreateAppRoutesOnl
445486 let mode : Mode | null = null ;
446487 let drawFn : DrawFn | null = null ;
447488 let viewFn : ViewFn < S > | null = null ;
489+ let debugLayoutEnabled = false ;
448490
449491 const hasInitialState = "initialState" in opts ;
450492 let committedState : S = hasInitialState ? ( opts . initialState as S ) : ( Object . freeze ( { } ) as S ) ;
@@ -557,6 +599,8 @@ export function createApp<S>(opts: CreateAppStateOptions<S> | CreateAppRoutesOnl
557599 backend,
558600 drawlistVersion,
559601 maxDrawlistBytes : config . maxDrawlistBytes ,
602+ rootPadding : config . rootPadding ,
603+ breakpointThresholds : config . breakpointThresholds ,
560604 terminalProfile,
561605 useV2Cursor : config . useV2Cursor ,
562606 ...( opts . config ?. drawlistValidateParams === undefined
@@ -846,7 +890,12 @@ export function createApp<S>(opts: CreateAppStateOptions<S> | CreateAppRoutesOnl
846890 const prev = viewport ;
847891 if ( prev === null || prev . cols !== ev . cols || prev . rows !== ev . rows ) {
848892 viewport = Object . freeze ( { cols : ev . cols , rows : ev . rows } ) ;
849- markDirty ( DIRTY_LAYOUT ) ;
893+ if ( widgetRenderer . hasViewportAwareComposites ( ) ) {
894+ widgetRenderer . invalidateCompositeWidgets ( ) ;
895+ markDirty ( DIRTY_LAYOUT | DIRTY_VIEW ) ;
896+ } else {
897+ markDirty ( DIRTY_LAYOUT ) ;
898+ }
850899 }
851900 }
852901 if ( ev . kind === "tick" && mode === "widget" ) {
@@ -1177,7 +1226,15 @@ export function createApp<S>(opts: CreateAppStateOptions<S> | CreateAppRoutesOnl
11771226
11781227 const renderStart = perfNow ( ) ;
11791228 const submitToken = perfMarkStart ( "submit_frame" ) ;
1180- const res = widgetRenderer . submitFrame ( vf , snapshot , viewport , theme , hooks , plan ) ;
1229+ const frameView : ViewFn < S > = debugLayoutEnabled
1230+ ? ( state ) => {
1231+ const root = vf ( state ) ;
1232+ const overlay = buildLayoutDebugOverlay ( widgetRenderer . getRectByIdIndex ( ) ) ;
1233+ if ( ! overlay ) return root ;
1234+ return ui . layers ( [ root , overlay ] ) ;
1235+ }
1236+ : vf ;
1237+ const res = widgetRenderer . submitFrame ( frameView , snapshot , viewport , theme , hooks , plan ) ;
11811238 perfMarkEnd ( "submit_frame" , submitToken ) ;
11821239 if ( ! res . ok ) {
11831240 fatalNowOrEnqueue ( res . code , res . detail ) ;
@@ -1363,6 +1420,18 @@ export function createApp<S>(opts: CreateAppStateOptions<S> | CreateAppRoutesOnl
13631420 requestRenderFromRenderer ( ) ;
13641421 } ,
13651422
1423+ debugLayout ( enabled ?: boolean ) : boolean {
1424+ assertOperational ( "debugLayout" ) ;
1425+ if ( mode === "raw" ) {
1426+ throwCode ( "ZRUI_MODE_CONFLICT" , "debugLayout: not available in draw mode" ) ;
1427+ }
1428+ const next = enabled === undefined ? ! debugLayoutEnabled : enabled === true ;
1429+ if ( next === debugLayoutEnabled ) return debugLayoutEnabled ;
1430+ debugLayoutEnabled = next ;
1431+ requestViewFromRenderer ( ) ;
1432+ return debugLayoutEnabled ;
1433+ } ,
1434+
13661435 start ( ) : Promise < void > {
13671436 assertOperational ( "start" ) ;
13681437 assertNotReentrant ( "start" ) ;
0 commit comments