@@ -30,7 +30,9 @@ private static readonly (string TabId, string PanelId)[] Tabs =
3030
3131 private const int CollapseThreshold = 240 ;
3232 private const int StatusPollIntervalMs = 500 ;
33+ private const int MaxLogRows = 200 ;
3334 private const float NarrowBreakpointPx = 1024f ;
35+ private const float PhoneBreakpointPx = 480f ;
3436
3537 // ---- UI document state ----
3638 // All fields below are populated by BindElements before any other access.
@@ -83,6 +85,9 @@ private static readonly (string TabId, string PanelId)[] Tabs =
8385
8486#pragma warning restore 8618
8587
88+ // Tracks the last applied safe area so Update only re-applies on change.
89+ private Rect _lastSafeArea ;
90+
8691 // ---- UI lifecycle (single entry point called from main's Awake) ----
8792
8893 // Full UI lifecycle: thread capture, document load, binding, callbacks,
@@ -112,6 +117,15 @@ private void InitializeUi()
112117 _root . schedule . Execute ( RefreshStatusBar ) . Every ( StatusPollIntervalMs ) ;
113118 }
114119
120+ // Re-apply safe-area padding whenever Screen.safeArea changes (startup,
121+ // orientation flip, multi-window resize). Runs every frame but exits
122+ // immediately when nothing has changed.
123+ private void Update ( )
124+ {
125+ if ( Screen . safeArea != _lastSafeArea )
126+ ApplySafeArea ( ) ;
127+ }
128+
115129 // ---- UI document load ----
116130
117131 // Loads the UXML/USS resources and clones the tree into the panel
@@ -223,6 +237,11 @@ private void BindElements()
223237 // scroller never engages.
224238 _logView . contentContainer . style . flexShrink = 0 ;
225239
240+ // Tell the renderer the log subtree moves as a unit when scrolled.
241+ // Without this hint Unity re-composites every row on each scroll
242+ // frame; with it the subtree is translated on the GPU instead.
243+ _logView . contentContainer . usageHints = UsageHints . GroupTransform ;
244+
226245 _logCount = Require < Label > ( "log-count" ) ;
227246 }
228247
@@ -403,31 +422,78 @@ void Update()
403422 if ( float . IsNaN ( needed ) || needed <= 0f ) return ;
404423 pageScroll . contentContainer . style . minHeight = needed ;
405424 }
425+ pageScroll . contentContainer . usageHints = UsageHints . GroupTransform ;
406426 controls . RegisterCallback < GeometryChangedEvent > ( _ => Update ( ) ) ;
407427 logCol . RegisterCallback < GeometryChangedEvent > ( _ => Update ( ) ) ;
408428 pageScroll . contentContainer . schedule . Execute ( Update ) . StartingIn ( 0 ) ;
409429 }
410430
411- // Mirrors the web sample's `@media (min-width: 1024px)`. USS 2021.3
412- // has no @media; toggle a .narrow class on .sample-app-grid via
413- // GeometryChangedEvent. Idempotent — only mutates on the boolean flip.
431+ // Mirrors the web sample's `@media` breakpoints. USS 2021.3 has no
432+ // @media; toggle class names via GeometryChangedEvent instead.
433+ // .narrow on sample-app-grid AND _root : < 1024 px — stacks grid
434+ // columns vertically and
435+ // switches status bar to a
436+ // vertical stack so UUIDs
437+ // never overflow sideways.
438+ // .phone on _root : < 480 px — larger text,
439+ // 44 px touch targets.
440+ // Each breakpoint is evaluated independently so toggling one does not
441+ // suppress the other. Both are idempotent — only mutate on a boolean flip.
414442 private void RegisterResponsiveLayout ( )
415443 {
416444 var grid = Require < VisualElement > ( "sample-app-grid" ) ;
417445 void Update ( )
418446 {
419447 var w = _root . layout . width ;
420448 if ( float . IsNaN ( w ) || w <= 0f ) return ;
449+
421450 var shouldBeNarrow = w < NarrowBreakpointPx ;
422451 var isNarrow = grid . ClassListContains ( "narrow" ) ;
423- if ( shouldBeNarrow == isNarrow ) return ;
424- if ( shouldBeNarrow ) grid . AddToClassList ( "narrow" ) ;
425- else grid . RemoveFromClassList ( "narrow" ) ;
452+ if ( shouldBeNarrow != isNarrow )
453+ {
454+ grid . EnableInClassList ( "narrow" , shouldBeNarrow ) ;
455+ // Also on _root so .narrow selectors can reach the sticky
456+ // header (status bar) which sits outside .sample-app-grid.
457+ _root . EnableInClassList ( "narrow" , shouldBeNarrow ) ;
458+ }
459+
460+ var shouldBePhone = w < PhoneBreakpointPx ;
461+ var isPhone = _root . ClassListContains ( "phone" ) ;
462+ if ( shouldBePhone != isPhone )
463+ {
464+ _root . EnableInClassList ( "phone" , shouldBePhone ) ;
465+ }
426466 }
427467 _root . RegisterCallback < GeometryChangedEvent > ( _ => Update ( ) ) ;
428468 _root . schedule . Execute ( Update ) . StartingIn ( 0 ) ;
429469 }
430470
471+ // Converts Screen.safeArea insets (screen-space pixels) to root
472+ // VisualElement padding (panel layout units). Called by Update() on
473+ // every frame where the safe area rect changes — typically once on
474+ // startup and once per orientation change.
475+ //
476+ // _lastSafeArea is updated only on success so that a too-early call
477+ // (layout not yet settled) retries automatically next frame.
478+ private void ApplySafeArea ( )
479+ {
480+ if ( _root == null ) return ;
481+ var w = _root . layout . width ;
482+ var h = _root . layout . height ;
483+ if ( float . IsNaN ( w ) || w <= 0f || float . IsNaN ( h ) || h <= 0f ) return ;
484+
485+ var safe = Screen . safeArea ;
486+ var scaleX = w / Screen . width ;
487+ var scaleY = h / Screen . height ;
488+
489+ _root . style . paddingTop = ( Screen . height - safe . yMax ) * scaleY ;
490+ _root . style . paddingBottom = safe . y * scaleY ;
491+ _root . style . paddingLeft = safe . x * scaleX ;
492+ _root . style . paddingRight = ( Screen . width - safe . xMax ) * scaleX ;
493+
494+ _lastSafeArea = safe ;
495+ }
496+
431497 // ---- Typed-events accordion construction ----
432498
433499 // Each accordion's Send button captures its EventSpec + input dictionary
@@ -705,6 +771,12 @@ private void AppendLog(string label, string? body, LogLevel level, LogSource sou
705771
706772 var row = BuildLogRow ( new LogEntry ( DateTime . Now , label , body , level , source ) ) ;
707773 _logView . Add ( row ) ;
774+
775+ // Keep the visual tree bounded so scroll performance doesn't degrade
776+ // as the log grows. Remove the oldest row when the cap is exceeded.
777+ while ( _logView . contentContainer . childCount > MaxLogRows )
778+ _logView . contentContainer . RemoveAt ( 0 ) ;
779+
708780 _logCount . text = _logView . contentContainer . childCount . ToString ( CultureInfo . InvariantCulture ) ;
709781
710782 // contentContainer.flexShrink = 0 (set in BindElements) makes the
0 commit comments