Files
Chris Tate 04b97cc2b7 feat: add native code editor example (#245)
* feat: add native code editor example

- Add an editable syntax-highlighted code surface with robust selection, large-file rendering, and expanded language coverage.
- Add the declarative Code Editor example with folder navigation, previews, permanent tabs, inline rename, save, and multi-window support.
- Extend tree interactions, macOS folder picking, component docs, tests, and live WASM previews.

* fix: address code editor review findings

* fix code editor large-file stability

* fix: harden large code editor state

* fix: polish code editor interactions

* fix: title empty code explorer windows

* fix: stabilize code editor rendering

* fix: harden code editor edge cases

* fix: address code editor review findings

* fix: resolve remaining code editor review findings

* fix: harden code editing and click handling

* fix: balance code editor tree spacing

* fix: load code editor folders on demand
2026-07-31 20:09:24 -05:00

249 lines
15 KiB
Plaintext

<!-- The complete code-editor view. It is compiled into release builds and
watched for live reload in debug builds; Model/Msg state stays in Zig. -->
<column background="background">
<!-- The custom titlebar is the window's drag surface. Matching traffic-light
and action slots keep the folder centered in the whole window while
the icon-only Save action occupies the trailing edge. -->
<row height="{titlebar_height}" padding="12" cross="center" background="background" window-drag="true" label="Code editor titlebar">
<spacer width="{chrome_leading}" />
<spacer width="32" />
<text grow="1" wrap="false" text-alignment="center">{rootName}</text>
<spacer width="{chrome_leading}" />
<button width="32" height="32" size="icon" variant="ghost" icon="save" label="Save" disabled="{saveDisabled}" on-press="save_file"></button>
</row>
<separator />
<stack grow="1">
<if test="{folderOpen}">
<column>
<!-- Both panes and the divider are declarative; the split value lives
in the model so resizing survives every rebuilt view. -->
<split grow="1" value="{sidebar_fraction}" on-resize="sidebar_resized">
<column min-width="220" background="background" label="Code editor sidebar">
<scroll grow="1" overscroll="none">
<!-- The split divider owns a 9px hit band after this pane, with
its hairline centered in that band. Keep the tree flush to
the pane's trailing edge so the visible hairline balances
this 6px leading gutter instead of doubling it. -->
<column>
<spacer height="6" />
<row>
<spacer width="6" />
<tree grow="1" label="Files and folders">
<for each="visible" key="relative_path" as="v">
<if test="{v.directory}">
<list-item height="32" gap="8" cross="center" selected="{v.selected}" expanded="{v.expanded}" tree-level="{v.tree_level}" focus-ring="background" on-press="select_entry:{v.index}" on-change="select_entry:{v.index}" on-toggle="toggle_entry:{v.index}" on-submit="begin_rename:{v.index}" role="treeitem" label="{v.name}">
<spacer width="{v.indent}" />
<if test="{v.renaming}">
<text-field height="28" grow="1" text="{renameText}" autofocus="true" on-input="edit_rename" on-submit="commit_rename" label="Rename item" />
</if>
<else>
<icon name="{v.icon}" width="16" height="16" />
<text grow="1" wrap="false">{v.name}</text>
</else>
</list-item>
</if>
<else>
<list-item height="32" gap="8" cross="center" selected="{v.selected}" tree-level="{v.tree_level}" focus-ring="background" on-press="preview_entry:{v.index}" on-change="select_entry:{v.index}" on-double-press="pin_entry:{v.index}" on-submit="begin_rename:{v.index}" role="treeitem" label="{v.name}">
<spacer width="{v.indent}" />
<if test="{v.renaming}">
<text-field height="28" grow="1" text="{renameText}" autofocus="true" on-input="edit_rename" on-submit="commit_rename" label="Rename item" />
</if>
<else>
<icon name="{v.icon}" width="16" height="16" />
<text grow="1" wrap="false">{v.name}</text>
</else>
</list-item>
</else>
</for>
</tree>
</row>
<spacer height="6" />
</column>
</scroll>
</column>
<!-- A pane container keeps the split's direct-child contract while
swapping the editor empty/file states declaratively. -->
<stack min-width="360">
<if test="{selectedIsFile}">
<column>
<!-- App-authored flat editor tabs share one surface and height
so labels stay fixed when selection moves. The baseline
paints behind the tabs, while a single-click preview stays
italic; double-clicking the tree row pins it as normal text. -->
<stack height="37" background="background" label="Editor tab strip">
<column grow="1">
<spacer height="36" />
<separator />
</column>
<!-- The runtime minimally reveals a newly selected tab. A
stable scroll identity preserves the standing offset when
the active tab is already fully visible. -->
<scroll grow="1" axis="horizontal" overscroll="none" label="Open file tabs">
<row height="37">
<for each="openTabs" key="relative_path" as="tab">
<if test="{tab.selected}">
<stack width="180" height="37" background="background" selected="true" role="tab" label="{tab.name}" on-press="activate_tab:{tab.index}" on-double-press="pin_entry:{tab.index}" on-hover-enter="hover_tab:{tab.index}" on-hover-leave="unhover_tab:{tab.index}">
<row height="37" gap="6" cross="center">
<spacer width="10" />
<icon name="file-text" width="14" height="14" foreground="text_muted" />
<if test="{tab.preview}">
<text grow="1"><span italic="true">{tab.name}</span></text>
</if>
<else>
<text grow="1" wrap="false">{tab.name}</text>
</else>
<if test="{tab.dirty}">
<text foreground="accent">●</text>
</if>
<if test="{tab.show_close}">
<button width="22" height="22" size="icon" variant="ghost" icon="x" label="Close tab" on-press="close_tab:{tab.index}"></button>
</if>
<else>
<spacer width="22" />
</else>
<separator width="1" height="36" />
</row>
<context-menu>
<menu-item on-press="close_tab:{tab.index}">Close</menu-item>
<menu-item disabled="{tab.only_tab}" on-press="close_other_tabs:{tab.index}">Close Others</menu-item>
</context-menu>
</stack>
</if>
<else>
<stack width="180" height="37" background="background" foreground="text_muted" role="tab" label="{tab.name}" on-press="activate_tab:{tab.index}" on-double-press="pin_entry:{tab.index}" on-hover-enter="hover_tab:{tab.index}" on-hover-leave="unhover_tab:{tab.index}">
<row height="37" gap="6" cross="center">
<spacer width="10" />
<icon name="file-text" width="14" height="14" foreground="text_muted" />
<if test="{tab.preview}">
<text grow="1"><span italic="true">{tab.name}</span></text>
</if>
<else>
<text grow="1" wrap="false">{tab.name}</text>
</else>
<if test="{tab.dirty}">
<text foreground="accent">●</text>
</if>
<if test="{tab.show_close}">
<button width="22" height="22" size="icon" variant="ghost" icon="x" label="Close tab" on-press="close_tab:{tab.index}"></button>
</if>
<else>
<spacer width="22" />
</else>
<separator width="1" height="36" />
</row>
<column grow="1">
<spacer height="36" />
<separator />
</column>
<context-menu>
<menu-item on-press="close_tab:{tab.index}">Close</menu-item>
<menu-item disabled="{tab.only_tab}" on-press="close_other_tabs:{tab.index}">Close Others</menu-item>
</context-menu>
</stack>
</else>
</for>
</row>
</scroll>
</stack>
<if test="{previewState == 'text'}">
<column grow="1">
<!-- `code` requires a literal lexer name. Select one
declaratively from the Zig-derived file language. -->
<if test="{previewLanguage == 'zig'}">
<code source="{preview}" language="zig" editable="{codeEditable}" on-input="edit_code" line-numbers="true" wrap="false" grow="1" label="{selectedPath}" />
</if>
<if test="{previewLanguage == 'javascript'}">
<code source="{preview}" language="javascript" editable="{codeEditable}" on-input="edit_code" line-numbers="true" wrap="false" grow="1" label="{selectedPath}" />
</if>
<if test="{previewLanguage == 'typescript'}">
<code source="{preview}" language="typescript" editable="{codeEditable}" on-input="edit_code" line-numbers="true" wrap="false" grow="1" label="{selectedPath}" />
</if>
<if test="{previewLanguage == 'jsx'}">
<code source="{preview}" language="jsx" editable="{codeEditable}" on-input="edit_code" line-numbers="true" wrap="false" grow="1" label="{selectedPath}" />
</if>
<if test="{previewLanguage == 'tsx'}">
<code source="{preview}" language="tsx" editable="{codeEditable}" on-input="edit_code" line-numbers="true" wrap="false" grow="1" label="{selectedPath}" />
</if>
<if test="{previewLanguage == 'json'}">
<code source="{preview}" language="json" editable="{codeEditable}" on-input="edit_code" line-numbers="true" wrap="false" grow="1" label="{selectedPath}" />
</if>
<if test="{previewLanguage == 'yaml'}">
<code source="{preview}" language="yaml" editable="{codeEditable}" on-input="edit_code" line-numbers="true" wrap="false" grow="1" label="{selectedPath}" />
</if>
<if test="{previewLanguage == 'shell'}">
<code source="{preview}" language="shell" editable="{codeEditable}" on-input="edit_code" line-numbers="true" wrap="false" grow="1" label="{selectedPath}" />
</if>
<if test="{previewLanguage == 'python'}">
<code source="{preview}" language="python" editable="{codeEditable}" on-input="edit_code" line-numbers="true" wrap="false" grow="1" label="{selectedPath}" />
</if>
<if test="{previewLanguage == 'rust'}">
<code source="{preview}" language="rust" editable="{codeEditable}" on-input="edit_code" line-numbers="true" wrap="false" grow="1" label="{selectedPath}" />
</if>
<if test="{previewLanguage == 'c_like'}">
<code source="{preview}" language="c" editable="{codeEditable}" on-input="edit_code" line-numbers="true" wrap="false" grow="1" label="{selectedPath}" />
</if>
<if test="{previewLanguage == 'go'}">
<code source="{preview}" language="go" editable="{codeEditable}" on-input="edit_code" line-numbers="true" wrap="false" grow="1" label="{selectedPath}" />
</if>
<if test="{previewLanguage == 'html'}">
<code source="{preview}" language="html" editable="{codeEditable}" on-input="edit_code" line-numbers="true" wrap="false" grow="1" label="{selectedPath}" />
</if>
<if test="{previewLanguage == 'css'}">
<code source="{preview}" language="css" editable="{codeEditable}" on-input="edit_code" line-numbers="true" wrap="false" grow="1" label="{selectedPath}" />
</if>
<if test="{previewLanguage == 'sql'}">
<code source="{preview}" language="sql" editable="{codeEditable}" on-input="edit_code" line-numbers="true" wrap="false" grow="1" label="{selectedPath}" />
</if>
<if test="{previewLanguage == 'markdown'}">
<code source="{preview}" language="markdown" editable="{codeEditable}" on-input="edit_code" line-numbers="true" wrap="false" grow="1" label="{selectedPath}" />
</if>
<if test="{previewLanguage == 'plain'}">
<code source="{preview}" language="plain" editable="{codeEditable}" on-input="edit_code" line-numbers="true" wrap="false" grow="1" label="{selectedPath}" />
</if>
</column>
</if>
<else>
<column grow="1" main="center" cross="center" padding="24">
<if test="{previewState == 'loading'}">
<text foreground="text_muted">Loading…</text>
</if>
<if test="{previewState == 'binary'}">
<text foreground="text_muted">Binary file preview is not available.</text>
</if>
<if test="{previewState == 'failed'}">
<text foreground="text_muted">This file could not be opened.</text>
</if>
<if test="{previewState == 'idle'}">
<text foreground="text_muted">Select a file to preview it.</text>
</if>
</column>
</else>
</column>
</if>
<else>
<column grow="1" gap="8" main="center" cross="center" padding="24">
<icon name="file-text" width="36" height="36" foreground="text_muted" />
<text size="heading">{emptyEditorTitle}</text>
<text foreground="text_muted">{emptyEditorDetail}</text>
</column>
</else>
</stack>
</split>
<status-bar>{status}</status-bar>
</column>
</if>
<else>
<column grow="1" gap="14" main="center" cross="center" padding="32">
<icon name="folder-open" width="48" height="48" foreground="text_muted" />
<text size="heading">Browse a code folder</text>
<text foreground="text_muted">Choose a folder to inspect its files and source.</text>
<button variant="primary" size="lg" icon="folder-open" on-press="open_folder">Open Folder…</button>
</column>
</else>
</stack>
</column>