Repository navigation
UI elements: progress, scroll, controls, images, menus, windows from code and drops - #16
Merged
Merged
Conversation
`progress(value:)` fills to an int percentage that stays up to date, kept between 0 and 100; `progress()` animates to show work under way. It is NSProgressIndicator's bar on macOS and the system's msctls_progress32 on Windows, smooth with a value and a marquee without one. Logic's `Text` becomes `Value`, since UI now shows numbers as well as text, and gains `number`. Compiled apps read the percentage through the template's `value` function; the runtime ABI goes to version 2. The downloader fixture shows a bar per download, and the gallery shows both.
`scroll(...)` stacks its children top to bottom, like `column` without `grow`, in the space its parent has left, and scrolls them up and down when they're too tall. It adds nothing to its parent's size, as spacer doesn't, so it belongs straight in a window or in a growing stack. Widgets are now created inside their parent: Ui's constructors take the Position they go at, and backends receive a Parent, the window or a scroll area's content, so native views nest. Layout gives a scroll node overflow and the scroll bar's width, reports frames relative to the scroll content, and sizes that content. The width is asked at every layout, since it changes with the user's settings; macOS lays windows out again when the preferred scroller style changes. macOS uses NSScrollView with a flipped document view. Windows gets a NiftyScroll child window with a vertical scroll bar that always shows, which hosts its controls, paints its own surfaces, moves its children with ScrollWindowEx and passes their notifications on to the window. Surfaces move to a Surfaces type both share. The gallery and the downloader's list now scroll.
`switch(on: $value, title:)` turns a bool on and off with the switch at the end of its title; it shares the checkbox's node and widget, told apart by a ToggleStyle. `slider($value, min:, max:)` picks a whole number in a range written literally, 0 to 100 by default. `picker($selection, options:)` picks a string from a list that stays up to date. Logic gains read_number, write_number and texts; the engine's input handlers now share one dispatch. Binding errors name the parameter they expect, such as `on: $name`. Compiled templates gain min and max, and kinds for each control. macOS uses NSSwitch beside a label, NSSlider and NSPopUpButton, whose items go through its menu so equal titles stay. Windows draws Fluent's toggle switch on a checkbox, a trackbar's track and thumb through custom draw, and a picker as a button that opens a Win32 pop-up menu. The syntax decisions are recorded in the language reference.
`button(title, selected: condition)` shows the button as selected while the condition holds, so a row of buttons whose actions set one state picks one of a few choices. Clicking never selects a button by itself; it shows what `selected` says once the action has run. A `.primary` button can't take it, being the window's default action. Logic gains `flag`. This fits both platforms better than a segmented control, which Win32 doesn't have: macOS uses on-off push buttons, put back to the selection after AppKit flips them on click, and Windows push-like checkboxes, which screen readers announce as pressed, drawn as Fluent's toggle buttons. The reasoning is a new row in the syntax decisions. Icon-only buttons on macOS no longer keep their title in the button, since setting it once shown drew it over the icon; it stays their tooltip and accessibility label.
`text_field($value, lines: n)` makes a text field n lines tall, from 1, the default, to 100. With more than one line it wraps, scrolls, and takes Return as a new line; bound text ends its lines in \n on both platforms. Neither platform's multi-line text box has a placeholder, so the checker keeps `placeholder` to one-line fields. macOS uses a text view in a bezel-bordered scroll view, sized from its font's line height; Windows a multi-line edit control with a scroll bar, whose \r\n line ends are turned into \n as the user types. Compiled templates carry `lines`.
`image(path, width:, height:)` shows the image in a file as large as fits its frame, keeping its shape; fixing one side sizes the other from the image, and with neither it shows at its own size. The path is text that stays up to date, so an image can show a file the app downloaded or the user picked; `~` stands for the home folder. A file that can't be read shows nothing, at the size given. Paths must be full or start with `~`: bundling images with an app needs `nf build` to carry them and a rule for finding them, which is left for later, so a relative path written directly is an error. ImageSize, shared by both backends, works out the frame and the fit. macOS uses NSImageView, scaling proportionally. Windows reads images with the Windows Imaging Component into memory and draws them into an owner-drawn static control, scaled once per size and alpha-blended over the surface under it. The gallery picks an image to show.
A one-line `text_field` takes an optional trailing block, run when the user presses Return in it, in place of the window's primary button; a field without one leaves Return to the primary button as before. A field with `lines:` takes Return as a new line, so it can't have one. The element table marks the block as an optional action, so the language server completes the field without a block and treats one as an action. macOS's text field delegate answers insertNewline: with the action and reports it handled. On Windows the edit control gets a subclass that asks the dialog manager for Return, runs the action on the key and swallows the character that follows. The downloader starts a download from its address field.
A top-level `menu(title) { … }` is a menu of the app's menu bar, holding
`menu_item(title, shortcut:) { action }`s, `divider()`s and nested menus.
A shortcut is written as a key after `shift+` or `alt+`, and always uses
the platform's command key: "n" is Cmd+N on macOS and Ctrl+N on Windows.
The checker refuses the system's own shortcuts (Quit and the editing
commands), and two items sharing one.
Elements now say where they may be written, so menu elements are refused
in windows and the language server offers each place only what fits it.
The runtime owns Key, Shortcut, SystemCommand and the generic Menu type,
which the checker uses for its menus too. Engine::start and reload take an
AppTemplate of windows and a menu bar, whose item actions share a frame.
Compiled programs describe their menus in the descriptor.
macOS builds the main menu: the app menu with Quit, the app's first menu,
the Edit menu from the same system commands, then the rest, with key
equivalents and a target that runs items by tag. Windows gives every
window a menu bar, keeping its content size, routes menu commands that
come with no control, and turns shortcuts into commands with an
accelerator table before the dialog manager sees the keys. The gallery
has a menu.
A context_menu() { … } child of a stack holds the same items, dividers and submenus as a menu bar menu, without shortcuts. It takes no room in layout and covers its whole stack; the innermost stack's menu shows. Its items run with the names around them, so each row of a list gets a menu for its own item.
macOS: the views widgets sit in answer menuForEvent: with the smallest hidden menu area under the click. Windows: hosts answer WM_CONTEXTMENU from their areas, from the mouse or the keyboard, with TrackPopupMenuEx; surfaces and menu areas share one Areas list.
A window takes a name between `window` and its title, as in `window settings("Settings", open: false) { … }`, and `open_window(settings)` and `close_window(settings)` open and close it once the event finishes; `open: false` keeps a window closed until code opens it. Opening an open window brings it to the front, the last request for a window decides, and windows open before others close.
The engine keeps a slot per window template and builds a window's content each time it opens. A user's close request now reaches the app through the backend (windowShouldClose: on macOS, WM_CLOSE on Windows), which closes the window the same way code does, so its widgets go with it. Reloading keeps open windows open and closed ones closed.
The downloader moves its folder into a settings window, opened from a button and a Settings… menu item.
A drop_target($dropped) { … } child of a stack takes files and links dragged onto it from other apps: a drop writes a path for each file and an address for each link into the bound [string], then runs the optional block. Like context_menu it covers its stack without taking room, the innermost stack takes the drop, and a stack has one.
macOS: the views widgets sit in are dragging destinations for file URLs and URLs once they hold a drop area, and find the innermost area under the drag, there or in the views around; context menus and drop areas share one lookup. Windows: hosts register an OLE IDropTarget, which reads CF_HDROP files and UniformResourceLocatorW links; the backend now initializes OLE rather than plain COM, and surfaces, context menus and drop areas share one Areas list.
The gallery takes a dropped image, and the downloader downloads dropped links.
On Apple Silicon a direct call reaches only ±128 MiB. The JIT allocated each function separately, so a call could land out of range and trip Cranelift's relocation assert, failing failures_match_the_interpreter about one run in six. One reserved arena per program keeps every call in range. Co-Authored-By: Claude <noreply@anthropic.com>
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the UI elements a full app like a download manager needs. Each one reaches the checker, interpreter, compiled apps, both backends,
FakeBackend, the LSP, the four highlighters, the reference docs, fixtures and the roadmap. Syntax decisions are recorded as rows 12–22 inlanguage.md, and row 4 (named windows) is updated.What's in it
progress(value:); no value means busyscroll { … }switch(on: $b, title:),slider($n, min:, max:),picker($s, options:)button(…, selected: cond)text_field($t, lines: 3)image(path, width:, height:); the path must be absolute or start with~text_field($q) { … }, run on Returnmenu("File") { menu_item("New", shortcut: "shift+n") { … } divider() menu("Sub") { … } }context_menu() { … }as a child of a stack; the innermost stack's menu winswindow settings("Settings", open: false) { … },open_window(settings),close_window(settings)drop_target($dropped) { … }: writes a[string], then runs the blockDesign decisions to review
ident ident(…)at the top level. Onlywindowtakes a name; any other element given one is an error.nf devreload: open windows stay open and closed ones stay closed.nifty-windowsnow depends onwindows-coredirectly, because theimplementmacro needs it.scrolltakes the space left in its stack.Checked
make checkpasses: fmt, clippy (-D warnings) on macOS and with--target x86_64-pc-windows-msvc, build,cargo test --workspace, the tree-sitter corpus (28/28) and the VS Code grammar tests.menuForEvent:Not yet checked by hand
macOS:
.primarybutton.Windows (nothing has run yet, only been linted):
WM_CONTEXTMENU).WM_CLOSE), and a Done button that closes its own window during itsWM_COMMAND.SetForegroundWindowbringing an open window to the front.IDropTarget).OleInitializeinstead ofCoInitializeEx.Follow-ups
revineditors/zed/extension.tomlafter this lands. The windows and drop commits change the Tree-sitter grammar.