Skip to content

✨ GPUI desktop UI — scope-centric views, system tray, real-time tracing logs - #38

Merged
Reverier-Xu merged 38 commits into
masterfrom
feat/woocraft
Aug 22, 2026
Merged

✨ GPUI desktop UI — scope-centric views, system tray, real-time tracing logs#38
Reverier-Xu merged 38 commits into
masterfrom
feat/woocraft

Conversation

@Reverier-Xu

Copy link
Copy Markdown
Member

Summary

Replaces the Slint desktop UI with a GPUI + woocraft implementation, following the scope-centric architecture (get-started form, per-scope connections, network logs, settings). Includes:

  • System tray support — close-to-tray keeps the app resident, tray menu with Show/Quit, /popup restores the window, window handle restored from the tray when stale, close removes the window instead of minimizing.
  • Real-time tracing logs — logs stream live from the daemon into a readonly code editor, styled like the tracing-subscriber console output (dimmed timestamps/targets, level-colored tokens), plus a scrollbar preview minimap: a 1px-per-row strip colored by log level, windowed to the track height and following the scroll position.
  • Per-session log rotation — timestamped archives with lock cleanup.
  • Window fixes — frameless window move/resize works after title-bar interaction; log events coalesced so the tray popup is never starved.
  • Proxy instance persistence — instances survive window close while the daemon keeps running in the tray.

合入后可以关闭的工作项

此 PR 合入后完成以下工作项,可一并关闭:

Supersedes the earlier draft PR #29 (copilot/continue-refactor-gpui) and the placeholder PR #35 (master→master).

Testing

  • cargo test -p wsrx-desktop — 10/10 passing (log formatting, span parsing, scrollbar preview windowing)
  • cargo clippy -p wsrx-desktop — clean
  • woocraft bumped for the scrollbar preview strip API

- Bump version to 0.6.0 (rust-version 1.95)
- Replace the entire Slint-based desktop UI with Woocraft, the GPUI
  component library (git dependency)
- Port all existing functionality:
  * Frameless window with custom title bar, sidebar and page stack
  * Get Started page (interface selector, port, remote address, add instance)
  * Connections pages per scope (accept / decline / remove, instance list
    with latency and copy-on-click, close buttons)
  * Network logs page streaming JSON log entries from wsrx.log
  * Settings page (version/updates, language, logs export, support,
    system info, footer)
  * Background daemon: axum API server (pool/connect/popup/version),
    latency worker with pingfall, log streaming, GitHub update check,
    single-instance lock + /popup focus
  * config.toml / scopes.toml persistence, light/dark theme, en_US /
    zh_CN / zh_TW i18n
- Delete all Slint UI code and bundled icon assets; keep only the wsrx
  base library untouched
- Drop skia/slint build inputs from the Nix flake; add vulkan-loader
- Remove the sidebar header (logo + app title)
- Remove the sidebar collapse toggle button from the title bar and the
  related show-sidebar state entirely; the sidebar is always visible
- Rebuild the sidebar as a uniform p-1 gap-1 flex column where every
  entry, including the controller port row, is the same full-width flat
  woocraft Button (port number moved into the label)
The old behavior only wrote config.toml / scopes.toml on graceful window
close, so theme toggles and scope deletions were lost whenever the app
was killed (or the close handler did not run).

- Persist scopes.toml right after every scope mutation (API scope
  request/update, allow, remove)
- Persist config.toml right after theme / language changes
- Split persistence into async (tokio context, e.g. axum handlers) and
  sync (UI thread) variants: tokio panics on blocking_read inside a
  runtime, which crashed the API server on scope requests
Visual hierarchy now relies solely on font weight (bold) and opacity,
matching the previous Slint UI. Removed all text_sm / text_lg / text_2xl
modifiers so every text node renders at the theme default (16px).
The sidebar already has a right border, so the title bar now gets a
1px theme-colored divider on its bottom edge for a consistent look.
The hand-rolled log list could not scroll. Replace it with a readonly
woocraft CodeEditor (plain text mode, no borders / background), filling
the whole page. Incoming log entries are accumulated and pushed into
the editor via set_value on every log event.
…ort button

- Logs page: EditorState::set_value was a silent no-op while read_only
  was set, so the editor never displayed anything; flip read_only around
  each update
- All pages now use overflow_y_scrollbar() with the wrapper sized after
  the call (size_full on the scrollport), which is what actually gives
  the scroll container a bounded height; the previous call order left
  the wrapper unsized and content simply overflowed
- Connections page is now a single scroll container (header + instance
  list together) instead of a fixed header with an inner scroller
- Normalize page layouts to gap-1 (flex gaps between components)
- Sidebar: render the controller port number as a right-aligned flex
  child inside the button (flex-1 + text-right) instead of appending it
  to the label text
Remove the 1px divider lines and per-row vertical padding, which made
the effective row spacing ~17px instead of the 4px gap-1 used on every
other page. Rows are now plain gap-1 blocks.
Keep the 1px separators between sections while staying on the gap-1
layout (no per-row padding).
- Home page: restore vertical + horizontal centering (justify_center +
  items_center on a min-h-full content column) so the layout matches the
  original grid-aligned, page-centered design; min-h-full keeps it
  safely scrollable when the window is small
- Sidebar: replace border_r_1 with a dedicated 1px divider div (same as
  the title bar divider) so it renders with the exact theme border color
  instead of a border that could look darker against the sidebar
  background
Button::expand(true) uses width:100%, which resolves against the row
width and can collapse to the label width. Use flex_1() (flex-grow with
a zero basis) so the selector always fills the remaining row space,
keeping the grid alignment with the remote-address input below.
- The Button with expand(true) still would not stretch reliably; replace
  it with a real Input (flex-1, bordered) wrapped in a Popover whose
  content lists the network interfaces; clicking an entry sets the input
  value and dismisses the popover
- The input is now the single source of truth for the local address:
  the send action reads it directly
- Give the refresh button a border (outline) to match the bordered
  selector
Keep the manual tweaks (gap-8 page spacing, pt-6/pb-12 padding, success
update button, 420px form width, default refresh button) and revert the
local-address picker from an Input+Popover to a Button:
- .default().expand(true) + w_full inside the flex-1 wrapper so it fills
  the row like the remote-address input below
- the dropdown menu now pops downward (default TopLeft anchor instead of
  BottomLeft)
woocraft's dropdown builder has no access to the trigger bounds, so
capture the button's laid-out width via on_prepaint into a shared cell
and pin the PopupMenu width with min_w/max_w. The menu now matches the
stretched button exactly and stays in sync if the layout changes.
Drop the on_prepaint width-capture machinery in favor of a simple
min_w/max_w(px(282.)) on the PopupMenu.
…d subtitle

- Render the bug-reporting system info in a readonly code editor (same
  pattern as the logs page) instead of a styled div
- Pin the header motto and the two footer credit lines to English
  (remove them from the i18n table)
The desktop crate now embeds logo.png via a RustEmbed asset source and
serves it through woocraft's CombinedSource (woocraft built-ins first,
desktop assets second). The get-started and settings page headers render
img("logo.png") instead of IconName::GlobeStar.
Restore the two logo SVGs from history into the embedded desktop
assets and drop the PNG: the get-started and settings headers render
logo-stroked.svg (no background) via gpui's svg element, and the title
bar uses the with-background logo.svg (colorized off) as its icon.
gpui's svg() element renders a monochrome mask and paints nothing
without a text color, so the page logos were invisible. Switch them to
the img() element, which rasterizes SVGs with their original colors.
Also swap the variants: the page headers use the with-background
logo.svg, while the title bar uses the background-less
logo-stroked.svg.
Keep the manual formatting and warning fixes across daemon, i18n and the
UI modules; resolve the remaining clippy warning by bundling the
instance-row theme colors into an InstanceRowColors struct and dropping
unused parameters, so the whole workspace is clippy-clean.
- On startup (after the single-instance check): prune log archives older
  than 3 days and archive any stale wsrx.log from a crashed session, so
  the live log (and the log viewer) only ever shows the current session
- On shutdown: archive the session's wsrx.log to wsrx-<unix-ts>.log
  (plain integer timestamp) and remove the single-instance lock; both
  are also run from an on_app_quit observer as a safety net if the
  window-close handler is bypassed, and both are idempotent
- Remove the old behavior that wiped the entire logs directory
- Enlarge the get-started logo to 96px
- Render the logs in a custom readonly editor backend (LogBackend) with
  a LogHighlighter that colors the level token of each line using the
  default tracing-subscriber console palette (TRACE purple, DEBUG blue,
  INFO green, WARN yellow, ERROR red)
- Format log lines to match the tracing console output
  (2026-08-22T14:07:00.123456Z  INFO target: message) instead of raw JSON
- The backend is a rope-based EditorBackend reusing RopeEditorSnapshot;
  set_value still works via the read-only flip
The editor builds text runs from highlight spans with a cumulative
cursor, so they must form a contiguous partition of the visible text.
Returning only the level tokens left gaps and misaligned positions,
which limited the color to the first line. Now the highlighter fills
the gaps with default-style spans (offsets relative to the visible
range), coloring the level of every line.
- Add the  feature to the woocraft git dependency (tray component
  ported from gpui-tray, MPL-2.0)
- Enable the tray when the running-in-tray setting is on: logo icon +
  Show / Quit context menu, left-click pops the window back up
- Closing the window now minimizes to the tray instead of quitting when
  the setting is enabled; tray Quit (or the tray event channel) performs
  a real shutdown
- The settings toggle is now functional and persists the setting
The woocraft title bar's own close button called remove_window()
directly, bypassing on_window_should_close, so clicking X always quit
the app even with the tray enabled. Route it through the same logic as
a window-manager close: minimize to the tray when running_in_tray is
set, otherwise shut down.

Also update the woocraft git dep for the non-blocking tray backend and
the atomic D-Bus name+serve (fast startup, no zbus warning).
minimize_window() left a taskbar entry; true close-to-tray must remove
the window entirely. Set QuitMode::Explicit so the app keeps running
when the last window closes, close the window (WM close or the title
bar button) without quitting when running_in_tray is on, and reopen the
window from the tray / /popup via a shared window handle.
A log flood (e.g. naga debug output) queued thousands of Log events in
the UI channel, burying Popup/Quit and freezing the main thread with a
full editor set_value per line. The event pump now batch-drains pending
Log events into a single UiEvent::Logs batch, handles critical events
immediately, and the editor is synced once per batch (1000-line cap).
Close-to-tray removes the window but the shared window handle stays
pointing at the removed window, so popup always failed silently on the
stale handle and never reached the reopen branch. Treat a failed handle
update as 'window missing' and reopen it.
A reopened RootView started from defaults: the Online event is only sent
once at startup and logs sent while the window was hidden were dropped,
so the sidebar showed no port and the log page was empty. Keep the API
port, update flag and a capped log buffer in ServerState; api::serve
records the port, the event pump persists logs/updates into state, and
RootView::view restores all of them (and seeds the editor) on reopen.
- Highlight timestamps and targets dimmed, levels in tracing console colors
- Render a 1px-per-row preview strip in the scrollbar (level-colored),
  windowed to the track height and following the scroll position
- Bump woocraft for the scrollbar preview strip API
- scrollbar preview is now a 6px column near the track's inner edge
  (backend-controlled geometry), leaving room for future git/LSP status
  strips to share the scrollbar
- bump woocraft for ScrollbarPreview geometry + borderless thumb
- add the scrollbar preview design doc + python simulation
@Reverier-Xu Reverier-Xu changed the title feat: GPUI desktop UI — scope-centric views, system tray, real-time tracing logs ✨ GPUI desktop UI — scope-centric views, system tray, real-time tracing logs Aug 22, 2026
@Reverier-Xu
Reverier-Xu merged commit e482b17 into master Aug 22, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

桌面ui双击后无法移动 System tray with slint UI

1 participant