Skip to content

Commit ffc2a52

Browse files
authored
Merge pull request #734 from immutable/feat/sdk-314-small-screen-ui
feat(audience-sample): mobile-responsive UI with safe area support (SDK-314)
2 parents 63e3bfa + dc8d528 commit ffc2a52

2 files changed

Lines changed: 202 additions & 7 deletions

File tree

examples/audience/Assets/SampleApp/Resources/AudienceSample.uss

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,116 @@
521521
width: 100%;
522522
}
523523

524+
/* Narrow breakpoint (< 1024 px) — .narrow is toggled on the root VisualElement
525+
(and on .sample-app-grid) by RegisterResponsiveLayout so selectors here reach
526+
the sticky header. Status bar switches to a vertical stack so UUID values
527+
never overflow sideways regardless of device width. */
528+
529+
.narrow .status-bar {
530+
padding-left: 16px;
531+
padding-right: 16px;
532+
}
533+
534+
.narrow .status-top-row {
535+
flex-direction: column;
536+
flex-wrap: nowrap;
537+
align-items: stretch;
538+
}
539+
540+
.narrow .status-group {
541+
flex-direction: column;
542+
align-items: stretch;
543+
margin-right: 0;
544+
min-width: 0;
545+
}
546+
547+
.narrow .status-cell {
548+
margin-right: 0;
549+
margin-bottom: 6px;
550+
min-width: 0;
551+
overflow: hidden;
552+
}
553+
554+
/* Value clips to available width — user taps to copy the full string. */
555+
.narrow .status-value {
556+
flex-grow: 1;
557+
flex-shrink: 1;
558+
overflow: hidden;
559+
min-width: 0;
560+
}
561+
562+
.narrow .status-vertical-rule {
563+
display: none;
564+
}
565+
566+
/* Phone breakpoint (< 480 px) — .phone is toggled on the root VisualElement
567+
by RegisterResponsiveLayout so selectors here reach the full page tree.
568+
Goal: LARGER text for legibility, 44 px touch targets throughout.
569+
Status bar vertical layout is already handled by .narrow above. */
570+
571+
.phone .sticky-top-main {
572+
padding-top: 12px;
573+
}
574+
575+
/* Slight title reduction frees header height; 24 px is still prominent. */
576+
.phone .title {
577+
font-size: 24px;
578+
}
579+
580+
581+
/* Status bar: bigger font on phone. */
582+
.phone .status-bar {
583+
padding-top: 12px;
584+
padding-bottom: 12px;
585+
font-size: 14px;
586+
}
587+
588+
.phone .status-cell {
589+
margin-bottom: 6px;
590+
}
591+
592+
/* Tabs: 13 px text, reduced letter-spacing and padding so all four fit on
593+
375 px while staying close to the 44 px touch-target guideline. */
594+
.phone .tab-bar .tab {
595+
padding-top: 13px;
596+
padding-bottom: 13px;
597+
padding-left: 10px;
598+
padding-right: 10px;
599+
font-size: 13px;
600+
letter-spacing: 0.3px;
601+
}
602+
603+
/* Buttons: 44 px minimum height and 14 px text throughout panels. */
604+
.phone Button {
605+
min-height: 44px;
606+
font-size: 14px;
607+
}
608+
609+
/* Section and field labels: bump from 11 px to 13 px. */
610+
.phone .section-label,
611+
.phone .field-label {
612+
font-size: 13px;
613+
}
614+
615+
/* Accordion headers: larger text, taller tap target. */
616+
.phone .accordion-title {
617+
font-size: 13px;
618+
}
619+
620+
.phone .accordion-header {
621+
padding-top: 14px;
622+
padding-bottom: 14px;
623+
}
624+
625+
/* Input fields: 14 px text and 44 px min-height for comfortable typing. */
626+
.phone .field .unity-text-field > .unity-text-field__input,
627+
.phone .field .unity-base-text-field__input {
628+
font-size: 14px;
629+
padding-top: 10px;
630+
padding-bottom: 10px;
631+
min-height: 44px;
632+
}
633+
524634
/* ---------- Panels ---------- */
525635

526636
.panel {

examples/audience/Assets/SampleApp/Scripts/AudienceSample.UI.cs

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)