Fit any window size
Fit your UI to any window, zoom and screen: follow the size you are given, keep a shape, move parts between layouts, lay out for a design size, and go full screen.
Measure in UI px
Your UI is measured in UI px: the window's logical pixels divided
by the zoom, a comfort setting the person using the workstation picks.
At 100% a UI px is one CSS px in a browser and one logical pixel
natively; at 125% a button 100 UI px wide spans 125 logical pixels.
Sizes, offsets, screen.width and UiElement.bounds are all in UI px,
so a layout looks the same at every zoom.
Coordinates and units covers every frame a
position can be in.
Your app's datumhue.screen is its own window, which its window manager
sizes; for the root app it is the workstation window
(The app window).
Follow the size you are given
local ui = datumhue.ui
local screen = datumhue.screen
local token = datumhue.theme.token
local page = ui.root:panel({
width = "100%",
height = "100%",
gap = 12,
})
local menu = page:panel({ gap = 8, color = token("surface_raised") })
menu:button({ text = "Home" })
menu:button({ text = "Search" })
if screen.fullscreen_available then
menu:button({
text = "Full screen",
on_click = function()
if screen.is_fullscreen then
screen.fullscreen(false)
else
screen.fullscreen("borderless")
end
end,
})
end
local main = page:panel({
flex_grow = 1,
color = token("surface_sunken"),
})
local info = main:label({ text = "" })
ui.root:on_resize(function(size)
if size == nil then
return
end
local wide = size.x >= 720
page:update({ flex_direction = wide and "row" or "column" })
menu:update({
flex_direction = wide and "column" or "row",
width = wide and 200 or "auto",
})
info.text = string.format(
"%.0f x %.0f UI px at %.0f%% zoom",
size.x,
size.y,
screen.zoom * 100
)
end)
In this example: datumhue.ui datumhue.screen datumhue.theme.token datumhue.ui.root UiElement:panel UiElement:button datumhue.screen.fullscreen_available datumhue.screen.is_fullscreen datumhue.screen.fullscreen UiElement:label UiElement:on_resize UiElement:update UiElement.text datumhue.screen.zoom
ui.root:on_resizeruns once after the first layout, then each time your app's size changes: the window, the zoom, or a window manager resizing your app.- From 720 UI px wide the menu sits beside the content; narrower, it runs across the top.
- The Full screen button shows only where the workstation can go full screen. In the root app it turns full screen on and off; under a window manager, each press asks for your window to be maximized.
Keep a shape
aspect_ratio is an element's width over its height. A width or height
you leave automatic follows the other through the ratio, and a minimum
or maximum you set on only one of them carries over to the other. Two
forms lose the shape:
- Limits on both. With
max_widthandmax_heightboth set, each holds only its own size, so once both apply, the element comes out wider or taller than its ratio. - Shrinking. An element that shrinks to make room for the others in its column gets shorter and keeps its width.
To fit the largest box of a shape in both the width and the height you have, such as a stage above a row of buttons, use two panels. The frame takes the width, up to the widest stage, and is as tall as the shape makes that width; it shrinks to the height the buttons leave and never grows. The stage inside it takes the frame's height, and the ratio gives its width.
local ui = datumhue.ui
local token = datumhue.theme.token
local RATIO = 4 / 3
local WIDEST = 960
local NARROWEST = 160
local page = ui.root:panel({
width = "100%",
height = "100%",
align_items = "center",
gap = 12,
overflow = "scroll_y",
})
-- The room for the stage: the page's width, as tall as the shape
-- makes it. It shrinks to the height the buttons leave.
local frame = page:panel({
width = "100%",
max_width = WIDEST,
max_height = WIDEST / RATIO,
min_height = NARROWEST / RATIO,
aspect_ratio = RATIO,
flex_shrink = 1,
align_items = "center",
justify_content = "center",
padding = 0,
border_radius = 0,
})
-- The largest box of the shape in the room: the room's height, and
-- the width that height gives.
local stage = frame:panel({
height = "100%",
min_width = NARROWEST,
aspect_ratio = RATIO,
color = token("surface_raised"),
})
local buttons = page:panel({
flex_direction = "row",
gap = 8,
padding = 0,
})
buttons:button({ text = "Run" })
buttons:button({ text = "Reset" })
In this example: datumhue.ui datumhue.theme.token datumhue.ui.root UiElement:panel UiElement:button
- The frame's
max_heightis itsmax_widthover the ratio, so its two limits agree. - At every window size the stage keeps its shape in the middle of the frame, and the buttons sit right under it. No code runs on a resize.
- The frame's
min_heightand the stage'smin_widthkeep the stage at least 160 by 120 UI px. In a window too short for that, the page scrolls.
Put a part in another place
A layout for another size can need a part somewhere else:
UiElement:reorderputs a parent's children in the order a layout needs. The elements stay, with their focus, scroll and state, and the Tab order and the arrow keys follow the new order.- An element stays in the element it was built in. For a light part
that sits in a different parent in each layout, such as a button or a
line of text, build one in each place. Show the one the layout needs
with
display. When the hidden one held focus, focus the one you show, so a keyboard or gamepad user keeps their place. - For a part with more in it, keep what it shows as data. Remove it with
UiElement:removeand build it again in its new place. - An app hosted in an element ends when the element is removed, so offer to run it again (Run a child inside your layout).
local ui = datumhue.ui
local token = datumhue.theme.token
local page = ui.root:panel({
width = "100%",
height = "100%",
gap = 12,
})
local main = page:panel({
flex_grow = 1,
gap = 8,
color = token("surface_sunken"),
})
local side = page:panel({ gap = 8, color = token("surface_raised") })
-- A Review button in each place; the layout shows one of them.
local function review(parent)
return parent:button({
text = "Review 3 cards",
align_self = "flex_start",
})
end
main:label({ text = "Today" })
local in_main = review(main)
local in_side = review(side)
side:label({ text = "Units" })
ui.root:on_resize(function(size)
if size == nil then
return
end
local wide = size.x >= 720
local shown = wide and in_side or in_main
local hidden = wide and in_main or in_side
page:update({ flex_direction = wide and "row" or "column" })
page:reorder(wide and { side, main } or { main, side })
local had_focus = ui.focused == hidden
hidden.display = "none"
shown.display = "flex"
if had_focus then
shown:focus()
end
end)
In this example: datumhue.ui datumhue.theme.token datumhue.ui.root UiElement:panel UiElement:button UiElement:label UiElement:on_resize UiElement:update UiElement:reorder datumhue.ui.focused UiElement:focus
- From 720 UI px wide the side column sits on the left; narrower, it
goes under the main panel.
reorderswaps the two panels. - The Review button shows in the side column on a wide window and under Today on a narrow one.
Lay out for a design size
Four requests shape the space your UI is laid out in:
| Request | Made by | Changes |
|---|---|---|
screen.request_resolution | your app, at start | the window's size; a browser page sets its own |
screen.request_design_resolution | your app, at start | the size of the canvas your UI is laid out on |
screen.request_stretch_mode | your app, at start | how that canvas fits the window |
screen.request_zoom | the person using your app | the comfort zoom on top of all three |
Each is a request, not a setting: your app's request goes to its window
manager, which answers it. Read what is in effect from screen.width,
screen.height, screen.design_resolution, screen.stretch_mode and
screen.zoom. Under a design resolution, a UI px is a pixel of the
design canvas as scaled to fit, times the zoom. The display's own scale,
such as a high-density screen's, always applies on top.
Go full screen
screen.fullscreen takes a full-screen mode, or false to leave it.
For the root app it changes the workstation window; any other app asks
its window manager to maximize its window instead.
- Read
screen.fullscreen_availablebefore you offer full screen. Where the host has none, as on iPhone Safari, the root app's call raises for a full-screen mode. screen.is_fullscreenreports whether the workstation window is in full screen, including exits your app did not make; a maximized app window does not count. Thefullscreenevent reports each change.- In a Chromium-based browser in full screen, a press of Escape reaches your app and holding Escape leaves full screen. The browser can first ask the person using the workstation to allow this. Where they decline, and in other browsers, Escape leaves full screen.
In a browser
In a browser, a few things happen only right after a tap, a click or a key press: opening a link or a file dialog, copying with a copy button, and entering full screen. A gamepad press does not count, and neither does a press from too long ago.
When one of these has no such press behind it, the workstation shows a small Continue chip in the middle of the app that asked. The operation runs when the person taps, clicks or presses Enter on the chip. Natively every press counts and the chip never shows, so your app does nothing for it either way.
- The chip takes keyboard and gamepad focus when it appears while its
app is in front, whatever
ui.focus_navigationsays. - Escape, gamepad B or the browser's Back dismisses the chip, and the
operation does not happen:
on_copyoron_pickdoes not fire. Focus goes back where it was. - A newer such operation from the same app replaces the one waiting.
Opening the sign-in window needs such a press too. A sign-in whose
chip is dismissed ends as
cancelled. One with no window to show its chip in, as while the root
app is a service app, ends as flow_unavailable, and one whose window
the browser blocks ends as popup_blocked.
See also
- Lay out a screen: rows, columns and space.
- Coordinates and units: UI px and every other frame.
- Write a window manager or launcher: answer an app's size and full-screen requests.
datumhue.screen: every screen property and request.
Reference used on this page: datumhue.ui datumhue.screen datumhue.theme.token datumhue.ui.root UiElement:panel UiElement:button datumhue.screen.fullscreen_available datumhue.screen.is_fullscreen datumhue.screen.fullscreen UiElement:label UiElement:on_resize UiElement:update UiElement.text datumhue.screen.zoom UiElement:reorder datumhue.ui.focused UiElement:focus datumhue.screen.request_resolution datumhue.screen.request_design_resolution datumhue.screen.request_stretch_mode datumhue.screen.request_zoom datumhue.screen.width UiElement.bounds datumhue.input.mouse_position datumhue.input.touches datumhue.input.app_mouse_position UiElement:remove datumhue.screen.height datumhue.screen.design_resolution datumhue.screen.stretch_mode datumhue.ui.focus_navigation