@@ -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.
@@ -73,7 +75,7 @@ private static readonly (string TabId, string PanelId)[] Tabs =
7375 // ---- UXML element fields (Tabs + status bar + header) ----
7476
7577 private readonly List < Button > _tabButtons = new List < Button > ( ) ;
76- private Label _prodWarning , _sdkVersionLabel ;
78+ private Label _prodWarning , _sdkVersionLabel , _titleLabel ;
7779 private Label _statusEndpoint , _statusConsent , _statusAnon , _statusUser , _statusSession , _statusQueue ;
7880
7981 // ---- UXML element fields (Log pane) ----
@@ -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
@@ -152,6 +166,7 @@ private void BindElements()
152166 {
153167 _prodWarning = Require < Label > ( "prod-warning" ) ;
154168 _sdkVersionLabel = Require < Label > ( "sdk-version" ) ;
169+ _titleLabel = _root . Q < Label > ( className : "title" ) ;
155170
156171 _statusEndpoint = Require < Label > ( "status-endpoint" ) ;
157172 _statusConsent = Require < Label > ( "status-consent" ) ;
@@ -223,6 +238,11 @@ private void BindElements()
223238 // scroller never engages.
224239 _logView . contentContainer . style . flexShrink = 0 ;
225240
241+ // Tell the renderer the log subtree moves as a unit when scrolled.
242+ // Without this hint Unity re-composites every row on each scroll
243+ // frame; with it the subtree is translated on the GPU instead.
244+ _logView . contentContainer . usageHints = UsageHints . GroupTransform ;
245+
226246 _logCount = Require < Label > ( "log-count" ) ;
227247 }
228248
@@ -403,31 +423,90 @@ void Update()
403423 if ( float . IsNaN ( needed ) || needed <= 0f ) return ;
404424 pageScroll . contentContainer . style . minHeight = needed ;
405425 }
426+ pageScroll . contentContainer . usageHints = UsageHints . GroupTransform ;
406427 controls . RegisterCallback < GeometryChangedEvent > ( _ => Update ( ) ) ;
407428 logCol . RegisterCallback < GeometryChangedEvent > ( _ => Update ( ) ) ;
408429 pageScroll . contentContainer . schedule . Execute ( Update ) . StartingIn ( 0 ) ;
409430 }
410431
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.
432+ // Mirrors the web sample's `@media` breakpoints. USS 2021.3 has no
433+ // @media; toggle class names via GeometryChangedEvent instead.
434+ // .narrow on sample-app-grid AND _root : < 1024 px — stacks grid
435+ // columns vertically and
436+ // switches status bar to a
437+ // vertical stack so UUIDs
438+ // never overflow sideways.
439+ // .phone on _root : < 480 px — larger text,
440+ // 44 px touch targets.
441+ // Each breakpoint is evaluated independently so toggling one does not
442+ // suppress the other. Both are idempotent — only mutate on a boolean flip.
414443 private void RegisterResponsiveLayout ( )
415444 {
416445 var grid = Require < VisualElement > ( "sample-app-grid" ) ;
446+ // Snapshot title text from UXML so we can restore it on widen.
447+ var baseTitle = _titleLabel ? . text ?? "" ;
417448 void Update ( )
418449 {
419450 var w = _root . layout . width ;
420451 if ( float . IsNaN ( w ) || w <= 0f ) return ;
452+
421453 var shouldBeNarrow = w < NarrowBreakpointPx ;
422454 var isNarrow = grid . ClassListContains ( "narrow" ) ;
423- if ( shouldBeNarrow == isNarrow ) return ;
424- if ( shouldBeNarrow ) grid . AddToClassList ( "narrow" ) ;
425- else grid . RemoveFromClassList ( "narrow" ) ;
455+ if ( shouldBeNarrow != isNarrow )
456+ {
457+ grid . EnableInClassList ( "narrow" , shouldBeNarrow ) ;
458+ // Also on _root so .narrow selectors can reach the sticky
459+ // header (status bar) which sits outside .sample-app-grid.
460+ _root . EnableInClassList ( "narrow" , shouldBeNarrow ) ;
461+
462+ // On narrow screens merge the version into the title as
463+ // plain text and hide the badge so the version reads inline.
464+ if ( _titleLabel != null )
465+ _titleLabel . text = shouldBeNarrow
466+ ? $ "{ baseTitle } { _sdkVersionLabel . text } "
467+ : baseTitle ;
468+ _sdkVersionLabel . style . display = shouldBeNarrow
469+ ? DisplayStyle . None
470+ : DisplayStyle . Flex ;
471+ }
472+
473+ var shouldBePhone = w < PhoneBreakpointPx ;
474+ var isPhone = _root . ClassListContains ( "phone" ) ;
475+ if ( shouldBePhone != isPhone )
476+ {
477+ _root . EnableInClassList ( "phone" , shouldBePhone ) ;
478+ }
426479 }
427480 _root . RegisterCallback < GeometryChangedEvent > ( _ => Update ( ) ) ;
428481 _root . schedule . Execute ( Update ) . StartingIn ( 0 ) ;
429482 }
430483
484+ // Converts Screen.safeArea insets (screen-space pixels) to root
485+ // VisualElement padding (panel layout units). Called by Update() on
486+ // every frame where the safe area rect changes — typically once on
487+ // startup and once per orientation change.
488+ //
489+ // _lastSafeArea is updated only on success so that a too-early call
490+ // (layout not yet settled) retries automatically next frame.
491+ private void ApplySafeArea ( )
492+ {
493+ if ( _root == null ) return ;
494+ var w = _root . layout . width ;
495+ var h = _root . layout . height ;
496+ if ( float . IsNaN ( w ) || w <= 0f || float . IsNaN ( h ) || h <= 0f ) return ;
497+
498+ var safe = Screen . safeArea ;
499+ var scaleX = w / Screen . width ;
500+ var scaleY = h / Screen . height ;
501+
502+ _root . style . paddingTop = ( Screen . height - safe . yMax ) * scaleY ;
503+ _root . style . paddingBottom = safe . y * scaleY ;
504+ _root . style . paddingLeft = safe . x * scaleX ;
505+ _root . style . paddingRight = ( Screen . width - safe . xMax ) * scaleX ;
506+
507+ _lastSafeArea = safe ;
508+ }
509+
431510 // ---- Typed-events accordion construction ----
432511
433512 // Each accordion's Send button captures its EventSpec + input dictionary
@@ -705,6 +784,12 @@ private void AppendLog(string label, string? body, LogLevel level, LogSource sou
705784
706785 var row = BuildLogRow ( new LogEntry ( DateTime . Now , label , body , level , source ) ) ;
707786 _logView . Add ( row ) ;
787+
788+ // Keep the visual tree bounded so scroll performance doesn't degrade
789+ // as the log grows. Remove the oldest row when the cap is exceeded.
790+ while ( _logView . contentContainer . childCount > MaxLogRows )
791+ _logView . contentContainer . RemoveAt ( 0 ) ;
792+
708793 _logCount . text = _logView . contentContainer . childCount . ToString ( CultureInfo . InvariantCulture ) ;
709794
710795 // contentContainer.flexShrink = 0 (set in BindElements) makes the
0 commit comments