Compare commits
371
Commits
No files matched your search
@@ -50,7 +50,7 @@ body:
|
|||||||
id: version
|
id: version
|
||||||
attributes:
|
attributes:
|
||||||
label: Commit or version
|
label: Commit or version
|
||||||
placeholder: 259ee2a
|
placeholder: 4f0b372
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: logs
|
id: logs
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
# One leg per runner registered with git.zacharyzhang.com; macOS arm64 is the crate job above.
|
os: [ubuntu-24.04, ubuntu-24.04-arm, windows-latest, windows-11-arm, macos-15-intel]
|
||||||
os: [ubuntu-24.04, windows-latest]
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
@@ -31,15 +30,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
toolchain: "1.95"
|
toolchain: "1.95"
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- if: runner.os != 'Linux'
|
- run: cargo test --lib --features test-support --quiet
|
||||||
run: cargo test --lib --features test-support --quiet
|
|
||||||
# The Linux job container runs as root, which ignores the file permissions the tests check.
|
|
||||||
- if: runner.os == 'Linux'
|
|
||||||
run: |
|
|
||||||
useradd --create-home ci
|
|
||||||
chmod o+x "$HOME"
|
|
||||||
chown -R ci "$GITHUB_WORKSPACE" "$HOME/.cargo" "$HOME/.rustup"
|
|
||||||
runuser -u ci -- env HOME=/home/ci CARGO_HOME="$HOME/.cargo" RUSTUP_HOME="$HOME/.rustup" PATH="$PATH" cargo test --lib --features test-support --quiet
|
|
||||||
- run: cargo build --example gallery
|
- run: cargo build --example gallery
|
||||||
|
|
||||||
site:
|
site:
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
name: Showcase
|
|
||||||
|
|
||||||
# Moves the site's showcase pin to the Gitea catalog's main once its checks pass, then deploys.
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "*/30 * * * *"
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
deploy:
|
|
||||||
description: Build and deploy even if the pin is current
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: showcase
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
env:
|
|
||||||
CATALOG: ZacharyZhang-NY/Ely-GPUI-Showcases
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
commit: ${{ steps.head.outputs.commit }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- id: head
|
|
||||||
env:
|
|
||||||
API: ${{ github.server_url }}/api/v1/repos/${{ env.CATALOG }}
|
|
||||||
FORCE: ${{ inputs.deploy }}
|
|
||||||
run: |
|
|
||||||
head=$(curl -fsS "$API/branches/main" | jq -r .commit.id)
|
|
||||||
pinned=$(tr -d '[:space:]' < frontend/showcase.lock)
|
|
||||||
if [ "$head" = "$pinned" ]; then
|
|
||||||
echo "showcase: pinned at ${head:0:7}, the catalog's main"
|
|
||||||
[ "$FORCE" = "true" ] && echo "commit=$head" >> "$GITHUB_OUTPUT"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
status=$(curl -fsS "$API/commits/$head/status")
|
|
||||||
total=$(echo "$status" | jq .total_count)
|
|
||||||
state=$(echo "$status" | jq -r .state)
|
|
||||||
if [ "$total" -eq 0 ] || [ "$state" = "pending" ]; then
|
|
||||||
echo "showcase: ${head:0:7}'s checks have not finished; next run looks again"
|
|
||||||
[ "$FORCE" = "true" ] && echo "commit=$pinned" >> "$GITHUB_OUTPUT"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
if [ "$state" != "success" ]; then
|
|
||||||
echo "::error::showcase: ${head:0:7} failed its catalog checks ($state); the pin stays at ${pinned:0:7}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "showcase: ${pinned:0:7} -> ${head:0:7}"
|
|
||||||
echo "commit=$head" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
needs: check
|
|
||||||
if: needs.check.outputs.commit != ''
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
env:
|
|
||||||
COMMIT: ${{ needs.check.outputs.commit }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
- uses: dtolnay/rust-toolchain@master
|
|
||||||
with:
|
|
||||||
toolchain: "1.95"
|
|
||||||
targets: wasm32-unknown-unknown
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
key: web
|
|
||||||
- name: wasm-bindgen at Cargo.lock's version
|
|
||||||
run: |
|
|
||||||
version=$(sed -n '/^name = "wasm-bindgen"$/{n;s/^version = "\(.*\)"$/\1/p;}' Cargo.lock)
|
|
||||||
echo "WASM_BINDGEN=wasm-bindgen@$version" >> "$GITHUB_ENV"
|
|
||||||
- uses: taiki-e/install-action@v2
|
|
||||||
with:
|
|
||||||
tool: ${{ env.WASM_BINDGEN }}
|
|
||||||
- name: wasm-opt
|
|
||||||
run: |
|
|
||||||
curl -fsSL https://github.com/WebAssembly/binaryen/releases/download/version_132/binaryen-version_132-x86_64-linux.tar.gz | tar -xz -C "$RUNNER_TEMP"
|
|
||||||
echo "$(ls -d "$RUNNER_TEMP"/binaryen-*/)bin" >> "$GITHUB_PATH"
|
|
||||||
- uses: pnpm/action-setup@v6
|
|
||||||
with:
|
|
||||||
version: 11
|
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: 24
|
|
||||||
cache: pnpm
|
|
||||||
cache-dependency-path: frontend/pnpm-lock.yaml
|
|
||||||
- run: echo "$COMMIT" > frontend/showcase.lock
|
|
||||||
- run: scripts/web.sh frontend/public/gallery
|
|
||||||
env:
|
|
||||||
ELY_GALLERY_ASSETS: https://elygpui.com/gallery/assets/
|
|
||||||
- working-directory: frontend
|
|
||||||
run: |
|
|
||||||
pnpm install --frozen-lockfile
|
|
||||||
pnpm test
|
|
||||||
pnpm build
|
|
||||||
pnpm run deploy
|
|
||||||
env:
|
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
||||||
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
|
|
||||||
- name: Commit the pin
|
|
||||||
run: |
|
|
||||||
git config user.name 'gitea-actions'
|
|
||||||
git config user.email 'actions@git.zacharyzhang.com'
|
|
||||||
git add frontend/showcase.lock
|
|
||||||
git diff --cached --quiet && exit 0
|
|
||||||
git commit -m "Site · Showcase: pin Ely-GPUI-Showcases at ${COMMIT:0:7}"
|
|
||||||
for attempt in 1 2 3; do
|
|
||||||
git pull --rebase origin main && git push origin HEAD:main && exit 0
|
|
||||||
sleep 5
|
|
||||||
done
|
|
||||||
exit 1
|
|
||||||
@@ -2,4 +2,3 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
/frontend/dist/
|
/frontend/dist/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/compat/kit/Cargo.lock
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# Builds Ely for Anthropic's OSS Scanner: the library and its tests, all crates fetched while online.
|
|
||||||
FROM debian:bookworm
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
|
||||||
RUSTUP_HOME=/usr/local/rustup \
|
|
||||||
CARGO_HOME=/usr/local/cargo \
|
|
||||||
PATH=/usr/local/cargo/bin:$PATH \
|
|
||||||
CARGO_NET_OFFLINE=false
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
ca-certificates curl build-essential clang cmake pkg-config git python3 \
|
|
||||||
libfontconfig-dev libfreetype-dev libxkbcommon-dev libxkbcommon-x11-dev \
|
|
||||||
libwayland-dev libx11-dev libx11-xcb-dev libxcb1-dev libxi-dev libxcursor-dev libxrandr-dev \
|
|
||||||
libvulkan-dev libasound2-dev libssl-dev libzstd-dev \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
RUN curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.95
|
|
||||||
|
|
||||||
# Tests check file permissions, which root ignores, so the build runs as a plain user.
|
|
||||||
RUN useradd --create-home ely
|
|
||||||
COPY --chown=ely:ely . /src
|
|
||||||
RUN chown -R ely:ely /usr/local/cargo /usr/local/rustup
|
|
||||||
USER ely
|
|
||||||
WORKDIR /src
|
|
||||||
|
|
||||||
# Fetch every crate, then build the library and its tests with debug info; tests run offline later.
|
|
||||||
RUN cargo fetch --locked
|
|
||||||
RUN cargo build --locked --lib --features test-support \
|
|
||||||
&& cargo test --locked --lib --features test-support --no-run
|
|
||||||
ENV CARGO_NET_OFFLINE=true
|
|
||||||
RUN cargo test --locked --lib --features test-support --quiet || echo "WARNING: cargo test reported failures"
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# Threat model
|
|
||||||
|
|
||||||
## What this project does and where untrusted input enters
|
|
||||||
Ely is a Rust component library for GPUI desktop and web apps. It runs inside the host app's process, so the host decides what is trusted. Treat as untrusted whatever an app's users or the network can hand a component:
|
|
||||||
- Markdown rendered by `documents` (pulldown-cmark) and text typed or pasted into fields (`forms::TextInput`, `editor::CodeEditor`).
|
|
||||||
- JSON and settings parsed with serde: saved layouts (`layout::workspace`), JSON editors and viewers (`devtools`), imported VS Code settings and themes (`settings::vscode`, `editor::settings`), GeoJSON (`maps::geojson`, `maps::choropleth`).
|
|
||||||
- Pictures decoded with `image` and QR frames read with `rqrr` (`media`, `misc::QrCodeScanner`).
|
|
||||||
- Terminal output and ANSI escape codes (`terminal`, `typography` `AnsiText` via vte).
|
|
||||||
- File names and paths from drops, archives and folder listings (`forms::files`, `files`, `lists::Folder::of`).
|
|
||||||
|
|
||||||
## Components that matter most / least
|
|
||||||
- Most: parsers and anything that turns external bytes into layout (markdown, JSON/GeoJSON, ANSI, images, QR, archive paths), and the few `unsafe` blocks that call AppKit (`shell::native`, `buttons::share`, `forms::color::dropper`).
|
|
||||||
- Less: pure drawing and theme code. `examples/`, `frontend/` (the website), `compat/` and `scripts/` are out of scope.
|
|
||||||
|
|
||||||
## How to exercise it
|
|
||||||
`cargo test --lib --features test-support` runs the suite headless on gpui's test platform. Parsers can be driven directly from unit tests.
|
|
||||||
|
|
||||||
## How you rate severity
|
|
||||||
- Memory unsafety in an `unsafe` block, or a path traversal that reaches outside a chosen folder: high.
|
|
||||||
- A panic, unbounded allocation or hang reachable from untrusted input: medium (it takes down the host app).
|
|
||||||
- Wrong rendering with no security effect: out of scope.
|
|
||||||
|
|
||||||
## Anything to leave alone
|
|
||||||
- Panics from asserts on a caller's own arguments are by design (fail fast). Report only panics reachable from runtime data.
|
|
||||||
- The macOS-only code (AppKit, wry WebView) does not build in this Linux image.
|
|
||||||
@@ -5,7 +5,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
|||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
- Rust 1.95, edition 2024. One crate: `ely-gpui-component`.
|
- Rust 1.95, edition 2024. One crate: `ely-gpui-component`.
|
||||||
- gpui, gpui_platform and gpui_web from crates.io's `gpui-pre` snapshots at `=0.3.8`, Zed at 279fe07, the pin GPUI Kit 0.7.1 holds; each keeps its crate name through `package`. gpui takes `stacker`, which keeps deep layouts off the stack's end; gpui_platform takes `font-kit`, without which macOS draws no text. Feature `runtime_shaders`, on by default, turns on gpui_platform's: machines without Xcode lack the Metal compiler.
|
- gpui and gpui_platform from Zed's repo by git at 1a28cff, the code gpui-pre 0.3.7 republishes. gpui takes `stacker`, which keeps deep layouts off the stack's end; gpui_platform takes `font-kit`, without which macOS draws no text. Feature `runtime_shaders`, on by default, turns on gpui_platform's: machines without Xcode lack the Metal compiler.
|
||||||
- Assets, embedded with `rust-embed`: Lucide 1.48.0 icons (ISC), Inter 4.1 (regular, medium, semibold, italic), JetBrains Mono 2.304 and IBM Plex Sans Regular (OFL). Plex sits unmodified, its name reserved, where gpui's svg renderer loads it: gpui_web answers `.SystemUIFont` with it, and svg text falls back to it on the web.
|
- Assets, embedded with `rust-embed`: Lucide 1.48.0 icons (ISC), Inter 4.1 (regular, medium, semibold, italic), JetBrains Mono 2.304 and IBM Plex Sans Regular (OFL). Plex sits unmodified, its name reserved, where gpui's svg renderer loads it: gpui_web answers `.SystemUIFont` with it, and svg text falls back to it on the web.
|
||||||
- Catalog data: `emojis` (Unicode emoji), `isolang` (ISO 639, native names), `isocountry` (ISO 3166), `iso_currency` (ISO 4217, also minor units for money).
|
- Catalog data: `emojis` (Unicode emoji), `isolang` (ISO 639, native names), `isocountry` (ISO 3166), `iso_currency` (ISO 4217, also minor units for money).
|
||||||
- Codes: `qrcode` (QR) and `barcoders` (Code 128, EAN-13), both MIT OR Apache-2.0, default features off. `rqrr` 0.11 ((MIT OR Apache-2.0) AND ISC, default features off) reads QR codes from the host's frames.
|
- Codes: `qrcode` (QR) and `barcoders` (Code 128, EAN-13), both MIT OR Apache-2.0, default features off. `rqrr` 0.11 ((MIT OR Apache-2.0) AND ISC, default features off) reads QR codes from the host's frames.
|
||||||
@@ -13,12 +13,11 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
|||||||
- Web views: `wry` 0.57 (Apache-2.0 OR MIT, default features off, macOS only) lays a child WKWebView over the window.
|
- Web views: `wry` 0.57 (Apache-2.0 OR MIT, default features off, macOS only) lays a child WKWebView over the window.
|
||||||
- Terminal: `alacritty_terminal` 0.26 (Apache-2.0, default features off, native only) for the grid, its parser and the pseudo-terminal; `futures` carries its events. `vte` 0.15 (Apache-2.0 OR MIT), the parser it re-exports, reads `AnsiText`'s codes alone, so colored output builds for wasm32.
|
- Terminal: `alacritty_terminal` 0.26 (Apache-2.0, default features off, native only) for the grid, its parser and the pseudo-terminal; `futures` carries its events. `vte` 0.15 (Apache-2.0 OR MIT), the parser it re-exports, reads `AnsiText`'s codes alone, so colored output builds for wasm32.
|
||||||
- Diffs: `similar` 3.2 (Apache-2.0) for line and word diffs and three-way merges; its `unicode` feature splits words at punctuation.
|
- Diffs: `similar` 3.2 (Apache-2.0) for line and word diffs and three-way merges; its `unicode` feature splits words at punctuation.
|
||||||
- Markdown: `markdown` 1.0.0 (markdown-rs, MIT) reads GitHub's flavor and math into a syntax tree with byte positions for `MarkdownRenderer`, `MarkdownEditor`, the highlights, block ranges and outline. `MarkdownRenderer` draws display math through `typography::Latex`, a ```` ```mermaid ```` block through `documents::Diagram`, and pictures from the owner's folder (`base`) or `data:` addresses (`base64` 0.22, MIT OR Apache-2.0); web pictures stay unloaded. Inline math shows as TeX: gpui text carries no inline pictures. Inline it reads as Typora's defaults: `:shortcode:` turns to its emoji as GitHub reads it (the `emojis` crate's names, GitHub's today, so `:cricket:` is the insect; read after escapes and references, so `\:smile:` turns too; inside an autolink or `<code>` it stays text; within one DOM text node, which filtered tags do not split, the first `:name:`-shaped run decides for all: blank text or a space before it turns every shortcode, else none, so `x:smile: :smile:` stays), inline HTML's `u`, `b`/`strong`, `i`/`em`, `s`/`del`/`strike`, `code`/`kbd`, `mark`, `br` and `a href` draw as what they mean GFM's filtered tags (`script`, `style`, `title`, `textarea`, `iframe`, `xmp`, `noembed`, `noframes`, `plaintext`) show as text, and other tags, hyphenated names too, vanish, leaving their text; a tag's name runs to white space or `/`, as HTML5 reads it; attribute values split on ASCII whitespace alone and decode as HTML decodes them; each Markdown container is the element GitHub writes (`em`, `strong`, `del`, `a`), on one stack with the tags and repaired as HTML5 repairs: an end tag closes the innermost element of its name and all above it, of which only formatting elements (`a`, `b`, `code`, `em`, `i`, `s`, `strike`, `strong`, `u`) reopen, a new `a` ends the open one and an `a` without `href` links nothing; other tags are not tracked, and `</br>` breaks as `<br>` does; and a footnote reference is its number in superscript, numbered by first reference, as its definition is; one `~` still strikes. At the root, blocks read as GitHub shows them: a quote whose first line is plain text reading `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]` or `[!CAUTION]` in any case, after escapes and references, spaces and tabs around it, with more after it, is an alert under its title (Important in the palette's violet, `syntax.keyword`, read at 4.5:1); a paragraph that is `[toc]` alone, as Typora reads it, lists every heading by depth, and a press on one reveals it as a link press does (`MarkdownDocument`'s `reveal`), the heading found where `CodeEditor` anchors carried it through each edit since the read, gone if an edit or an undo took it, and a secondary press acts on press, since a block writing back moves the list; front matter shows in a box without its fences; an HTML block shows its text with the inline tags above, white space collapsed and owed as a browser lays it, block elements breaking lines and closing what opened inside them, HTML5's start tags ending an open `p`, `fieldset` and `menu` too, and an `li` start an `li`, a `dd` or `dt` start either, as HTML5 ends them, comments dropped, the filtered tags shown as text, and an unclosed tag dropping the rest; a rule is a 2px `border_strong` line; bullets go •, ◦, ▪ by list depth, ordered lists counted. Exports read a tree as the drawing does through `MarkdownReader` (`block` gives its `MarkdownBlock`s, `inline` its text, `note` and `label` its footnotes, `headings` the `[toc]` list), one fresh stack per paragraph, heading or cell. `MarkdownDocument` holds a whole file in one `CodeEditor` (text and undo, colored by `CodeEditor::highlighter` with `markdown_highlights`): visual draws the file's top-level blocks in a gpui `list`, a pressed block opening as its source in a `TextInput` whose typing is the document's text at once and whose write-back is one undo step; a press in a table opens that cell alone, in place, as its source in a one-line field (a press on its text at the pressed character, past it at its end; a table block opened by its edge, the keyboard or a step from a neighbor opens its first or last cell), Tab and Shift-Tab going to the next and previous cell, Up and Down to the cell above and below or out of the table, Enter and Escape writing it back, a bare `|` typed in it escaped as the cell keeps it, a trailing backslash doubled only when a pipe follows at once, an emptied edge cell of a row without that outer pipe written as one so its column stays, the caret read through the same escaping, a padding press on the open cell keeping its typing, and a table edit writing it back before it runs; source is the editor; preview draws without presses. A written-back block, a mode switch or a new text is read at once up to 128 KiB, longer ones on the background executor; typing in source or split reads 300 ms after the last key, off the main thread (markdown-rs grows with the square of its nodes, not its bytes: in release, 128 KiB of one-letter paragraLine truncated
|
- Markdown: `pulldown-cmark` 0.13 (MIT, default features off) for CommonMark with tables, tasks, strikethrough, footnotes and math.
|
||||||
- Pictures: `image` 0.25 (MIT OR Apache-2.0), the crate gpui decodes with, default features off; it turns decoded frames and rims map tiles.
|
- Pictures: `image` 0.25 (MIT OR Apache-2.0), the crate gpui decodes with, default features off; it turns decoded frames and rims map tiles.
|
||||||
- Math and diagrams: RaTeX 0.1.14 (`ratex-parser`, `ratex-layout`, `ratex-svg` with `embed-fonts`, `ratex-types`; MIT, KaTeX's fonts OFL) typesets `Latex` to SVG with outlined glyphs; `mermaid-rs-renderer` 0.3.1 (MIT, default features off) draws `Diagram`. Both go through gpui's `SvgRenderer` at the window's scale, loaded as gpui assets off the main thread; `usvg` 0.46, gpui's own, reads a diagram's size. A RaTeX or Mermaid panic on parsed input becomes the element's error. Each element holds its picture through `primitives::held`: a new source, or the element leaving the window, removes the old asset from gpui's cache and drops its image from the atlas after the frame.
|
|
||||||
- Maps: Natural Earth's 110m countries (public domain). `assets/maps/countries.geojson`, trimmed by `scripts/countries.py` to an ISO code, a name and positions to 0.01°, backs `maps::WorldMap`; the gallery's tiles are cut from the same file, z0 to z3, by `scripts/tiles.py` with Pillow.
|
- Maps: Natural Earth's 110m countries (public domain). `assets/maps/countries.geojson`, trimmed by `scripts/countries.py` to an ISO code, a name and positions to 0.01°, backs `maps::WorldMap`; the gallery's tiles are cut from the same file, z0 to z3, by `scripts/tiles.py` with Pillow.
|
||||||
- Web: on wasm32, `web-time` 1.1 (MIT OR Apache-2.0) gives the `Instant` gpui's executor returns, std's own natively; jiff takes `js`, rust-embed `debug-embed`, similar `wasm32_web_time`. The gallery's web entry takes `gpui_web`, `wasm-bindgen`, `js-sys` and `web-sys` as wasm-only dev-dependencies, and a Hebrew cut of Noto Sans Hebrew 2.003 (OFL, `examples/gallery/fonts`): gpui_web asks the browser for emoji and CJK alone. The `wasm-bindgen` CLI at Cargo.lock's version and binaryen's `wasm-opt` finish the bundle.
|
- Web: on wasm32, `web-time` 1.1 (MIT OR Apache-2.0) gives the `Instant` gpui's executor returns, std's own natively; jiff takes `js`, rust-embed `debug-embed`, similar `wasm32_web_time`. The gallery's web entry takes `gpui_web`, `wasm-bindgen`, `js-sys` and `web-sys` as wasm-only dev-dependencies, and a Hebrew cut of Noto Sans Hebrew 2.003 (OFL, `examples/gallery/fonts`): gpui_web asks the browser for emoji and CJK alone. The `wasm-bindgen` CLI at Cargo.lock's version and binaryen's `wasm-opt` finish the bundle.
|
||||||
- Gallery: `examples/gallery`. Website: `frontend/` (Vite 8.3, pnpm 11, TypeScript, React 19.3, TanStack Router 1.170 in code-based routes, Lenis 1.3), in serro.ai's language: heavy titles over thin faint text, hairlines, one accent, full-screen sections, isometric line miniatures on a canvas. Pages name a story by its title's first part (`src/data.ts`'s `name`); scroll bars stay hidden; layouts step at 1024 and 768. Each story runs live from the gallery built for wasm32 through gpui_web, in a same-origin iframe, or, where a browser cannot run it, as a native capture. The Docs tab is Markdown in `frontend/src/docs/`, read in its `ORDER` and rendered by `marked` 18 (MIT) inside `vite.config.ts`'s `docs` plugin, so a bad guide fails `pnpm build` and the browser gets HTML alone; a fenced block tagged `example=NAME` shows `examples/docs/NAME.rs`, an example `scripts/check.sh` compiles as `docs_NAME`, so every full Rust sample on the site builds. The Showcase tab lists apps built with Ely from the public repo `ZacharyZhang-NY/Ely-GPUI-Showcases` on git.zacharyzhang.com (Gitea): manifests, PNG previews and Markdown READMEs, checked by its `scripts/catalog.mjs` (`marked`, `pngjs`, `twitter-text`), its stars refreshed by its CI. The mark is the letter E in iso blocks, its middle arm in the accent: one geometry in `src/iso/mark.ts`, drawn by the engine the miniatures use, worn by the header in the page's iso colors and by the favicon. Below 768px the header shows the mark without its word, which stays the home link's name; below 414px it drops the sound switch. Geist and Geist Mono (OFL, `@fontsource-variable`), Reicon icons (MIT, based on Solar Icons, CC BY 4.0, credited in the footer), simple-icons' Gitea mark (CC0). It deploys to Cloudflare Workers static assets through `npx wrangler` (4.143).
|
- Gallery: `examples/gallery`. Website: `frontend/` (Vite 8.3, pnpm 11, TypeScript, React 19.3, TanStack Router 1.170 in code-based routes, Lenis 1.3), in serro.ai's language: heavy titles over thin faint text, hairlines, one accent, full-screen sections, isometric line miniatures on a canvas. Pages name a story by its title's first part (`src/data.ts`'s `name`); scroll bars stay hidden; layouts step at 1024 and 768. Each story runs live from the gallery built for wasm32 through gpui_web, in a same-origin iframe, or, where a browser cannot run it, as a native capture. The Docs tab is Markdown in `frontend/src/docs/`, read in its `ORDER` and rendered by `marked` 18 (MIT) inside `vite.config.ts`'s `docs` plugin, so a bad guide fails `pnpm build` and the browser gets HTML alone; a fenced block tagged `example=NAME` shows `examples/docs/NAME.rs`, an example `scripts/check.sh` compiles as `docs_NAME`, so every full Rust sample on the site builds. The Showcase tab lists apps built with Ely from the public repo `ZacharyZhang-NY/Ely-GPUI-Showcases`: manifests, PNG previews and Markdown READMEs, checked by its `scripts/catalog.mjs` (`marked`, `pngjs`, `twitter-text`), its stars refreshed by its CI. The mark is the letter E in iso blocks, its middle arm in the accent: one geometry in `src/iso/mark.ts`, drawn by the engine the miniatures use, worn by the header in the page's iso colors and by the favicon. Below 768px the header shows the mark without its word, which stays the home link's name; below 414px it drops the sound switch. Geist and Geist Mono (OFL, `@fontsource-variable`), Reicon icons (MIT, based on Solar Icons, CC BY 4.0, credited in the footer), simple-icons' GitHub mark (CC0). It deploys to Cloudflare Workers static assets through `npx wrangler` (4.143).
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
@@ -30,8 +29,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
|||||||
- In `frontend/`: `ELY_GALLERY_ASSETS=<site>/gallery/assets/ pnpm gallery` builds the gallery into `public/gallery`, `pnpm dev` serves the site, `pnpm build` checks types and writes `dist`, `pnpm deploy` runs `wrangler deploy`, `pnpm showcase` writes the showcase catalog into `src/showcase.json` and `public/showcase` (both ignored): it downloads the commit `showcase.lock` pins, installs that commit's packages and runs its own validator, so a bad catalog fails the build; `dev` and `build` run it first, and `ELY_SHOWCASE=<checkout>` reads a local catalog instead. `pnpm test` runs the iso engine's, the mark's and the showcase list's tests, and `pnpm local` builds the gallery for localhost, builds the site and serves both on http://localhost:4173/ (wasm-bindgen and wasm-opt on PATH, as `scripts/web.sh` says). `node scripts/og.mjs`, with `pnpm preview` up, renders `public/og.png` from the hero and the header's mark. `node scripts/favicon.mjs` writes `public/favicon.svg`, which follows the system's mode, `public/apple-touch-icon.png` and the repo's `logo.png` (2048px, transparent) from the mark, in the colors `styles.css` sets. The site reads `examples/gallery/stories.json` for its chapters, stories and search.
|
- In `frontend/`: `ELY_GALLERY_ASSETS=<site>/gallery/assets/ pnpm gallery` builds the gallery into `public/gallery`, `pnpm dev` serves the site, `pnpm build` checks types and writes `dist`, `pnpm deploy` runs `wrangler deploy`, `pnpm showcase` writes the showcase catalog into `src/showcase.json` and `public/showcase` (both ignored): it downloads the commit `showcase.lock` pins, installs that commit's packages and runs its own validator, so a bad catalog fails the build; `dev` and `build` run it first, and `ELY_SHOWCASE=<checkout>` reads a local catalog instead. `pnpm test` runs the iso engine's, the mark's and the showcase list's tests, and `pnpm local` builds the gallery for localhost, builds the site and serves both on http://localhost:4173/ (wasm-bindgen and wasm-opt on PATH, as `scripts/web.sh` says). `node scripts/og.mjs`, with `pnpm preview` up, renders `public/og.png` from the hero and the header's mark. `node scripts/favicon.mjs` writes `public/favicon.svg`, which follows the system's mode, `public/apple-touch-icon.png` and the repo's `logo.png` (2048px, transparent) from the mark, in the colors `styles.css` sets. The site reads `examples/gallery/stories.json` for its chapters, stories and search.
|
||||||
- `python3 scripts/stories.py` writes `examples/gallery/stories.json`, every page's sections in drawing order with their components, for the site: the names in a title, and in its `SHOWN` table what a story draws beyond them. It fails on a section opened inside another and on a ticked entry with a home of its own that no story names. `scripts/check.sh` refuses a stale one.
|
- `python3 scripts/stories.py` writes `examples/gallery/stories.json`, every page's sections in drawing order with their components, for the site: the names in a title, and in its `SHOWN` table what a story draws beyond them. It fails on a section opened inside another and on a ticked entry with a home of its own that no story names. `scripts/check.sh` refuses a stale one.
|
||||||
- `cargo test --lib --features test-support`, `cargo clippy --all-targets --features test-support -- -D warnings`, `cargo fmt --check`. `scripts/check.sh` runs them with the house rules and lints the gallery for wasm32.
|
- `cargo test --lib --features test-support`, `cargo clippy --all-targets --features test-support -- -D warnings`, `cargo fmt --check`. `scripts/check.sh` runs them with the house rules and lints the gallery for wasm32.
|
||||||
- `cargo test --manifest-path compat/kit/Cargo.toml --target-dir target/kit` runs Ely beside GPUI Kit in one window: Tab through both, each library's dialog over the other's fields, one asset source. `scripts/check.sh` first resolves it afresh and holds `gpui-kit` to the newest release `cargo search` names, so a Kit release that moves its gpui pin fails resolution there, naming `gpui-pre`; Ely's pin then follows. Its lockfile stays out of git.
|
- `rm -rf target/debug/incremental` and `cargo clean -p ely-gpui-component` after each task item keep the disk lean. Cargo keeps every old build of the crate and its tests under a new hash; by T25 they had filled the disk.
|
||||||
- `rm -rf target/debug/incremental target/kit/debug/incremental` and `cargo clean -p ely-gpui-component` after each task item keep the disk lean. Cargo keeps every old build of the crate and its tests under a new hash; by T25 they had filled the disk.
|
|
||||||
- `scripts/icons.sh <lucide-name>...` adds icons. Then add a line to `IconName` in `src/primitives/icon.rs`.
|
- `scripts/icons.sh <lucide-name>...` adds icons. Then add a line to `IconName` in `src/primitives/icon.rs`.
|
||||||
- `python3 scripts/countries.py <ne_110m_admin_0_countries.geojson>` writes the bundled world; `python3 scripts/tiles.py <same file>` writes the gallery's map tiles.
|
- `python3 scripts/countries.py <ne_110m_admin_0_countries.geojson>` writes the bundled world; `python3 scripts/tiles.py <same file>` writes the gallery's map tiles.
|
||||||
|
|
||||||
@@ -40,24 +38,22 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
|||||||
- `src/<chapter>/`: one folder per chapter of `gpui-components.md`.
|
- `src/<chapter>/`: one folder per chapter of `gpui-components.md`.
|
||||||
- `src/theme`: palettes, tokens, `ActiveTheme`. `src/motion`: durations, easings, spring.
|
- `src/theme`: palettes, tokens, `ActiveTheme`. `src/motion`: durations, easings, spring.
|
||||||
- `examples/gallery/pages/<chapter>.rs`: one page per chapter, in PRD order.
|
- `examples/gallery/pages/<chapter>.rs`: one page per chapter, in PRD order.
|
||||||
- `compat/kit`: a crate of its own, with its own workspace, that tests Ely beside GPUI Kit.
|
|
||||||
- `TASKS.md` and `tasks/`: progress. Every PRD entry has a line and a tag; `tasks/reviews.md` keeps every review round.
|
- `TASKS.md` and `tasks/`: progress. Every PRD entry has a line and a tag; `tasks/reviews.md` keeps every review round.
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
- Colors, control and icon sizes, text sizes, radii, shadows come from `cx.theme()`. No raw `px()` literals inside components.
|
- Colors, control and icon sizes, text sizes, radii, shadows come from `cx.theme()`. No raw `px()` literals inside components.
|
||||||
- Gaps and padding use gpui's rem scale helpers (`gap_2`, `px_3`). That scale is the spacing system.
|
- Gaps and padding use gpui's rem scale helpers (`gap_2`, `px_3`). That scale is the spacing system.
|
||||||
- Borders use gpui's fixed-pixel helpers (`border_1` is 1px). Hairlines do not scale. gpui paints a border no wider than half its box, so a line as thin as its box is a fill, `w_px` or `h_px` with `bg`, as the resize handles draw theirs.
|
- Borders use gpui's fixed-pixel helpers (`border_1` is 1px). Hairlines do not scale.
|
||||||
- One component, one home. Duplicates point to it.
|
- One component, one home. Duplicates point to it.
|
||||||
- Fail fast. No silent fallbacks. Log state changes with `log`.
|
- Fail fast. No silent fallbacks. Log state changes with `log`.
|
||||||
- A handler a component needs is an argument of `new`, so leaving it out fails to compile. A state live data or the outside world can reach, a key gone from a list that changed or a failed load, logs an error naming the owner and draws a visible state, nothing chosen or an error line, and the app runs on. An assert on a caller's argument or on a fact true by construction stays; one that runtime data or input can trip is fixed. Chapters move to this as `tasks/panics.md` ticks them.
|
|
||||||
- Files stay at or under 500 lines. Comments are one short line, and rare.
|
- Files stay at or under 500 lines. Comments are one short line, and rare.
|
||||||
- A repeating animation (spinner, breath, sweep) holds still under reduced motion instead of shortening, or it would spin at 1 ms a turn. An effect set off by a change (flash, shake, a bar that fades at rest) reads elapsed time through `motion::since_change` rather than restarting an animation id, so what it holds keeps its state: an animation's id passes to every descendant, and a new one rebuilds their focus, drags and hover.
|
- A repeating animation (spinner, breath, sweep) holds still under reduced motion instead of shortening, or it would spin at 1 ms a turn. An effect set off by a change (flash, shake, a bar that fades at rest) reads elapsed time through `motion::since_change` rather than restarting an animation id, so what it holds keeps its state: an animation's id passes to every descendant, and a new one rebuilds their focus, drags and hover.
|
||||||
- `motion::duration()` honors reduced motion. Springs overshoot. gpui hands an easing's value to the whole animator unclamped, so `motion::spring` goes inside the animator, on only what may overshoot (`forms::Switch` springs its thumb's travel, not its colors).
|
- `motion::duration()` honors reduced motion. Springs overshoot. gpui hands an easing's value to the whole animator unclamped, so `motion::spring` goes inside the animator, on only what may overshoot (`forms::Switch` springs its thumb's travel, not its colors).
|
||||||
- `svg()` paints only with its own `text_color`. `Icon` always sets one.
|
- `svg()` paints only with its own `text_color`. `Icon` always sets one.
|
||||||
- Buttons stay out of focus on click (`prevent_default` on mouse down). Tab still reaches them.
|
- Buttons stay out of focus on click (`prevent_default` on mouse down). Tab still reaches them.
|
||||||
- The focus ring is a 1px focus-colored border (`FocusRing`). gpui paints shadows under the element, so a shadow ring floods transparent elements. Focusable elements keep a 1px border, transparent at rest.
|
- The focus ring is a 1px focus-colored border (`FocusRing`). gpui paints shadows under the element, so a shadow ring floods transparent elements. Focusable elements keep a 1px border, transparent at rest.
|
||||||
- `ely_gpui_component::init` binds Tab and Shift-Tab to `FocusNext` and `FocusPrev` under `ElyFocus`, the key context every `FocusScope` sets. gpui ranks a binding with no context at the deepest level, so a host's Tab in a deeper context, GPUI Kit's code editor among them, lost or won by which `init` ran last; under a context, the deeper one wins. Wrap the root in `FocusScope` with `.root()` and focus its handle. A root scope takes focus when the focused element leaves the tree, as a toast's or an alert's button does.
|
- `ely_gpui_component::init` binds Tab and Shift-Tab to `FocusNext` and `FocusPrev`. Wrap the root in `FocusScope` with `.root()` and focus its handle. A root scope takes focus when the focused element leaves the tree, as a toast's or an alert's button does.
|
||||||
- The gallery scripts mouse input by posting `NSEvent`s to its own queue, so a press takes AppKit's path and reaches a native view over the window (`misc::WebView`). `Window::dispatch_event` reaches only gpui's listeners. gpui's normal window takes mouse moves from a tracking area, which only a real cursor feeds, so the harness sets the window's `acceptsMouseMovedEvents` for posted moves.
|
- The gallery scripts mouse input by posting `NSEvent`s to its own queue, so a press takes AppKit's path and reaches a native view over the window (`misc::WebView`). `Window::dispatch_event` reaches only gpui's listeners. gpui's normal window takes mouse moves from a tracking area, which only a real cursor feeds, so the harness sets the window's `acceptsMouseMovedEvents` for posted moves.
|
||||||
- gpui starts a drag on the first move past its threshold, and only the moves after it reach `on_drag_move`. gpui 0.2.2's mac window replayed a held drag every 16 ms; gpui_macos replays only while `pressedMouseButtons` shows a real button, which posted events never set. So `Step::DragTo` posts each move twice, as the replay did: the second post of the first step reaches `on_drag_move`, where a component that anchors on its first drag move sets its anchor (Scrollbar, Knob, Split).
|
- gpui starts a drag on the first move past its threshold, and only the moves after it reach `on_drag_move`. gpui 0.2.2's mac window replayed a held drag every 16 ms; gpui_macos replays only while `pressedMouseButtons` shows a real button, which posted events never set. So `Step::DragTo` posts each move twice, as the replay did: the second post of the first step reaches `on_drag_move`, where a component that anchors on its first drag move sets its anchor (Scrollbar, Knob, Split).
|
||||||
- Every shot of a gpui window, a page's or a script's, waits three of its frames through `on_next_frame`: gpui presents without waiting on the GPU, and a heavy page's draw outlasts a fixed pause, so a shot could show the frame before the last step. A page's pauses stay, for work off the main thread such as pictures decoding. A covered window draws no frames, so its capture fails after 5 s instead of shooting a stale frame.
|
- Every shot of a gpui window, a page's or a script's, waits three of its frames through `on_next_frame`: gpui presents without waiting on the GPU, and a heavy page's draw outlasts a fixed pause, so a shot could show the frame before the last step. A page's pauses stay, for work off the main thread such as pictures decoding. A covered window draws no frames, so its capture fails after 5 s instead of shooting a stale frame.
|
||||||
@@ -66,12 +62,11 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
|||||||
- Whatever draws over the page goes through `primitives::raise`: at the top it defers; inside something raised it lays out on its own and draws last there, so a list opened in a dialog lies over the dialog. gpui takes a deferred draw inside another but paints them all by priority alone, so a tooltip (0) deferred inside a fullscreen dialog (1) would draw under it. A raise takes its owner's id and draws under it, so its keyed state and focus hold while other raises come and go. `scripts/check.sh` refuses gpui's `deferred` anywhere else.
|
- Whatever draws over the page goes through `primitives::raise`: at the top it defers; inside something raised it lays out on its own and draws last there, so a list opened in a dialog lies over the dialog. gpui takes a deferred draw inside another but paints them all by priority alone, so a tooltip (0) deferred inside a fullscreen dialog (1) would draw under it. A raise takes its owner's id and draws under it, so its keyed state and focus hold while other raises come and go. `scripts/check.sh` refuses gpui's `deferred` anywhere else.
|
||||||
- gpui hides hover after a key press until the mouse moves. A hover that holds or shows something (the toast stack, a carousel, tooltips, hover cards, charts, scroll activity) sets `HoverListenerMode::InputModalityIndependent`, so typing under a resting pointer keeps it. A hover preview the keyboard also moves keeps the default on its items; the box that clears it on leave listens independently, so a real exit still reports, and ignores a leave that follows a key through `last_input_was_keyboard` (`forms::Calendar`).
|
- gpui hides hover after a key press until the mouse moves. A hover that holds or shows something (the toast stack, a carousel, tooltips, hover cards, charts, scroll activity) sets `HoverListenerMode::InputModalityIndependent`, so typing under a resting pointer keeps it. A hover preview the keyboard also moves keeps the default on its items; the box that clears it on leave listens independently, so a real exit still reports, and ignores a leave that follows a key through `last_input_was_keyboard` (`forms::Calendar`).
|
||||||
- `occlude()` blocks the pointer for everything painted before it, its own ancestors too. Put it on the outermost box that should stop the pointer, as the toast stack does, or the ancestors' hover never fires.
|
- `occlude()` blocks the pointer for everything painted before it, its own ancestors too. Put it on the outermost box that should stop the pointer, as the toast stack does, or the ancestors' hover never fires.
|
||||||
- A control inside a `drag_region` occludes it. On Windows gpui answers the system's hit test with the caption wherever a `Drag` hitbox lies under the pointer unoccluded, and Windows then keeps the press: no click reaches the control. `block_mouse_except_scroll` is not enough, since that answer reads every hitbox under the pointer. Window buttons occlude, which lets them answer as close, maximize and minimize, and so do a title bar's slots; a column header's item row occludes, staying the scroll container, and its title is a drag region of its own; the mini window drags by its title alone, so its buttons sit outside the region and its hover still shows the bar.
|
|
||||||
- A drag that pans, turns or slides goes through gpui's drag, whose moves reach the owner in or out of its box; `on_mouse_move` stops at the box's edge. gpui sends a drag's moves to every listener of its type, so each payload carries its owner's `EntityId` and handlers check it.
|
- A drag that pans, turns or slides goes through gpui's drag, whose moves reach the owner in or out of its box; `on_mouse_move` stops at the box's edge. gpui sends a drag's moves to every listener of its type, so each payload carries its owner's `EntityId` and handlers check it.
|
||||||
- gpui paints pictures without a transform, so a picture turns by its pixels: `media::turn` turns decoded frames by quarter turns off the main thread, an asset keyed by the picture and the turn. gpui's loader turns a photo upright by its EXIF orientation first, so `media::ImageViewer` turns from the upright frame and `media::ImageUpload` measures that frame. An svg turns to any angle through its `Transformation`, so a control that turns shows one (`interaction::Rotatable`).
|
- gpui paints pictures without a transform, so a picture turns by its pixels: `media::turn` turns decoded frames by quarter turns off the main thread, an asset keyed by the picture and the turn. gpui's loader turns a photo upright by its EXIF orientation first, so `media::ImageViewer` turns from the upright frame and `media::ImageUpload` measures that frame. An svg turns to any angle through its `Transformation`, so a control that turns shows one (`interaction::Rotatable`).
|
||||||
- A box that keeps a cursor over its items rings the item the cursor is on, not the box, as SelectableList marks its row (`interaction::SelectionArea`).
|
- A box that keeps a cursor over its items rings the item the cursor is on, not the box, as SelectableList marks its row (`interaction::SelectionArea`).
|
||||||
- Boxes that scroll together go through `interaction::ScrollSync`: the pane a wheel turned leads, and a canvas laid before the panes copies its place in prepaint, before they draw, so no frame shows them apart.
|
- Boxes that scroll together go through `interaction::ScrollSync`: the pane a wheel turned leads, and a canvas laid before the panes copies its place in prepaint, before they draw, so no frame shows them apart.
|
||||||
- A component that takes a starting value from its owner keeps it in `layout::seeded`: a new value from the owner replaces local drags. A field that reports its words as they change keeps what it sent, so the owner's echo leaves the caret alone (`mail::SignatureEditor`). A cursor inside a seeded view stays while it lies in what is shown, so the owner's echo of a turn leaves it (`calendar::CalendarMonthView`, `YearView`). A number field keys the owner's value as shown: a new one waits while the field is focused and lands before the blur commits, and an echo of its own commit is no new value (`forms::NumberInput`). A ranged number field holds room for its widest bound through `Input::least`, since a text field has no width of its own.
|
- A component that takes a starting value from its owner keeps it in `layout::seeded`: a new value from the owner replaces local drags. A field that reports its words as they change keeps what it sent, so the owner's echo leaves the caret alone (`mail::SignatureEditor`). A cursor inside a seeded view stays while it lies in what is shown, so the owner's echo of a turn leaves it (`calendar::CalendarMonthView`, `YearView`).
|
||||||
- The capture harness turns on reduced motion, so shots are still: repeating motion rests on one frame, and a shimmer shows no band. gpui's mac window stops its display link while occluded, so a capture advances a moving picture only on the draws the harness forces.
|
- The capture harness turns on reduced motion, so shots are still: repeating motion rests on one frame, and a shimmer shows no band. gpui's mac window stops its display link while occluded, so a capture advances a moving picture only on the draws the harness forces.
|
||||||
- Each capture pass starts its page fresh (`Gallery::fresh`): page state lives under the pass, so the dark pass runs its script from the same start as the light one.
|
- Each capture pass starts its page fresh (`Gallery::fresh`): page state lives under the pass, so the dark pass runs its script from the same start as the light one.
|
||||||
- The capture harness finds gpui windows by handle through `raw-window-handle`. AppKit popups, which gpui does not own, come from the window list by process and level.
|
- The capture harness finds gpui windows by handle through `raw-window-handle`. AppKit popups, which gpui does not own, come from the window list by process and level.
|
||||||
@@ -84,12 +79,12 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
|||||||
- `TextInput::bounds_for` reads the last layout. Text changed this frame clamps to the laid-out end until the next paint.
|
- `TextInput::bounds_for` reads the last layout. Text changed this frame clamps to the laid-out end until the next paint.
|
||||||
- The harness types with `dispatch_keystroke` through `AnyWindowHandle`, which leaves the root view free to redraw.
|
- The harness types with `dispatch_keystroke` through `AnyWindowHandle`, which leaves the root view free to redraw.
|
||||||
- The macOS open panel runs out of process too. Escape does not reach it; the harness sends it `cancel:`.
|
- The macOS open panel runs out of process too. Escape does not reach it; the harness sends it `cancel:`.
|
||||||
- gpui's `test-support` and `leak-detection` stay behind this crate's `test-support` feature and are never on for the gallery: one records every `debug_selector`'s bounds each frame, the other tracks every entity handle, so a test that leaks one fails (`tests::a_leaked_entity_fails_its_test`).
|
- gpui's `test-support` stays behind this crate's `test-support` feature and is never on for the gallery: it tracks every entity handle for leaks and records every `debug_selector`'s bounds each frame.
|
||||||
- A tracked `FocusHandle` is a Tab stop only when built with `.tab_stop(true)`. `tab_index` on the div reaches only handles gpui makes itself. `primitives::tab_stop` keeps one per id.
|
- A tracked `FocusHandle` is a Tab stop only when built with `.tab_stop(true)`. `tab_index` on the div reaches only handles gpui makes itself. `primitives::tab_stop` keeps one per id.
|
||||||
- Floating lists go below their anchor, or above when only above has room (`forms::options::float`). gpui's own switch keeps the anchor point, so a flipped list would cover its trigger. An option list is at least its trigger's width and at most the window's less `window_margin` each side, so gpui can snap it inside and its rows end in an ellipsis; wider, gpui pins it left and the rest runs off.
|
- Floating lists go below their anchor, or above when only above has room (`forms::options::float`). gpui's own switch keeps the anchor point, so a flipped list would cover its trigger. An option list is at least its trigger's width and at most the window's less `window_margin` each side, so gpui can snap it inside and its rows end in an ellipsis; wider, gpui pins it left and the rest runs off.
|
||||||
- A trigger that opens a list goes through `forms::select::listing`: toggle, keys, blur and popup live there, for Select, TabOverflowMenu and Breadcrumb alike. A field that suggests as you type floats its rows through `forms::select::Picker` and `forms::options::Popup`, as Combobox and TagInput do.
|
- A trigger that opens a list goes through `forms::select::listing`: toggle, keys, blur and popup live there, for Select, TabOverflowMenu and Breadcrumb alike. A field that suggests as you type floats its rows through `forms::select::Picker` and `forms::options::Popup`, as Combobox and TagInput do.
|
||||||
- A palette goes through `navigation::palette::Palette`: query field, grouped rows, cursor, keys and focus live there. CommandPalette, QuickOpen, QuickSwitcher, SearchPalette and QuickLauncher only build rows.
|
- A palette goes through `navigation::palette::Palette`: query field, grouped rows, cursor, keys and focus live there. CommandPalette, QuickOpen, QuickSwitcher, SearchPalette and QuickLauncher only build rows.
|
||||||
- A menu goes through `menus::menu`: rows, submenus, keys, focus and presses outside live there, for DropdownMenu, OverflowMenu, SplitButton, ContextMenu, SearchableMenu and MenuBar. A press on the host's own box is the host's to handle, so a trigger toggles and a right click moves a context menu without a close and reopen in one event. A row of icon buttons (`Menu::buttons`) is one entry: Up and Down reach it, Left and Right cross it, a reader presses a button through Click and hears the marked one, and a mark holds its item by label, so a row that changes under it runs nothing unasked. Panels occlude what lies under them. A submenu that does not fit on the right opens on the left of its row, sliding in from that side. A row's submenu opens once the pointer rests on it 150 ms; while a submenu is open, a row crossed inside the triangle from the last pointer place to the submenu's near corners waits 300 ms and switches only if the pointer still rests there (`menus::aim`); a row outside it marks at once. Keys and presses open at once.
|
- A menu goes through `menus::menu`: rows, submenus, keys, focus and presses outside live there, for DropdownMenu, OverflowMenu, SplitButton, ContextMenu, SearchableMenu and MenuBar. A press on the host's own box is the host's to handle, so a trigger toggles and a right click moves a context menu without a close and reopen in one event.
|
||||||
- gpui focuses the nearest focusable ancestor on mouse down, and apps wrap everything in a `FocusScope`. A press that must leave focus alone calls `prevent_default`, as buttons and context regions do.
|
- gpui focuses the nearest focusable ancestor on mouse down, and apps wrap everything in a `FocusScope`. A press that must leave focus alone calls `prevent_default`, as buttons and context regions do.
|
||||||
- gpui runs the click of every pressed element under the pointer, a row's with its button's. A pressable row that holds buttons stops their press at their box on mouse down (`files::transfer`), as `overlays::media_button` stops its own.
|
- gpui runs the click of every pressed element under the pointer, a row's with its button's. A pressable row that holds buttons stops their press at their box on mouse down (`files::transfer`), as `overlays::media_button` stops its own.
|
||||||
- gpui draws a window's first frame inside `open_window`. State the first frame reads, such as `cx.set_menus`, is set before it opens.
|
- gpui draws a window's first frame inside `open_window`. State the first frame reads, such as `cx.set_menus`, is set before it opens.
|
||||||
@@ -153,13 +148,12 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
|||||||
- Pixels a host draws at a box's size go through `rendering::RenderSurface`: it asks for one BGRA frame at the box's device pixels in prepaint, draws again only for a new size or revision, and frees the old frame and, on leave, the last one. The revision covers all the frame reads, theme colors too.
|
- Pixels a host draws at a box's size go through `rendering::RenderSurface`: it asks for one BGRA frame at the box's device pixels in prepaint, draws again only for a new size or revision, and frees the old frame and, on leave, the last one. The revision covers all the frame reads, theme colors too.
|
||||||
- Tests type a shortcut as it is bound: a `secondary-` binding is Command on macOS and Control elsewhere, so its test types `secondary-`; `cmd-` is the platform key on every OS and reaches only handlers that read that modifier (`lists::SelectableList`). A Tab count runs through `forms::EyeDropper` only on macOS, where it is enabled.
|
- Tests type a shortcut as it is bound: a `secondary-` binding is Command on macOS and Control elsewhere, so its test types `secondary-`; `cmd-` is the platform key on every OS and reaches only handlers that read that modifier (`lists::SelectableList`). A Tab count runs through `forms::EyeDropper` only on macOS, where it is enabled.
|
||||||
- `img()` keeps loading state, and moves an animated picture's frames, only with an id; the frames hold still while its window is inactive or gpui's own `reduce_motion` is on, which the theme's reduced motion does not set. Content masks are rectangles, but `img()` draws only the part of its picture inside its box and rounds that part with its own radius, so a `Cover` crop keeps its corners.
|
- `img()` keeps loading state, and moves an animated picture's frames, only with an id; the frames hold still while its window is inactive or gpui's own `reduce_motion` is on, which the theme's reduced motion does not set. Content masks are rectangles, but `img()` draws only the part of its picture inside its box and rounds that part with its own radius, so a `Cover` crop keeps its corners.
|
||||||
- An app has one asset source: `Assets`, or `Assets::before(other)` when it has files of its own or runs another library such as GPUI Kit; Ely's paths load from Ely, the rest from `other`. `init` loads every file Ely bundles through the app's source and fails naming the first it cannot, since gpui's svg renderer reads IBM Plex there too.
|
|
||||||
- gpui reads a `&str` picture source with a scheme as a web address, and any other as an embedded asset. A file goes through `Path` (`documents::source` in the crate, `ui::picture` in the gallery).
|
- gpui reads a `&str` picture source with a scheme as a web address, and any other as an embedded asset. A file goes through `Path` (`documents::source` in the crate, `ui::picture` in the gallery).
|
||||||
- Taffy sizes a flex column by the height a ratio box has at its set width, before its cap, so chat's `shaped` pictures sit in a block inside such a column (`chat::ImageMessage`, `VideoMessage`, `ImageGrid`).
|
- Taffy sizes a flex column by the height a ratio box has at its set width, before its cap, so chat's `shaped` pictures sit in a block inside such a column (`chat::ImageMessage`, `VideoMessage`, `ImageGrid`).
|
||||||
- Taffy 0.13 collapses a child's vertical margin through a block parent with no padding or border on that side, and its size cache keeps only that block's size, so a box that measures it again leaves the margin out. A component moves a box with a relative offset, or with padding where what follows moves too, not with such a margin: `layout::Drawer` pulls and snaps its panel, and a fullscreen `overlays::Dialog` slides its card, with `top`; `motion::Stagger`, `Transition`, a centered Dialog and the palette's card rise with padding, so the box a scrim centers grows with them; the gallery's inspector sample is its probe's own child. A margin that stops at its parent is safe: a flex or grid item's own, or a child's of a flex item or of a box with padding or a border on that side. A flex wrapper stops the collapse but gives its items a definite height, so percent heights inside start to resolve (the Drawer's `max_h(relative(0.85))`).
|
- Taffy 0.13 collapses a child's vertical margin through a block parent with no padding or border on that side, and its size cache keeps only that block's size, so a box that measures it again leaves the margin out. A component moves a box with a relative offset, or with padding where what follows moves too, not with such a margin: `layout::Drawer` pulls and snaps its panel, and a fullscreen `overlays::Dialog` slides its card, with `top`; `motion::Stagger`, `Transition`, a centered Dialog and the palette's card rise with padding, so the box a scrim centers grows with them; the gallery's inspector sample is its probe's own child. A margin that stops at its parent is safe: a flex or grid item's own, or a child's of a flex item or of a box with padding or a border on that side. A flex wrapper stops the collapse but gives its items a definite height, so percent heights inside start to resolve (the Drawer's `max_h(relative(0.85))`).
|
||||||
- Tiles in a picture's shape sit in flex rows of `flex_1` cells (`generative::GenerationGrid`). A cell's padding counts in its flex base, so a row of fewer cells gives each more width; cells that match across rows keep their inset on a box inside (`messaging::VideoCallGrid`).
|
- Tiles in a picture's shape sit in flex rows of `flex_1` cells (`generative::GenerationGrid`). A cell's padding counts in its flex base, so a row of fewer cells gives each more width; cells that match across rows keep their inset on a box inside (`messaging::VideoCallGrid`).
|
||||||
- Rise reads green and fall red unless a component's `red_up` swaps them, as markets in East Asia read. `finance::quotes::moves` picks the pair.
|
- Rise reads green and fall red unless a component's `red_up` swaps them, as markets in East Asia read. `finance::quotes::moves` picks the pair.
|
||||||
- Charts lay out in `f32` pixels of their own box, tested without gpui, and paint through `charts::paint::at`: a canvas's paint gets window coordinates, and `with_element_offset` works only in prepaint. Their sizes come from `theme.chart()`. A chart value is finite and within ±`charts::LIMIT` (1e30), so sums of values and their pixels stay inside `f32`; the builders refuse others, and a scale works in halves so a domain across the limit maps.
|
- Charts lay out in `f32` pixels of their own box, tested without gpui, and paint through `charts::paint::at`: a canvas's paint gets window coordinates, and `with_element_offset` works only in prepaint. Their sizes come from `theme.chart()`.
|
||||||
- A label pinned to a point sits in a zero-size absolute box that centers it with flex (`charts::axes`), so no offset guesses at text size.
|
- A label pinned to a point sits in a zero-size absolute box that centers it with flex (`charts::axes`), so no offset guesses at text size.
|
||||||
- Tokens live in `src/theme`; the raw `px()` scan skips that folder only.
|
- Tokens live in `src/theme`; the raw `px()` scan skips that folder only.
|
||||||
- An owner's palette, edited or imported, reaches the theme per mode through `Theme::set_palette`, and `Theme::palette` names the one the shown mode fades to; high contrast strengthens Ely's own palettes alone. A color goes by its field's name through `token` and `token_mut`, on `Palette` and `Syntax` alike. Ely's palettes pass `tooling::A11yChecker`'s audit in both modes, with high contrast and without; a test holds them there.
|
- An owner's palette, edited or imported, reaches the theme per mode through `Theme::set_palette`, and `Theme::palette` names the one the shown mode fades to; high contrast strengthens Ely's own palettes alone. A color goes by its field's name through `token` and `token_mut`, on `Palette` and `Syntax` alike. Ely's palettes pass `tooling::A11yChecker`'s audit in both modes, with high contrast and without; a test holds them there.
|
||||||
@@ -179,13 +173,12 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
|||||||
- A component that fills its container takes no width on its root: a block or a flex column stretches it, in a flex row the host gives it `flex_1`, and as a window's root it fills the window, since gpui sets a window root's auto sides to the window's size. A percent width ignores the root's own margins, so they push it past its container, and in a flex row it takes the whole row as its basis and leaves a `flex_1().min_w_0()` neighbor nothing. A root centered under a cap keeps `w_full`, since a flex column stretches no child with an auto side margin, and `mx_auto` needs a width to fill (`layout::Container`). So does a box that keeps a ratio: a block follows its ratio from a stretch, but a flex or grid box only from a set width, and callers may make one flex (`layout::AspectRatio`, and `primitives::framed` under VideoPlayer, CameraPreview, ModelViewer, ParticipantTile and InpaintCanvas). `Button::full_width` is its host asking, since a button takes no style. Each chapter's test holds its filling roots at the full width of a flex column in a padded block through `layout::tests::narrow_width`, which reads a root by its `debug_selector`.
|
- A component that fills its container takes no width on its root: a block or a flex column stretches it, in a flex row the host gives it `flex_1`, and as a window's root it fills the window, since gpui sets a window root's auto sides to the window's size. A percent width ignores the root's own margins, so they push it past its container, and in a flex row it takes the whole row as its basis and leaves a `flex_1().min_w_0()` neighbor nothing. A root centered under a cap keeps `w_full`, since a flex column stretches no child with an auto side margin, and `mx_auto` needs a width to fill (`layout::Container`). So does a box that keeps a ratio: a block follows its ratio from a stretch, but a flex or grid box only from a set width, and callers may make one flex (`layout::AspectRatio`, and `primitives::framed` under VideoPlayer, CameraPreview, ModelViewer, ParticipantTile and InpaintCanvas). `Button::full_width` is its host asking, since a button takes no style. Each chapter's test holds its filling roots at the full width of a flex column in a padded block through `layout::tests::narrow_width`, which reads a root by its `debug_selector`.
|
||||||
- A field's `Highlight` sets color, wash, weight, slant and strike per span; measuring and painting share one set of runs, so wrapped heights match.
|
- A field's `Highlight` sets color, wash, weight, slant and strike per span; measuring and painting share one set of runs, so wrapped heights match.
|
||||||
- A block editor keeps one undo for the document: text and structure together, as snapshots through `forms::History`; it takes Undo and Redo before its fields do.
|
- A block editor keeps one undo for the document: text and structure together, as snapshots through `forms::History`; it takes Undo and Redo before its fields do.
|
||||||
- A selection across a BlockEditor's fields lives in the editor (`blocks::span`): a press in a field drags into others, Shift-press reaches from the caret's field. The pressed field selects its own part; the editor sets the other end's and washes blocks between. The editor watches the focused end through `TextInput::edits`, which counts every edit, text changed or not: an edit there, once an input method commits, is rebuilt from the last snapshot as head, typed and tail in one undo step; a caret move or lost focus lets the span go, since gpui runs bindings before key listeners. Backspace, Delete, Cut and Enter take the span before the field acts; copy writes it as Markdown.
|
|
||||||
- gpui calls focus listeners only while the window is active. A test that needs focus events activates its window with `window.activate_window()`.
|
- gpui calls focus listeners only while the window is active. A test that needs focus events activates its window with `window.activate_window()`.
|
||||||
- A terminal takes Tab and Shift-Tab through bindings under `ElyTerminal`, ahead of focus moves; other keys go through `terminal::keys` in a key listener, and typed text through its input handler.
|
- A terminal takes Tab and Shift-Tab through bindings under `ElyTerminal`, ahead of focus moves; other keys go through `terminal::keys` in a key listener, and typed text through its input handler.
|
||||||
- A shell in the gallery starts without startup files and with a fixed prompt, so captures show no user or host.
|
- A shell in the gallery starts without startup files and with a fixed prompt, so captures show no user or host.
|
||||||
- gpui has no `(ElementId, usize)` id. A child id names its index: `(self.id.clone(), format!("row-{ix}"))`.
|
- gpui has no `(ElementId, usize)` id. A child id names its index: `(self.id.clone(), format!("row-{ix}"))`.
|
||||||
- Clippy's `single_range_in_vec_init` rejects `[a..b]` and `vec![a..b]`; bind the range first.
|
- Clippy's `single_range_in_vec_init` rejects `[a..b]` and `vec![a..b]`; bind the range first.
|
||||||
- A component built on `lists::Tree` fills its box; the host gives it a height. So does `documents::BlockEditor`, which scrolls in a `ScrollArea` through its own handle (`ScrollArea::track_scroll`) and brings the caret into view each time it moves, not each frame, so a wheel moves past a still caret. A fixed-width column beside a `flex_1` one takes `flex_none`, or it shrinks.
|
- A component built on `lists::Tree` fills its box; the host gives it a height. A fixed-width column beside a `flex_1` one takes `flex_none`, or it shrinks.
|
||||||
- Editor rows read their text through `CodeEditor::row_text`: a multi-line ghost cuts its line, and the rest follows the ghost's last line.
|
- Editor rows read their text through `CodeEditor::row_text`: a multi-line ghost cuts its line, and the rest follows the ghost's last line.
|
||||||
- gpui clears `debug_bounds` each frame: a test sees an element leave as well as appear.
|
- gpui clears `debug_bounds` each frame: a test sees an element leave as well as appear.
|
||||||
- Time reads `web_time::Instant`: std's compiles for wasm32 and panics in the browser. `scripts/check.sh` refuses std's in `src` and the gallery.
|
- Time reads `web_time::Instant`: std's compiles for wasm32 and panics in the browser. `scripts/check.sh` refuses std's in `src` and the gallery.
|
||||||
@@ -196,7 +189,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
|||||||
- The gallery's pictures go through `asset!` and `ui::picture` or `ui::resource`: a file natively, on the web an address under `ELY_GALLERY_ASSETS`, since gpui reads no files there.
|
- The gallery's pictures go through `asset!` and `ui::picture` or `ui::resource`: a file natively, on the web an address under `ELY_GALLERY_ASSETS`, since gpui reads no files there.
|
||||||
- On the web a paste reaches the focused field as the browser's paste event, through `EntityInputHandler::paste`, so Paste stays unbound there (`TextInput`, `CodeEditor`). Code that reads the clipboard itself awaits `read_from_clipboard_async`: gpui_web's `read_from_clipboard` answers at once and finds nothing (`tables::DataGrid`'s Cmd-V).
|
- On the web a paste reaches the focused field as the browser's paste event, through `EntityInputHandler::paste`, so Paste stays unbound there (`TextInput`, `CodeEditor`). Code that reads the clipboard itself awaits `read_from_clipboard_async`: gpui_web's `read_from_clipboard` answers at once and finds nothing (`tables::DataGrid`'s Cmd-V).
|
||||||
- The first frame wears the host's mode through `Theme::set_mode_now`, a mode sent before the window opens included; `set_mode` fades from the colors on screen.
|
- The first frame wears the host's mode through `Theme::set_mode_now`, a mode sent before the window opens included; `set_mode` fades from the colors on screen.
|
||||||
- The system time zone comes from `typography::format::system_zone`: an unknown zone logs one error naming the first component that asked and reads as jiff's `TimeZone::unknown()`, which is UTC. A time shown in the system zone goes through `typography::format::datetime`, which adds " UTC" in the unknown zone, so the screen never claims local time. jiff's `TimeZone::system()` falls back to UTC in silence.
|
- The system time zone comes from `typography::format::system_zone`, which fails loud; jiff's `TimeZone::system()` falls back to UTC in silence.
|
||||||
- A Select with nothing to pick stays shut: disabled, it shows its placeholder; open, it fails (`media::DeviceSelector` says no device was found).
|
- A Select with nothing to pick stays shut: disabled, it shows its placeholder; open, it fails (`media::DeviceSelector` says no device was found).
|
||||||
- A list opens with its cursor on the current choice: Select on the chosen row, Combobox on its value, MultiSelect on the first one ticked, SelectableList on its first selected row. A selection the owner makes, not the echo of the list's own pick, moves SelectableList's cursor there.
|
- A list opens with its cursor on the current choice: Select on the chosen row, Combobox on its value, MultiSelect on the first one ticked, SelectableList on its first selected row. A selection the owner makes, not the echo of the list's own pick, moves SelectableList's cursor there.
|
||||||
- An event repeats as RFC 5545 has it: weeks count from the start's own week, and a month or a year without the start's date is passed over (`calendar::Recurrence`).
|
- An event repeats as RFC 5545 has it: weeks count from the start's own week, and a month or a year without the start's date is passed over (`calendar::Recurrence`).
|
||||||
@@ -205,12 +198,12 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
|||||||
- A header that holds a name beside counts and actions wraps: `flex_wrap`, the name `flex_1` with `min_w(label_width)`, the rest one `flex_none` group that drops below.
|
- A header that holds a name beside counts and actions wraps: `flex_wrap`, the name `flex_1` with `min_w(label_width)`, the rest one `flex_none` group that drops below.
|
||||||
- Every header, row and strip works at 280px: a name keeps its minimum width, the rest wraps below or scrolls, and nothing draws past its box.
|
- Every header, row and strip works at 280px: a name keeps its minimum width, the rest wraps below or scrolls, and nothing draws past its box.
|
||||||
- gpui takes a scroll box's reach from its direct children's bounds, and a block child stretches to the box. Content that scrolls sideways sets its least width on that child (`tables::body::least`).
|
- gpui takes a scroll box's reach from its direct children's bounds, and a block child stretches to the box. Content that scrolls sideways sets its least width on that child (`tables::body::least`).
|
||||||
- gpui turns a wheel along the other axis onto a box that scrolls one way, and the page around it scrolls too. A box inside a scrolling page keeps its wheel to its axis with `layout::on_axis`, and so does the page. gpui also hands a wheel to every scroll box under the pointer, so a `ScrollArea` stops a wheel along an axis it has room on and passes one it cannot use, and a floating option list occludes (`forms::options::Popup`).
|
- gpui turns a wheel along the other axis onto a box that scrolls one way, and the page around it scrolls too. A box inside a scrolling page keeps its wheel to its axis with `layout::on_axis`, and so does the page.
|
||||||
- The focus color belongs to the focus ring. A chosen or shown item takes accent, as `forms::picks` and `lists::ListItem` do.
|
- The focus color belongs to the focus ring. A chosen or shown item takes accent, as `forms::picks` and `lists::ListItem` do.
|
||||||
- A box that shows a host's picture takes the picture's shape from the host (`ratio`), so nothing is cropped and a point given as a share lands on the picture. A focusable one keeps its focus border on an outer box, so the picture keeps that shape exactly (`media::ImageCropper`).
|
- A box that shows a host's picture takes the picture's shape from the host (`ratio`), so nothing is cropped and a point given as a share lands on the picture. A focusable one keeps its focus border on an outer box, so the picture keeps that shape exactly (`media::ImageCropper`).
|
||||||
- A default only the component can work out, such as a crop fitted to a picture it decodes, reaches the owner once through `window.defer` after render (`media::ImageUpload`).
|
- A default only the component can work out, such as a crop fitted to a picture it decodes, reaches the owner once through `window.defer` after render (`media::ImageUpload`).
|
||||||
- A ScrubInput shows its number in about 140px beside a short label. In a narrower column a pair hides it, so a long label takes a row of its own (`canvas::TypographyPanel`).
|
- A ScrubInput shows its number in about 140px beside a short label. In a narrower column a pair hides it, so a long label takes a row of its own (`canvas::TypographyPanel`).
|
||||||
- Design panels edit a whole value and hand the owner a changed copy through `canvas::panel::editing`; the owner's handler is an argument of `new`. A swatch that opens a picker is `canvas::panel::color_well`.
|
- Design panels edit a whole value and hand the owner a changed copy through `canvas::panel::editing`, which takes the owner's handler itself: a component without one fails at render. A swatch that opens a picker is `canvas::panel::color_well`.
|
||||||
- A press that stops propagation also stops the ancestors' focus on press. A layer that takes keys focuses itself in its own press handler (`canvas::ToolLayer`, `canvas::MindMap`), and a field over it stops its own presses.
|
- A press that stops propagation also stops the ancestors' focus on press. A layer that takes keys focuses itself in its own press handler (`canvas::ToolLayer`, `canvas::MindMap`), and a field over it stops its own presses.
|
||||||
- A button inside a pressable row goes through `lists::row_action`, which keeps its press and its Space or Enter from the row (`devtools::ConnectionManager`).
|
- A button inside a pressable row goes through `lists::row_action`, which keeps its press and its Space or Enter from the row (`devtools::ConnectionManager`).
|
||||||
- A field opened over a component hands focus back to that component on Enter and Escape, through `forms::Editing`'s `returning` and `give_back` (`canvas::writing`); a blur keeps the words and leaves focus where it went.
|
- A field opened over a component hands focus back to that component on Enter and Escape, through `forms::Editing`'s `returning` and `give_back` (`canvas::writing`); a blur keeps the words and leaves focus where it went.
|
||||||
@@ -221,13 +214,13 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
|||||||
- `typography::Ellipsis` shapes its line with the text style's own run, so a strike or an underline set on its box shows (`lists::ListItem::struck`, a closed task's title).
|
- `typography::Ellipsis` shapes its line with the text style's own run, so a strike or an underline set on its box shows (`lists::ListItem::struck`, a closed task's title).
|
||||||
- Focus that must follow an item to another parent is taken by the owner and keyed by the item: a handle gpui makes for an element dies with its path (`project::KanbanBoard`, whose cards keep focus across columns).
|
- Focus that must follow an item to another parent is taken by the owner and keyed by the item: a handle gpui makes for an element dies with its path (`project::KanbanBoard`, whose cards keep focus across columns).
|
||||||
- The theme's `platform`, the system's to start, decides the window buttons Ely draws, the title bar's layout and key caps, `secondary` included (`typography::keys::keystroke`). A title bar whose buttons the OS draws follows the OS.
|
- The theme's `platform`, the system's to start, decides the window buttons Ely draws, the title bar's layout and key caps, `secondary` included (`typography::keys::keystroke`). A title bar whose buttons the OS draws follows the OS.
|
||||||
- A window whose sidebar runs to its top is `shell::ColumnShell`: each column's header drags the window; the first leaves the traffic lights room, and drawn controls lead the first header in the macOS manner or end the last. A set width is a flex basis, not a width, so it holds its header's own parts, which a width let run past it, and yields when the shell runs short; the host's header items keep one control's width and scroll when short. A resizable column drags by its border toward the filling column, its right before it and its left after it; the handle draws that border and occludes the header under it, and a drag sets off from the wider of the width drawn and the owner's, since a header's parts may hold a column wider and a short shell squeeze it narrower; a drag that has not moved sideways changes nothing, and one that comes back where it set off gives the width the owner had.
|
- A window whose sidebar runs to its top is `shell::ColumnShell`: each column's header drags the window; the first leaves the traffic lights room, and drawn controls lead the first header in the macOS manner or end the last. A set width yields before a header's own parts; the host's header items keep one control's width and scroll when short.
|
||||||
- Taffy counts a flex item's text in its least width whatever width the item sets. A box that must add none lays its content absolutely inside a filling box (`shell::ColumnShell`'s title and body).
|
- Taffy counts a flex item's text in its least width whatever width the item sets. A box that must add none lays its content absolutely inside a filling box (`shell::ColumnShell`'s title and body).
|
||||||
- A centered title sits between a start and an end that grow alike from a zero basis, their padding on a box inside them, since taffy counts padding in the flex base; their content is their least width, so the title gives way before either end (`shell::TitleBar`).
|
- A centered title sits between a start and an end that grow alike from a zero basis, their padding on a box inside them, since taffy counts padding in the flex base; their content is their least width, so the title gives way before either end (`shell::TitleBar`).
|
||||||
- A pane the window's blur shows through is `shell::Vibrancy`, filled with the palette's `glass`, in a window opened `WindowBackgroundAppearance::Blurred`: vibrancy on macOS, acrylic on Windows. gpui's `MicaBackdrop` and `MicaAltBackdrop` are DWM backdrops from Windows 11 build 22621; older builds get none, in silence, and macOS plain transparency.
|
- A pane the window's blur shows through is `shell::Vibrancy`, filled with the palette's `glass`, in a window opened `WindowBackgroundAppearance::Blurred`: vibrancy on macOS, acrylic on Windows. gpui's `MicaBackdrop` and `MicaAltBackdrop` are DWM backdrops from Windows 11 build 22621; older builds get none, in silence, and macOS plain transparency.
|
||||||
- The chart's hues go by `theme::HUE_NAMES`, for an event's color and a label's alike.
|
- The chart's hues go by `theme::HUE_NAMES`, for an event's color and a label's alike.
|
||||||
- Text whose every character counts, encoded data and keys, goes through `typography::literal`: JetBrains Mono draws `==` and `->` as ligatures (`devtools::Encoder`).
|
- Text whose every character counts, encoded data and keys, goes through `typography::literal`: JetBrains Mono draws `==` and `->` as ligatures (`devtools::Encoder`).
|
||||||
- Messages go through `i18n::I18n`, a global the app sets: `cx.global::<I18n>().text(key, args)` fills each `{name}`. Every catalog holds the first one's keys; a missing key, argument or catalog fails. Copy a component draws, tooltips and default placeholders too, reads `i18n::text` (app catalog, then the builtin English and Chinese); `i18n/tests/shipped.rs` fails on a fixed English literal in a component Ely ships (labels, tooltips, button and segment text, match arms, capitalized literal arguments); a choice's value stays its key, its label the catalog's (`tables::Test::key`). A right-to-left locale mirrors a stack with `Direction::row` and sets its text with `Direction::align`; CoreText orders mixed Hebrew and Latin runs on macOS, but gpui sets no paragraph direction, so a line takes its first strong character's: `text` leads a right-to-left message with U+200F, so its words suit labels, not a field's value, and `Locale::number` a right-to-left minus with U+200E, as CLDR does. gpui wraps shaped glyphs in the order they are drawn, on the web as on macOS, so a right-to-left line that wraps comes out garbled: right-to-left text stays on one line, an `Ellipsis`, which cuts by characters and sits by its text's alignment.
|
- Messages go through `i18n::I18n`, a global the app sets: `cx.global::<I18n>().text(key, args)` fills each `{name}`. Every catalog holds the first one's keys; a missing key, argument or catalog fails. A right-to-left locale mirrors a stack with `Direction::row` and sets its text with `Direction::align`; CoreText orders mixed Hebrew and Latin runs on macOS, but gpui sets no paragraph direction, so a line takes its first strong character's: `text` leads a right-to-left message with U+200F, so its words suit labels, not a field's value, and `Locale::number` a right-to-left minus with U+200E, as CLDR does. gpui wraps shaped glyphs in the order they are drawn, on the web as on macOS, so a right-to-left line that wraps comes out garbled: right-to-left text stays on one line, an `Ellipsis`, which cuts by characters and sits by its text's alignment.
|
||||||
- gpui orders Tab stops by `tab_index` before paint order, and registers none for an `invisible()` element. A stop that must come first where it sits, yet draw over what paints after it, stays in flow at zero size and raises what it shows (`i18n::SkipLink`): a lower `tab_index` would sort it before the root scope's own handle, so Tab from the root would pass it.
|
- gpui orders Tab stops by `tab_index` before paint order, and registers none for an `invisible()` element. A stop that must come first where it sits, yet draw over what paints after it, stays in flow at zero size and raises what it shows (`i18n::SkipLink`): a lower `tab_index` would sort it before the root scope's own handle, so Tab from the root would pass it.
|
||||||
- Color-blind safe charts swap Ely's chart hues for a set fitted to stay apart under Machado's protanopia, deuteranopia and tritanopia; a test holds every pair 0.10 apart in Oklab and each hue 3:1 on its page (`Theme::set_color_blind_safe`, in Ely's own palettes alone).
|
- Color-blind safe charts swap Ely's chart hues for a set fitted to stay apart under Machado's protanopia, deuteranopia and tritanopia; a test holds every pair 0.10 apart in Oklab and each hue 3:1 on its page (`Theme::set_color_blind_safe`, in Ely's own palettes alone).
|
||||||
- gpui packs pictures in its atlas without padding and samples them with a linear filter, on the web as on macOS, so a scaled picture's edge takes color from its atlas neighbor, and pictures laid edge to edge show seams. A map tile carries a one-pixel rim copied from its own edge and draws one of its pixels larger each side, clipped to its box (`maps::tile`).
|
- gpui packs pictures in its atlas without padding and samples them with a linear filter, on the web as on macOS, so a scaled picture's edge takes color from its atlas neighbor, and pictures laid edge to edge show seams. A map tile carries a one-pixel rim copied from its own edge and draws one of its pixels larger each side, clipped to its box (`maps::tile`).
|
||||||
@@ -249,7 +242,6 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
|||||||
- 2026-09-24: gpui 0.2.2 has no accessibility tree and no tray, badge, or notification API. Those entries carry `blocked` or `prove`.
|
- 2026-09-24: gpui 0.2.2 has no accessibility tree and no tray, badge, or notification API. Those entries carry `blocked` or `prove`.
|
||||||
- 2026-09-24: the gallery photographs its own window with `CGWindowListCreateImage`. No Screen Recording permission needed.
|
- 2026-09-24: the gallery photographs its own window with `CGWindowListCreateImage`. No Screen Recording permission needed.
|
||||||
- 2026-09-24: license MIT OR Apache-2.0.
|
- 2026-09-24: license MIT OR Apache-2.0.
|
||||||
- 2026-10-05: license MIT alone, in `LICENSE`, at the owner's word; replaces the 2026-09-24 choice.
|
|
||||||
- 2026-09-25: layouts persist as versioned JSON through serde. Unknown fields are ignored. Newer versions, unknown panels and bad pane trees are refused, and restore is all or nothing.
|
- 2026-09-25: layouts persist as versioned JSON through serde. Unknown fields are ignored. Newer versions, unknown panels and bad pane trees are refused, and restore is all or nothing.
|
||||||
- 2026-09-25: an entry built from a later chapter's parts lands with that chapter; its line points there. MenuBar goes to Menus, QuickLauncher to Navigation.
|
- 2026-09-25: an entry built from a later chapter's parts lands with that chapter; its line points there. MenuBar goes to Menus, QuickLauncher to Navigation.
|
||||||
- 2026-09-25: SystemNotification is blocked. An unbundled app has no notification center (probed on macOS 27.2), and gpui 0.2.2 has no API.
|
- 2026-09-25: SystemNotification is blocked. An unbundled app has no notification center (probed on macOS 27.2), and gpui 0.2.2 has no API.
|
||||||
@@ -266,13 +258,4 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
|||||||
- 2026-09-29: the ported debug gallery draws its heaviest page about 1.6 times slower than on gpui 0.2.2 (tooling: 9 fps, worst frame 117 ms, against 14 fps and 72 ms), with gpui's accessibility tree on or off (`Application::new_inaccessible` measured the same). Ely's code did not change there; the frame cost is gpui's.
|
- 2026-09-29: the ported debug gallery draws its heaviest page about 1.6 times slower than on gpui 0.2.2 (tooling: 9 fps, worst frame 117 ms, against 14 fps and 72 ms), with gpui's accessibility tree on or off (`Application::new_inaccessible` measured the same). Ely's code did not change there; the frame cost is gpui's.
|
||||||
- 2026-09-29: the web gallery runs on one thread, since a static site sends no cross-origin isolation, and draws through WebGL2 where WebGPU is missing. Its pictures are served beside the wasm, not embedded: the wasm stays under Cloudflare's 25 MiB, and a story fetches only its own. `wasm-opt -Oz` takes the wasm from 23.0 to 20.0 MB; gzip and brotli stay near 7.9 and 5.5 MB.
|
- 2026-09-29: the web gallery runs on one thread, since a static site sends no cross-origin isolation, and draws through WebGL2 where WebGPU is missing. Its pictures are served beside the wasm, not embedded: the wasm stays under Cloudflare's 25 MiB, and a story fetches only its own. `wasm-opt -Oz` takes the wasm from 23.0 to 20.0 MB; gzip and brotli stay near 7.9 and 5.5 MB.
|
||||||
- 2026-10-04: the Showcase follows gpui-kit.com/apps: apps live in their own public repo and arrive by pull request. The site pins a commit of it in `frontend/showcase.lock` and moves only when someone bumps the pin, so stars refresh on the site with each bump. READMEs take Markdown and PNG pictures alone, no HTML or video, so the site needs no sanitizer. The catalog started empty.
|
- 2026-10-04: the Showcase follows gpui-kit.com/apps: apps live in their own public repo and arrive by pull request. The site pins a commit of it in `frontend/showcase.lock` and moves only when someone bumps the pin, so stars refresh on the site with each bump. READMEs take Markdown and PNG pictures alone, no HTML or video, so the site needs no sanitizer. The catalog started empty.
|
||||||
- 2026-10-05: the pin moves by itself: `.github/workflows/showcase.yml` looks at the catalog's main every 30 minutes and, once that commit's checks pass, builds the gallery and the site, deploys through `CLOUDFLARE_API_TOKEN` (secret) and `CLOUDFLARE_ACCOUNT_ID` (variable), then commits the pin; a run with `deploy` set redeploys at the current pin. A catalog commit that fails its checks keeps the pin. The catalog runs its checks on every pull request without approval, save from accounts new to GitHub; they get no secrets and a read-only token.
|
|
||||||
- 2026-10-09: the repos moved from GitHub to Gitea at git.zacharyzhang.com; GitHub's copies are gone. The site links there, `pnpm showcase` downloads the catalog's archive from there, and `showcase.yml` runs on Gitea Actions, reading the catalog's head and combined commit status through Gitea's API. The catalog's stars still come from GitHub's API, signed in only with its `STARS_GITHUB_TOKEN` secret, since Gitea's own token means nothing there. TokenLedger, merged on GitHub after the copy was taken, was restored from the site's copy.
|
|
||||||
- 2026-10-04: chapter 44, Rendering, holds `RenderSurface` alone. gpui at 1a28cff paints `RenderImage`s everywhere, `CVPixelBuffer`s (NV12) on macOS's Metal renderer alone, and shares no device or shader hook, so shader, camera and GPU views are the host's, feeding the surface, and zero-copy or Metal, OpenGL, Vulkan and Direct3D surfaces are blocked (`tasks/ch44.md`). CI tests the crate and builds the gallery on Linux, Windows and macOS, arm64 and x86_64.
|
- 2026-10-04: chapter 44, Rendering, holds `RenderSurface` alone. gpui at 1a28cff paints `RenderImage`s everywhere, `CVPixelBuffer`s (NV12) on macOS's Metal renderer alone, and shares no device or shader hook, so shader, camera and GPU views are the host's, feeding the surface, and zero-copy or Metal, OpenGL, Vulkan and Direct3D surfaces are blocked (`tasks/ch44.md`). CI tests the crate and builds the gallery on Linux, Windows and macOS, arm64 and x86_64.
|
||||||
- 2026-10-05: an overlay its owner removes without calling `Close` still hands focus back: `take_focus` records each holder and what it took focus from, `give_back` forgets it, and when the focused element leaves the tree the root scope walks back past every gone holder to the focus before them all, else takes it; nested overlays removed together return to what the outer one took focus from. Before, a dialog whose action set its owner's state to closed left focus on the root, and keys reached nothing until a click.
|
|
||||||
- 2026-10-05: `Select::label` and `TextInput::label` name the control for assistive technology without drawing anything; a text input with neither label nor placeholder had no name, and a select read only its value, so a field named by a description list beside it was anonymous in the AX tree.
|
|
||||||
- 2026-10-05: `NumberInput::label` names the field for assistive technology without drawing it, through `TextInput::set_label` on the field's inner input, which lives across frames; a number field named only by a description list beside it was anonymous in the AX tree.
|
|
||||||
- 2026-10-05: `TemplatePicker`'s list is one Tab stop: up, down, Home and End move the highlight, Enter uses it; rows are list items with name, selection, place and count; its search placeholder, All chip, Use button and empty line read from the builtin catalog in English and Chinese. Before, the rows were reachable by mouse alone and anonymous in the AX tree.
|
|
||||||
- 2026-10-05: `SelectableList` is a list in the AX tree, named by `label`; each `ListItem` is a list item with its title as name, its description, its selection, and, in a selectable list, its place and count (`ListItem::place`). Before, the rows had no role and were missing from the AX tree.
|
|
||||||
- 2026-10-05: `NumberInput::blank` shows a placeholder, not a number, as for a mixed selection; Enter or blur on the empty field commits nothing, a typed number commits as usual. Before, a mixed field held a real number the owner had to invent, and Enter sent it. `ScrubInput` moved to `number/scrub.rs`.
|
|
||||||
- 2026-10-07: gpui comes from crates.io's `gpui-pre` at the exact snapshot GPUI Kit pins, not from Zed's git. Two sources build two gpui crates whose types do not mix, so Ely and Kit could not share a window. The code is still Zed's; the publisher is not. This replaces the 2026-09-28 choice of Zed's git, and the pin moves when Kit's does.
|
|
||||||
+1
-15
@@ -46,18 +46,4 @@ CI runs the same on every pull request. Fill in the pull request template: what
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Ely is MIT. Unless you say otherwise, what you contribute is licensed the same way.
|
Ely is MIT or Apache-2.0, at your option. Unless you say otherwise, what you contribute is licensed the same way.
|
||||||
|
|
||||||
## Opening a pull request on git.zacharyzhang.com
|
|
||||||
|
|
||||||
Forking is disabled on this server. Open pull requests with AGit instead:
|
|
||||||
|
|
||||||
1. Register an account at https://git.zacharyzhang.com
|
|
||||||
2. Clone, commit your changes, then push to a `refs/for/<target-branch>` ref:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git
|
|
||||||
git push origin HEAD:refs/for/main -o topic=<short-name> -o title="<PR title>" -o description="<PR body>"
|
|
||||||
```
|
|
||||||
|
|
||||||
3. To update the pull request, push again with the same `topic`.
|
|
||||||
Generated
+227
-591
File diff suppressed because it is too large.
Load diff
+6
-15
@@ -3,38 +3,31 @@ name = "ely-gpui-component"
|
|||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.95"
|
rust-version = "1.95"
|
||||||
license = "MIT"
|
license = "MIT OR Apache-2.0"
|
||||||
description = "A restrained component library for GPUI, in light and dark."
|
description = "A restrained component library for GPUI, in light and dark."
|
||||||
repository = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components"
|
repository = "https://github.com/ZacharyZhang-NY/Ely-GPUI-Components"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["runtime_shaders"]
|
default = ["runtime_shaders"]
|
||||||
runtime_shaders = ["gpui_platform/runtime_shaders"]
|
runtime_shaders = ["gpui_platform/runtime_shaders"]
|
||||||
test-support = ["gpui/test-support", "gpui/leak-detection"]
|
test-support = ["gpui/test-support"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
base64 = "0.22"
|
|
||||||
barcoders = { version = "2", default-features = false, features = ["std"] }
|
barcoders = { version = "2", default-features = false, features = ["std"] }
|
||||||
emojis = "0.9"
|
emojis = "0.9"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
gpui = { package = "gpui-pre", version = "=0.3.8", default-features = false, features = ["stacker"] }
|
gpui = { git = "https://github.com/zed-industries/zed", rev = "1a28cff4b409169bac058bca40dfbfeb7621d19b", default-features = false, features = ["stacker"] }
|
||||||
gpui_platform = { package = "gpui-pre-platform", version = "=0.3.8", features = ["font-kit"] }
|
gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "1a28cff4b409169bac058bca40dfbfeb7621d19b", features = ["font-kit"] }
|
||||||
image = { version = "0.25", default-features = false }
|
image = { version = "0.25", default-features = false }
|
||||||
iso_currency = { version = "0.7", features = ["iterator"] }
|
iso_currency = { version = "0.7", features = ["iterator"] }
|
||||||
isocountry = "0.3"
|
isocountry = "0.3"
|
||||||
isolang = { version = "2.4", features = ["local_names", "list_languages"] }
|
isolang = { version = "2.4", features = ["local_names", "list_languages"] }
|
||||||
jiff = "0.2"
|
jiff = "0.2"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
markdown = "=1.0.0"
|
|
||||||
mermaid-rs-renderer = { version = "=0.3.1", default-features = false }
|
|
||||||
pulldown-cmark = { version = "0.13", default-features = false }
|
pulldown-cmark = { version = "0.13", default-features = false }
|
||||||
qrcode = { version = "0.14", default-features = false }
|
qrcode = { version = "0.14", default-features = false }
|
||||||
ratex-layout = "=0.1.14"
|
|
||||||
ratex-parser = "=0.1.14"
|
|
||||||
ratex-svg = { version = "=0.1.14", features = ["embed-fonts"] }
|
|
||||||
ratex-types = "=0.1.14"
|
|
||||||
regex = "1"
|
regex = "1"
|
||||||
rqrr = { version = "0.11", default-features = false }
|
rqrr = { version = "0.11", default-features = false }
|
||||||
rust-embed = "8.12"
|
rust-embed = "8.12"
|
||||||
@@ -43,8 +36,6 @@ serde_json = { version = "1", features = ["preserve_order"] }
|
|||||||
similar = { version = "3.2", default-features = false, features = ["std", "text", "inline", "unicode"] }
|
similar = { version = "3.2", default-features = false, features = ["std", "text", "inline", "unicode"] }
|
||||||
smallvec = "1"
|
smallvec = "1"
|
||||||
unicode-segmentation = "1"
|
unicode-segmentation = "1"
|
||||||
unicode-width = "0.2"
|
|
||||||
usvg = "0.46"
|
|
||||||
vte = { version = "0.15", default-features = false, features = ["std", "ansi"] }
|
vte = { version = "0.15", default-features = false, features = ["std", "ansi"] }
|
||||||
web-time = "1.1"
|
web-time = "1.1"
|
||||||
|
|
||||||
@@ -70,7 +61,7 @@ rustix = { version = "1", default-features = false, features = ["std", "fs"] }
|
|||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
|
|
||||||
[target.'cfg(target_family = "wasm")'.dev-dependencies]
|
[target.'cfg(target_family = "wasm")'.dev-dependencies]
|
||||||
gpui_web = { package = "gpui-pre-web", version = "=0.3.8" }
|
gpui_web = { git = "https://github.com/zed-industries/zed", rev = "1a28cff4b409169bac058bca40dfbfeb7621d19b" }
|
||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
web-sys = { version = "0.3", features = ["Location", "UrlSearchParams", "Window"] }
|
web-sys = { version = "0.3", features = ["Location", "UrlSearchParams", "Window"] }
|
||||||
|
|||||||
+176
@@ -0,0 +1,176 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
File renamed without changes.
@@ -11,9 +11,9 @@ Status: early. `TASKS.md` tracks every component, chapter by chapter. To help, s
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components" }
|
ely-gpui-component = { git = "https://github.com/ZacharyZhang-NY/Ely-GPUI-Components" }
|
||||||
gpui = { package = "gpui-pre", version = "=0.3.8" }
|
gpui = { git = "https://github.com/zed-industries/zed", rev = "1a28cff4b409169bac058bca40dfbfeb7621d19b" }
|
||||||
gpui_platform = { package = "gpui-pre-platform", version = "=0.3.8", features = ["font-kit"] }
|
gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "1a28cff4b409169bac058bca40dfbfeb7621d19b", features = ["font-kit"] }
|
||||||
```
|
```
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
@@ -22,17 +22,13 @@ use gpui::App;
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
gpui_platform::application().with_assets(Assets).run(|cx: &mut App| {
|
gpui_platform::application().with_assets(Assets).run(|cx: &mut App| {
|
||||||
ely_gpui_component::init(cx).expect("Ely failed to start");
|
ely_gpui_component::init(cx);
|
||||||
Theme::set_mode(Mode::Dark, cx);
|
Theme::set_mode(Mode::Dark, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`init` registers the fonts and the theme. It returns an error if the application's asset source cannot load one of Ely's files.
|
`init` registers the fonts and the theme. It panics if `Assets` is not wired into the application.
|
||||||
|
|
||||||
## With GPUI Kit
|
|
||||||
|
|
||||||
Ely runs beside Longbridge's [GPUI Kit](https://gpui-kit.com) in one window, on the GPUI snapshot Kit 0.7.1 pins. Pass `Assets::before(gpui_kit::assets::Assets)` as the one asset source, call both `init`s, open the window with `gpui_kit::open_window`, and wrap the view in Ely's root `FocusScope`. The [guide](https://elygpui.com/docs/gpui-kit) covers themes, focus and the inspector; `compat/kit` holds a whole app and the tests behind each claim.
|
|
||||||
|
|
||||||
## Gallery
|
## Gallery
|
||||||
|
|
||||||
@@ -65,5 +61,5 @@ pnpm deploy # Cloudflare, through wrangler
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT. Lucide icons: ISC. Inter, JetBrains Mono, IBM Plex Sans and the gallery's Noto Sans Hebrew: SIL Open Font License 1.1.
|
MIT or Apache-2.0, at your option. Lucide icons: ISC. Inter, JetBrains Mono, IBM Plex Sans and the gallery's Noto Sans Hebrew: SIL Open Font License 1.1.
|
||||||
The gallery photos in `examples/gallery/assets` were generated for this project with gpt-image-2.
|
The gallery photos in `examples/gallery/assets` were generated for this project with gpt-image-2.
|
||||||
@@ -187,8 +187,6 @@ Per-component lines live in `tasks/`. Tags there:
|
|||||||
- [x] T50g Rendering · Acceptance: the checklist, item by item
|
- [x] T50g Rendering · Acceptance: the checklist, item by item
|
||||||
- [x] T51 Showcase · an apps page fed by a public repo — `tasks/showcase.md`
|
- [x] T51 Showcase · an apps page fed by a public repo — `tasks/showcase.md`
|
||||||
- [x] T52 Logo · an isometric mark for the header, favicon and OG image — `tasks/logo.md`
|
- [x] T52 Logo · an isometric mark for the header, favicon and OG image — `tasks/logo.md`
|
||||||
- [x] T53 Panics · handlers move into `new`, live states log and show, asserts audited — `tasks/panics.md`
|
|
||||||
- [x] T54 GPUI Kit · Ely and Kit share one app and one window — `tasks/kit.md`
|
|
||||||
|
|
||||||
## T00 detail
|
## T00 detail
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
<!-- @license lucide-static v1.48.0 - ISC -->
|
|
||||||
<svg
|
|
||||||
class="lucide lucide-message-square-warning"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
>
|
|
||||||
<path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z" />
|
|
||||||
<path d="M12 15h.01" />
|
|
||||||
<path d="M12 7v4" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 481 B |
@@ -1,16 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "ely-kit-compat"
|
|
||||||
version = "0.0.0"
|
|
||||||
edition = "2024"
|
|
||||||
rust-version = "1.95"
|
|
||||||
publish = false
|
|
||||||
description = "Ely and the newest GPUI Kit in one app and one window."
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
path = "src/lib.rs"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
ely-gpui-component = { path = "../..", features = ["test-support"] }
|
|
||||||
gpui-kit = { version = ">=0.7.1", features = ["test-support"] }
|
|
||||||
|
|
||||||
[workspace]
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
use ely_gpui_component::{
|
|
||||||
Assets,
|
|
||||||
buttons::Button,
|
|
||||||
primitives::FocusScope,
|
|
||||||
theme::{ActiveTheme, Mode, Theme},
|
|
||||||
typography::{Heading, Paragraph},
|
|
||||||
};
|
|
||||||
use gpui_kit::{
|
|
||||||
App, AppContext, Context, Entity, FocusHandle, IntoElement, ParentElement, Render, Styled,
|
|
||||||
Window, WindowOptions,
|
|
||||||
component::{
|
|
||||||
WindowExt,
|
|
||||||
button::Button as KitButton,
|
|
||||||
input::{Input, InputState},
|
|
||||||
theme::{Theme as KitTheme, ThemeMode},
|
|
||||||
},
|
|
||||||
div, px,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Start {
|
|
||||||
focus: FocusHandle,
|
|
||||||
name: Entity<InputState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for Start {
|
|
||||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
let theme = cx.theme();
|
|
||||||
let dark = theme.is_dark();
|
|
||||||
FocusScope::new(&self.focus)
|
|
||||||
.root()
|
|
||||||
.size_full()
|
|
||||||
.p_8()
|
|
||||||
.bg(theme.colors.bg)
|
|
||||||
.child(column(dark, &self.name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn column(dark: bool, name: &Entity<InputState>) -> impl IntoElement {
|
|
||||||
div()
|
|
||||||
.w(px(480.))
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_4()
|
|
||||||
.child(Heading::h1("Ely and GPUI Kit"))
|
|
||||||
.child(Paragraph::new(
|
|
||||||
"One window, one focus order, one asset source.",
|
|
||||||
))
|
|
||||||
.child(Input::new(name))
|
|
||||||
.child(
|
|
||||||
KitButton::new("kit-dialog")
|
|
||||||
.label("Kit dialog")
|
|
||||||
.on_click(|_, window, cx| {
|
|
||||||
window.open_dialog(cx, |dialog, _, _| dialog.title("From GPUI Kit"));
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("mode", if dark { "Light mode" } else { "Dark mode" }).on_click(
|
|
||||||
move |_, window, cx| {
|
|
||||||
let (ely, kit) = if dark {
|
|
||||||
(Mode::Light, ThemeMode::Light)
|
|
||||||
} else {
|
|
||||||
(Mode::Dark, ThemeMode::Dark)
|
|
||||||
};
|
|
||||||
Theme::set_mode(ely, cx);
|
|
||||||
KitTheme::change(kit, Some(window), cx);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
gpui_kit::application()
|
|
||||||
.with_assets(Assets::before(gpui_kit::assets::Assets))
|
|
||||||
.run(|cx: &mut App| {
|
|
||||||
gpui_kit::init(cx);
|
|
||||||
ely_gpui_component::init(cx).expect("Ely failed to start");
|
|
||||||
gpui_kit::open_window(WindowOptions::default(), cx, |window, cx| {
|
|
||||||
cx.new(|cx| {
|
|
||||||
let focus = cx.focus_handle();
|
|
||||||
window.focus(&focus, cx);
|
|
||||||
Start {
|
|
||||||
focus,
|
|
||||||
name: cx.new(|cx| InputState::new(window, cx).placeholder("Name")),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.expect("window failed to open");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
use ely_gpui_component::Assets;
|
|
||||||
use gpui_kit::AssetSource;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn one_source_serves_both_libraries() {
|
|
||||||
let kit = gpui_kit::assets::Assets;
|
|
||||||
let both = Assets::before(gpui_kit::assets::Assets);
|
|
||||||
let ely = Assets.list("").unwrap();
|
|
||||||
assert!(ely.len() > 300, "Ely bundles {} files", ely.len());
|
|
||||||
for path in &ely {
|
|
||||||
let served = both.load(path).unwrap().expect("Ely's file loads");
|
|
||||||
assert_eq!(
|
|
||||||
served,
|
|
||||||
Assets.load(path).unwrap().unwrap(),
|
|
||||||
"{path} is Ely's"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let icons = kit.list("icons/").unwrap();
|
|
||||||
assert!(!icons.is_empty(), "Kit lists its icons");
|
|
||||||
for path in &icons {
|
|
||||||
let served = both.load(path).unwrap().expect("Kit's icon loads");
|
|
||||||
if Assets.load(path).unwrap().is_none() {
|
|
||||||
assert_eq!(served, kit.load(path).unwrap().unwrap(), "{path} is Kit's");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
use ely_gpui_component::{buttons::Button, overlays::Dialog, primitives::FocusScope};
|
|
||||||
use gpui_kit::{
|
|
||||||
AppContext, Bounds, Context, Entity, FocusHandle, IntoElement, KeyUpEvent, Keystroke,
|
|
||||||
ParentElement, Render, Styled, TestAppContext, VisualTestContext, Window, WindowBounds,
|
|
||||||
WindowOptions,
|
|
||||||
component::{
|
|
||||||
WindowExt,
|
|
||||||
input::{Editor, EditorState, Input, InputState},
|
|
||||||
},
|
|
||||||
div, point,
|
|
||||||
prelude::*,
|
|
||||||
px, size,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const CODE: &str = "fn main() {}";
|
|
||||||
|
|
||||||
/// Ely's root scope inside Kit's `Root`.
|
|
||||||
pub struct Desk {
|
|
||||||
pub root: FocusHandle,
|
|
||||||
pub open: FocusHandle,
|
|
||||||
pub line: Entity<InputState>,
|
|
||||||
pub code: Entity<EditorState>,
|
|
||||||
pub ely_dialog: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for Desk {
|
|
||||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
let view = cx.entity();
|
|
||||||
FocusScope::new(&self.root)
|
|
||||||
.root()
|
|
||||||
.size_full()
|
|
||||||
.p_4()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_4()
|
|
||||||
.child(
|
|
||||||
div().debug_selector(|| "ely-open".into()).child(
|
|
||||||
Button::new("ely-open", "Open")
|
|
||||||
.focus_handle(&self.open)
|
|
||||||
.on_click(|_, window, cx| {
|
|
||||||
window.open_dialog(cx, |dialog, _, _| dialog.title("Kit dialog"));
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(Input::new(&self.line))
|
|
||||||
.child(div().h(px(120.)).child(Editor::new(&self.code).size_full()))
|
|
||||||
.when(self.ely_dialog, |desk| {
|
|
||||||
desk.child(
|
|
||||||
Dialog::new("ely-dialog", "Ely dialog", move |_, cx| {
|
|
||||||
view.update(cx, |desk, cx| {
|
|
||||||
desk.ely_dialog = false;
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.action(|_| Button::new("ely-first", "First"))
|
|
||||||
.action(|_| Button::new("ely-second", "Second")),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Starts both libraries in order; Kit opens the window.
|
|
||||||
pub fn open(cx: &mut TestAppContext, ely_last: bool) -> (Entity<Desk>, VisualTestContext) {
|
|
||||||
cx.update(|cx| {
|
|
||||||
if ely_last {
|
|
||||||
gpui_kit::init(cx);
|
|
||||||
ely_gpui_component::init_for_tests(cx);
|
|
||||||
} else {
|
|
||||||
ely_gpui_component::init_for_tests(cx);
|
|
||||||
gpui_kit::init(cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let (window, desk) = cx.update(|cx| {
|
|
||||||
let options = WindowOptions {
|
|
||||||
window_bounds: Some(WindowBounds::Windowed(Bounds {
|
|
||||||
origin: point(px(0.), px(0.)),
|
|
||||||
size: size(px(800.), px(600.)),
|
|
||||||
})),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
gpui_kit::open_window(options, cx, |window, cx| {
|
|
||||||
cx.new(|cx| Desk {
|
|
||||||
root: cx.focus_handle(),
|
|
||||||
open: cx.focus_handle().tab_stop(true),
|
|
||||||
line: cx.new(|cx| InputState::new(window, cx)),
|
|
||||||
code: cx.new(|cx| {
|
|
||||||
EditorState::new(window, cx)
|
|
||||||
.language("rust")
|
|
||||||
.default_value(CODE)
|
|
||||||
}),
|
|
||||||
ely_dialog: false,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.expect("Kit opens the window")
|
|
||||||
});
|
|
||||||
let cx = VisualTestContext::from_window(window, cx);
|
|
||||||
cx.run_until_parked();
|
|
||||||
(desk, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether the handle holds focus.
|
|
||||||
pub fn focused(cx: &mut VisualTestContext, handle: &FocusHandle) -> bool {
|
|
||||||
cx.update(|window, _| handle.is_focused(window))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Key down and up; gpui presses on the release.
|
|
||||||
pub fn press(cx: &mut VisualTestContext, key: &str) {
|
|
||||||
cx.simulate_keystrokes(key);
|
|
||||||
cx.simulate_event(KeyUpEvent {
|
|
||||||
keystroke: Keystroke::parse(key).expect("a key"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use gpui_kit::{
|
|
||||||
FocusHandle, Focusable, Modifiers, TestAppContext, VisualTestContext, test::TestWindowExt,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::desk::{focused, open, press};
|
|
||||||
|
|
||||||
/// Draws until `done` holds; fails after a second.
|
|
||||||
fn settle(cx: &mut VisualTestContext, done: impl Fn(&mut VisualTestContext) -> bool) {
|
|
||||||
for _ in 0..50 {
|
|
||||||
cx.run_until_parked();
|
|
||||||
cx.update(|window, cx| window.render_frame(cx));
|
|
||||||
if done(cx) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
std::thread::sleep(Duration::from_millis(20));
|
|
||||||
}
|
|
||||||
panic!("the window did not settle");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn kit_dialog_open(cx: &mut VisualTestContext) -> bool {
|
|
||||||
cx.update(|window, _| window.try_find("dialog").is_some())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Opens Kit's dialog, tabs inside it, closes it.
|
|
||||||
fn kit_dialog_round_trip(
|
|
||||||
cx: &mut TestAppContext,
|
|
||||||
open_with: impl FnOnce(&mut VisualTestContext, &Handles),
|
|
||||||
back_to: impl Fn(&Handles) -> FocusHandle,
|
|
||||||
) {
|
|
||||||
let (desk, mut cx) = open(cx, true);
|
|
||||||
let handles = cx.update(|_, cx| {
|
|
||||||
let desk = desk.read(cx);
|
|
||||||
Handles {
|
|
||||||
button: desk.open.clone(),
|
|
||||||
line: desk.line.read(cx).focus_handle(cx),
|
|
||||||
code: desk.code.read(cx).focus_handle(cx),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
open_with(&mut cx, &handles);
|
|
||||||
settle(&mut cx, kit_dialog_open);
|
|
||||||
for _ in 0..3 {
|
|
||||||
cx.simulate_keystrokes("tab");
|
|
||||||
for handle in [&handles.button, &handles.line, &handles.code] {
|
|
||||||
assert!(!focused(&mut cx, handle), "Tab stays in Kit's dialog");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cx.simulate_keystrokes("escape");
|
|
||||||
settle(&mut cx, |cx| !kit_dialog_open(cx));
|
|
||||||
assert!(
|
|
||||||
focused(&mut cx, &back_to(&handles)),
|
|
||||||
"focus returns where it was"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Handles {
|
|
||||||
button: FocusHandle,
|
|
||||||
line: FocusHandle,
|
|
||||||
code: FocusHandle,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui_kit::test]
|
|
||||||
fn enter_on_an_ely_button_opens_a_kit_dialog_that_hands_focus_back(cx: &mut TestAppContext) {
|
|
||||||
kit_dialog_round_trip(
|
|
||||||
cx,
|
|
||||||
|cx, handles| {
|
|
||||||
cx.update(|window, cx| window.focus(&handles.button, cx));
|
|
||||||
press(cx, "enter");
|
|
||||||
},
|
|
||||||
|handles| handles.button.clone(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui_kit::test]
|
|
||||||
fn a_press_on_an_ely_button_leaves_focus_for_the_kit_dialog_to_return(cx: &mut TestAppContext) {
|
|
||||||
kit_dialog_round_trip(
|
|
||||||
cx,
|
|
||||||
|cx, handles| {
|
|
||||||
cx.update(|window, cx| window.focus(&handles.line, cx));
|
|
||||||
let bounds = cx.debug_bounds("ely-open").expect("Ely's button drew");
|
|
||||||
cx.simulate_click(bounds.center(), Modifiers::none());
|
|
||||||
},
|
|
||||||
|handles| handles.line.clone(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui_kit::test]
|
|
||||||
fn an_ely_dialog_over_kit_fields_traps_tab_and_hands_focus_back(cx: &mut TestAppContext) {
|
|
||||||
let (desk, mut cx) = open(cx, true);
|
|
||||||
let (button, line, code) = cx.update(|_, cx| {
|
|
||||||
let desk = desk.read(cx);
|
|
||||||
(
|
|
||||||
desk.open.clone(),
|
|
||||||
desk.line.read(cx).focus_handle(cx),
|
|
||||||
desk.code.read(cx).focus_handle(cx),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
cx.update(|window, cx| window.focus(&line, cx));
|
|
||||||
desk.update(&mut cx, |desk, cx| {
|
|
||||||
desk.ely_dialog = true;
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
settle(&mut cx, |cx| !focused(cx, &line));
|
|
||||||
for _ in 0..3 {
|
|
||||||
cx.simulate_keystrokes("tab");
|
|
||||||
for handle in [&button, &line, &code] {
|
|
||||||
assert!(!focused(&mut cx, handle), "Tab stays in Ely's dialog");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cx.simulate_keystrokes("escape");
|
|
||||||
settle(&mut cx, |cx| focused(cx, &line));
|
|
||||||
assert!(
|
|
||||||
!desk.read_with(&cx, |desk, _| desk.ely_dialog),
|
|
||||||
"Escape closed it"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
use gpui_kit::{Focusable, TestAppContext};
|
|
||||||
|
|
||||||
use crate::desk::{CODE, focused, open};
|
|
||||||
|
|
||||||
fn tab_runs_through_both(cx: &mut TestAppContext, ely_last: bool) {
|
|
||||||
let (desk, mut cx) = open(cx, ely_last);
|
|
||||||
let (open_button, line, code) = cx.update(|_, cx| {
|
|
||||||
let desk = desk.read(cx);
|
|
||||||
(desk.open.clone(), desk.line.clone(), desk.code.clone())
|
|
||||||
});
|
|
||||||
cx.update(|window, cx| window.focus(&open_button, cx));
|
|
||||||
cx.simulate_keystrokes("tab");
|
|
||||||
let line_focus = cx.update(|_, cx| line.read(cx).focus_handle(cx));
|
|
||||||
assert!(
|
|
||||||
focused(&mut cx, &line_focus),
|
|
||||||
"Tab leaves Ely's button for Kit's field"
|
|
||||||
);
|
|
||||||
cx.simulate_keystrokes("tab");
|
|
||||||
let code_focus = cx.update(|_, cx| code.read(cx).focus_handle(cx));
|
|
||||||
assert!(
|
|
||||||
focused(&mut cx, &code_focus),
|
|
||||||
"Tab reaches Kit's code editor"
|
|
||||||
);
|
|
||||||
cx.simulate_keystrokes("tab");
|
|
||||||
assert!(
|
|
||||||
focused(&mut cx, &code_focus),
|
|
||||||
"Tab stays in the code editor"
|
|
||||||
);
|
|
||||||
let text = cx.update(|_, cx| code.read(cx).value().to_string());
|
|
||||||
assert!(
|
|
||||||
text.len() > CODE.len() && text.trim() == CODE,
|
|
||||||
"Tab indents: {text:?}"
|
|
||||||
);
|
|
||||||
cx.simulate_keystrokes("shift-tab");
|
|
||||||
let text = cx.update(|_, cx| code.read(cx).value().to_string());
|
|
||||||
assert_eq!(text, CODE, "Shift-Tab outdents");
|
|
||||||
cx.update(|window, cx| window.focus(&line_focus, cx));
|
|
||||||
cx.simulate_keystrokes("shift-tab");
|
|
||||||
assert!(
|
|
||||||
focused(&mut cx, &open_button),
|
|
||||||
"Shift-Tab returns to Ely's button"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui_kit::test]
|
|
||||||
fn tab_runs_through_both_with_ely_started_last(cx: &mut TestAppContext) {
|
|
||||||
tab_runs_through_both(cx, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui_kit::test]
|
|
||||||
fn tab_runs_through_both_with_kit_started_last(cx: &mut TestAppContext) {
|
|
||||||
tab_runs_through_both(cx, false);
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
//! Ely and GPUI Kit in one app and one window.
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod assets;
|
|
||||||
#[cfg(test)]
|
|
||||||
mod desk;
|
|
||||||
#[cfg(test)]
|
|
||||||
mod dialogs;
|
|
||||||
#[cfg(test)]
|
|
||||||
mod focus;
|
|
||||||
@@ -54,7 +54,7 @@ fn main() {
|
|||||||
gpui_platform::application()
|
gpui_platform::application()
|
||||||
.with_assets(Assets)
|
.with_assets(Assets)
|
||||||
.run(|cx: &mut App| {
|
.run(|cx: &mut App| {
|
||||||
ely_gpui_component::init(cx).expect("Ely failed to start");
|
ely_gpui_component::init(cx);
|
||||||
cx.open_window(WindowOptions::default(), |window, cx| {
|
cx.open_window(WindowOptions::default(), |window, cx| {
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
let root = cx.focus_handle();
|
let root = cx.focus_handle();
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ fn main() {
|
|||||||
gpui_platform::application()
|
gpui_platform::application()
|
||||||
.with_assets(Assets)
|
.with_assets(Assets)
|
||||||
.run(|cx: &mut App| {
|
.run(|cx: &mut App| {
|
||||||
ely_gpui_component::init(cx).expect("Ely failed to start");
|
ely_gpui_component::init(cx);
|
||||||
cx.open_window(WindowOptions::default(), |window, cx| {
|
cx.open_window(WindowOptions::default(), |window, cx| {
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
let focus = cx.focus_handle();
|
let focus = cx.focus_handle();
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ fn main() {
|
|||||||
gpui_platform::application()
|
gpui_platform::application()
|
||||||
.with_assets(Assets)
|
.with_assets(Assets)
|
||||||
.run(|cx: &mut App| {
|
.run(|cx: &mut App| {
|
||||||
ely_gpui_component::init(cx).expect("Ely failed to start");
|
ely_gpui_component::init(cx);
|
||||||
cx.set_global(
|
cx.set_global(
|
||||||
I18n::new("en-US")
|
I18n::new("en-US")
|
||||||
.catalog(
|
.catalog(
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ fn main() {
|
|||||||
gpui_platform::application()
|
gpui_platform::application()
|
||||||
.with_assets(AppAssets)
|
.with_assets(AppAssets)
|
||||||
.run(|cx: &mut App| {
|
.run(|cx: &mut App| {
|
||||||
ely_gpui_component::init(cx).expect("Ely failed to start");
|
ely_gpui_component::init(cx);
|
||||||
IconTheme::default()
|
IconTheme::default()
|
||||||
.name("Cargo.toml", IconName::Package)
|
.name("Cargo.toml", IconName::Package)
|
||||||
.apply(cx);
|
.apply(cx);
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ fn main() {
|
|||||||
gpui_platform::application()
|
gpui_platform::application()
|
||||||
.with_assets(Assets)
|
.with_assets(Assets)
|
||||||
.run(|cx: &mut App| {
|
.run(|cx: &mut App| {
|
||||||
ely_gpui_component::init(cx).expect("Ely failed to start");
|
ely_gpui_component::init(cx);
|
||||||
cx.open_window(WindowOptions::default(), |window, cx| {
|
cx.open_window(WindowOptions::default(), |window, cx| {
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
let focus = cx.focus_handle();
|
let focus = cx.focus_handle();
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ fn main() {
|
|||||||
gpui_platform::application()
|
gpui_platform::application()
|
||||||
.with_assets(Assets)
|
.with_assets(Assets)
|
||||||
.run(|cx: &mut App| {
|
.run(|cx: &mut App| {
|
||||||
ely_gpui_component::init(cx).expect("Ely failed to start");
|
ely_gpui_component::init(cx);
|
||||||
cx.open_window(WindowOptions::default(), |window, cx| {
|
cx.open_window(WindowOptions::default(), |window, cx| {
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
let focus = cx.focus_handle();
|
let focus = cx.focus_handle();
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ fn main() {
|
|||||||
gpui_platform::application()
|
gpui_platform::application()
|
||||||
.with_assets(Assets)
|
.with_assets(Assets)
|
||||||
.run(|cx: &mut App| {
|
.run(|cx: &mut App| {
|
||||||
ely_gpui_component::init(cx).expect("Ely failed to start");
|
ely_gpui_component::init(cx);
|
||||||
cx.open_window(WindowOptions::default(), |window, cx| {
|
cx.open_window(WindowOptions::default(), |window, cx| {
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
let focus = cx.focus_handle();
|
let focus = cx.focus_handle();
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ fn main() {
|
|||||||
gpui_platform::application()
|
gpui_platform::application()
|
||||||
.with_assets(Assets)
|
.with_assets(Assets)
|
||||||
.run(|cx: &mut App| {
|
.run(|cx: &mut App| {
|
||||||
ely_gpui_component::init(cx).expect("Ely failed to start");
|
ely_gpui_component::init(cx);
|
||||||
Theme::set_mode_now(Mode::Dark, cx);
|
Theme::set_mode_now(Mode::Dark, cx);
|
||||||
let bounds = Bounds::centered(None, size(px(640.0), px(400.0)), cx);
|
let bounds = Bounds::centered(None, size(px(640.0), px(400.0)), cx);
|
||||||
let options = WindowOptions {
|
let options = WindowOptions {
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ fn main() {
|
|||||||
gpui_platform::application()
|
gpui_platform::application()
|
||||||
.with_assets(Assets)
|
.with_assets(Assets)
|
||||||
.run(|cx: &mut App| {
|
.run(|cx: &mut App| {
|
||||||
ely_gpui_component::init(cx).expect("Ely failed to start");
|
ely_gpui_component::init(cx);
|
||||||
cx.open_window(WindowOptions::default(), |window, cx| {
|
cx.open_window(WindowOptions::default(), |window, cx| {
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
let focus = cx.focus_handle();
|
let focus = cx.focus_handle();
|
||||||
|
|||||||
@@ -41,8 +41,7 @@ mod web;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
use anyhow::bail;
|
use anyhow::{Context as _, Result, bail};
|
||||||
use anyhow::{Context as _, Result};
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
use assets::GalleryAssets;
|
use assets::GalleryAssets;
|
||||||
use ely_gpui_component::theme::{Mode, Theme};
|
use ely_gpui_component::theme::{Mode, Theme};
|
||||||
@@ -114,8 +113,8 @@ pub struct Start {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sets Ely up and opens the gallery's window.
|
/// Sets Ely up and opens the gallery's window.
|
||||||
fn launch(start: Start, cx: &mut App) -> Result<WindowHandle<shell::Gallery>> {
|
fn launch(start: Start, cx: &mut App) -> WindowHandle<shell::Gallery> {
|
||||||
ely_gpui_component::init(cx)?;
|
ely_gpui_component::init(cx);
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
ely_gpui_component::tooling::install_inspector(cx);
|
ely_gpui_component::tooling::install_inspector(cx);
|
||||||
pages::bind_keys(cx);
|
pages::bind_keys(cx);
|
||||||
@@ -143,10 +142,10 @@ fn launch(start: Start, cx: &mut App) -> Result<WindowHandle<shell::Gallery>> {
|
|||||||
.open_window(options, |window, cx| {
|
.open_window(options, |window, cx| {
|
||||||
cx.new(|cx| shell::Gallery::new(start, window, cx))
|
cx.new(|cx| shell::Gallery::new(start, window, cx))
|
||||||
})
|
})
|
||||||
.context("gallery window failed to open")?;
|
.expect("gallery window failed to open");
|
||||||
menu_actions(window, cx);
|
menu_actions(window, cx);
|
||||||
cx.activate(true);
|
cx.activate(true);
|
||||||
Ok(window)
|
window
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
@@ -205,13 +204,7 @@ fn main() -> Result<()> {
|
|||||||
gpui_platform::application()
|
gpui_platform::application()
|
||||||
.with_assets(GalleryAssets)
|
.with_assets(GalleryAssets)
|
||||||
.run(move |cx: &mut App| {
|
.run(move |cx: &mut App| {
|
||||||
let window = match launch(start, cx) {
|
let window = launch(start, cx);
|
||||||
Ok(window) => window,
|
|
||||||
Err(error) => {
|
|
||||||
log::error!("gallery: {error:#}");
|
|
||||||
return cx.quit();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if let Some(dir) = capture {
|
if let Some(dir) = capture {
|
||||||
capture::run(window, dir, page, scripted, cx);
|
capture::run(window, dir, page, scripted, cx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,20 +73,23 @@ pub fn switchers(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
|
|||||||
.gap_6()
|
.gap_6()
|
||||||
.child(
|
.child(
|
||||||
div().w(px(260.)).flex().flex_col().gap_2().child(
|
div().w(px(260.)).flex().flex_col().gap_2().child(
|
||||||
WorkspaceSwitcher::new("account-workspaces", workspaces, workspace, move |key, _, cx| change(&to_workspace, cx, |places| places.workspace = key.clone()))
|
WorkspaceSwitcher::new("account-workspaces", workspaces, workspace)
|
||||||
|
.on_select(move |key, _, cx| change(&to_workspace, cx, |places| places.workspace = key.clone()))
|
||||||
.on_create(|_, _| log::info!("gallery: create workspace")),
|
.on_create(|_, _| log::info!("gallery: create workspace")),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
AccountSwitcher::new("account-accounts", accounts, account, move |key, _, cx| change(&to_account, cx, |places| places.account = key.clone()))
|
AccountSwitcher::new("account-accounts", accounts, account)
|
||||||
|
.on_select(move |key, _, cx| change(&to_account, cx, |places| places.account = key.clone()))
|
||||||
.on_add(|_, _| log::info!("gallery: add account")),
|
.on_add(|_, _| log::info!("gallery: add account")),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(probe(
|
.child(probe(
|
||||||
"account-user",
|
"account-user",
|
||||||
UserMenu::new("account-user", "Ada Lovelace", "ada@atlas.dev", |_, _| log::info!("gallery: sign out"))
|
UserMenu::new("account-user", "Ada Lovelace", "ada@atlas.dev")
|
||||||
.presence(Presence::Online)
|
.presence(Presence::Online)
|
||||||
.item(MenuItem::new("Profile").icon(IconName::User).on_click(|_, _| log::info!("gallery: profile")))
|
.item(MenuItem::new("Profile").icon(IconName::User).on_click(|_, _| log::info!("gallery: profile")))
|
||||||
.item(MenuItem::new("Settings").icon(IconName::Settings).on_click(|_, _| log::info!("gallery: settings"))),
|
.item(MenuItem::new("Settings").icon(IconName::Settings).on_click(|_, _| log::info!("gallery: settings")))
|
||||||
|
.on_sign_out(|_, _| log::info!("gallery: sign out")),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -130,7 +133,7 @@ pub fn profile(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
.action(Button::new("account-card-profile", "View profile")),
|
.action(Button::new("account-card-profile", "View profile")),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(div().w(px(400.)).child(ProfileEditor::new("account-editor", now, move |profile, _, cx| {
|
.child(div().w(px(400.)).child(ProfileEditor::new("account-editor", now).on_save(move |profile, _, cx| {
|
||||||
let profile = profile.clone();
|
let profile = profile.clone();
|
||||||
change(&saved, cx, move |now| *now = profile)
|
change(&saved, cx, move |now| *now = profile)
|
||||||
}))),
|
}))),
|
||||||
@@ -212,10 +215,8 @@ pub fn security_page(window: &mut Window, cx: &mut App) -> impl IntoElement + us
|
|||||||
let now = state.read(cx);
|
let now = state.read(cx);
|
||||||
let (sessions, keys, secret) = (now.sessions.clone(), now.keys.clone(), now.secret.clone());
|
let (sessions, keys, secret) = (now.sessions.clone(), now.keys.clone(), now.secret.clone());
|
||||||
let [out, others, made, revoked, done] = [(); 5].map(|_| state.clone());
|
let [out, others, made, revoked, done] = [(); 5].map(|_| state.clone());
|
||||||
let mut manager = ApiKeyManager::new(
|
let mut manager = ApiKeyManager::new("account-keys", keys)
|
||||||
"account-keys",
|
.on_create(move |name, _, cx| {
|
||||||
keys,
|
|
||||||
move |name, _, cx| {
|
|
||||||
let name = name.clone();
|
let name = name.clone();
|
||||||
change(&made, cx, move |security| {
|
change(&made, cx, move |security| {
|
||||||
security.made += 1;
|
security.made += 1;
|
||||||
@@ -232,15 +233,14 @@ pub fn security_page(window: &mut Window, cx: &mut App) -> impl IntoElement + us
|
|||||||
);
|
);
|
||||||
security.secret = Some("ely_live_5f1c0a9e2b7d4c8a3e6f7d3e".into());
|
security.secret = Some("ely_live_5f1c0a9e2b7d4c8a3e6f7d3e".into());
|
||||||
})
|
})
|
||||||
},
|
})
|
||||||
move |key, _, cx| {
|
.on_revoke(move |key, _, cx| {
|
||||||
let key = key.clone();
|
let key = key.clone();
|
||||||
change(&revoked, cx, move |security| {
|
change(&revoked, cx, move |security| {
|
||||||
security.keys.retain(|each| each.key != key)
|
security.keys.retain(|each| each.key != key)
|
||||||
})
|
})
|
||||||
},
|
})
|
||||||
move |_, cx| change(&done, cx, |security| security.secret = None),
|
.on_dismiss(move |_, cx| change(&done, cx, |security| security.secret = None));
|
||||||
);
|
|
||||||
if let Some(secret) = secret {
|
if let Some(secret) = secret {
|
||||||
manager = manager.secret(secret);
|
manager = manager.secret(secret);
|
||||||
}
|
}
|
||||||
@@ -256,7 +256,8 @@ pub fn security_page(window: &mut Window, cx: &mut App) -> impl IntoElement + us
|
|||||||
.items_start()
|
.items_start()
|
||||||
.gap_8()
|
.gap_8()
|
||||||
.child(div().w(px(380.)).child(
|
.child(div().w(px(380.)).child(
|
||||||
SessionList::new("account-sessions", sessions, move |key, _, cx| {
|
SessionList::new("account-sessions", sessions)
|
||||||
|
.on_sign_out(move |key, _, cx| {
|
||||||
let key = key.clone();
|
let key = key.clone();
|
||||||
change(&out, cx, move |security| security.sessions.retain(|each| each.key != key))
|
change(&out, cx, move |security| security.sessions.retain(|each| each.key != key))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ pub fn plan(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(div().w(px(360.)).flex().flex_col().gap_3().child(
|
.child(div().w(px(360.)).flex().flex_col().gap_3().child(
|
||||||
PaymentMethodForm::new("account-card-form", move |card, _, cx| {
|
PaymentMethodForm::new("account-card-form").on_submit(move |card, _, cx| {
|
||||||
let saved_as: SharedString = format!("{} ending {}", card.brand.name(), &card.number[card.number.len() - 4..]).into();
|
let saved_as: SharedString = format!("{} ending {}", card.brand.name(), &card.number[card.number.len() - 4..]).into();
|
||||||
change(&saved, cx, move |plan| plan.card = Some(saved_as))
|
change(&saved, cx, move |plan| plan.card = Some(saved_as))
|
||||||
}),
|
}),
|
||||||
@@ -116,16 +116,18 @@ pub fn history(_: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_4()
|
.gap_4()
|
||||||
.child(
|
.child(
|
||||||
UpgradePrompt::new("account-nudge", "9 of 10 projects used", |_, _| log::info!("gallery: upgrade"))
|
UpgradePrompt::new("account-nudge", "9 of 10 projects used")
|
||||||
.body("Pro has no limit on projects.")
|
.body("Pro has no limit on projects.")
|
||||||
|
.on_upgrade(|_, _| log::info!("gallery: upgrade"))
|
||||||
.on_dismiss(|_, _| log::info!("gallery: not now")),
|
.on_dismiss(|_, _| log::info!("gallery: not now")),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
UpgradePrompt::new("account-wall", "Version history is on Pro", |_, _| log::info!("gallery: upgrade"))
|
UpgradePrompt::new("account-wall", "Version history is on Pro")
|
||||||
.body("See and restore every version of a document.")
|
.body("See and restore every version of a document.")
|
||||||
.benefit("Every version, kept a year")
|
.benefit("Every version, kept a year")
|
||||||
.benefit("Restore with one press")
|
.benefit("Restore with one press")
|
||||||
.benefit("Compare any two")
|
.benefit("Compare any two")
|
||||||
|
.on_upgrade(|_, _| log::info!("gallery: upgrade"))
|
||||||
.on_dismiss(|_, _| log::info!("gallery: not now")),
|
.on_dismiss(|_, _| log::info!("gallery: not now")),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -205,22 +207,27 @@ pub fn members(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
.gap_6()
|
.gap_6()
|
||||||
.w(px(640.))
|
.w(px(640.))
|
||||||
.child(
|
.child(
|
||||||
TeamMemberTable::new("account-members", members, roles(), move |key, next, _, cx| {
|
TeamMemberTable::new("account-members", members, roles())
|
||||||
|
.on_role(move |key, next, _, cx| {
|
||||||
let (key, next) = (key.clone(), next.clone());
|
let (key, next) = (key.clone(), next.clone());
|
||||||
change(&role, cx, move |team| team.members.iter_mut().filter(|member| member.key == key).for_each(|member| member.role = next.clone()))
|
change(&role, cx, move |team| team.members.iter_mut().filter(|member| member.key == key).for_each(|member| member.role = next.clone()))
|
||||||
}, move |key, _, cx| {
|
})
|
||||||
|
.on_remove(move |key, _, cx| {
|
||||||
let key = key.clone();
|
let key = key.clone();
|
||||||
change(&removed, cx, move |team| team.members.retain(|member| member.key != key))
|
change(&removed, cx, move |team| team.members.retain(|member| member.key != key))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
InvitationList::new("account-invites", invitations, roles(), move |email, role, _, cx| {
|
InvitationList::new("account-invites", invitations, roles())
|
||||||
|
.on_invite(move |email, role, _, cx| {
|
||||||
let (email, role) = (email.clone(), role.clone());
|
let (email, role) = (email.clone(), role.clone());
|
||||||
change(&invited, cx, move |team| {
|
change(&invited, cx, move |team| {
|
||||||
team.sent += 1;
|
team.sent += 1;
|
||||||
team.invitations.insert(0, Invitation { key: format!("new-{}", team.sent).into(), email, role, sent: "Sent just now".into(), expired: false });
|
team.invitations.insert(0, Invitation { key: format!("new-{}", team.sent).into(), email, role, sent: "Sent just now".into(), expired: false });
|
||||||
})
|
})
|
||||||
}, |key, _, _| log::info!("gallery: resend {key}"), move |key, _, cx| {
|
})
|
||||||
|
.on_resend(|key, _, _| log::info!("gallery: resend {key}"))
|
||||||
|
.on_revoke(move |key, _, cx| {
|
||||||
let key = key.clone();
|
let key = key.clone();
|
||||||
change(&revoked, cx, move |team| team.invitations.retain(|each| each.key != key))
|
change(&revoked, cx, move |team| team.invitations.retain(|each| each.key != key))
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -30,18 +30,19 @@ pub fn sign_in(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
("google", "Google"),
|
("google", "Google"),
|
||||||
("apple", "Apple"),
|
("apple", "Apple"),
|
||||||
],
|
],
|
||||||
move |key, _, cx| change(&picked, cx, |state| state.provider = Some(key.clone())),
|
)
|
||||||
);
|
.on_pick(move |key, _, cx| change(&picked, cx, |state| state.provider = Some(key.clone())));
|
||||||
if let Some(provider) = provider {
|
if let Some(provider) = provider {
|
||||||
providers = providers.busy(provider);
|
providers = providers.busy(provider);
|
||||||
}
|
}
|
||||||
let mut form = LoginForm::new("account-login", move |_, _, cx| {
|
let mut form = LoginForm::new("account-login")
|
||||||
change(&tried, cx, |state| {
|
.on_submit(move |_, _, cx| {
|
||||||
state.error = Some("No account uses that email and password.".into())
|
change(&tried, cx, |state| {
|
||||||
|
state.error = Some("No account uses that email and password.".into())
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
.on_forgot(|_, _| log::info!("gallery: forgot password"))
|
||||||
.on_forgot(|_, _| log::info!("gallery: forgot password"))
|
.on_sign_up(|_, _| log::info!("gallery: sign up"));
|
||||||
.on_sign_up(|_, _| log::info!("gallery: sign up"));
|
|
||||||
if let Some(error) = error {
|
if let Some(error) = error {
|
||||||
form = form.error(error);
|
form = form.error(error);
|
||||||
}
|
}
|
||||||
@@ -70,12 +71,13 @@ pub fn sign_up(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
let state = keep("account-sign-up", SignUp::default, window, cx);
|
let state = keep("account-sign-up", SignUp::default, window, cx);
|
||||||
let error = state.read(cx).error.clone();
|
let error = state.read(cx).error.clone();
|
||||||
let tried = state.clone();
|
let tried = state.clone();
|
||||||
let mut form = SignupForm::new("account-signup", move |signup, _, cx| {
|
let mut form = SignupForm::new("account-signup")
|
||||||
let taken = format!("An account already uses {}.", signup.email);
|
.terms("I agree to the terms of service")
|
||||||
change(&tried, cx, |state| state.error = Some(taken.into()))
|
.on_submit(move |signup, _, cx| {
|
||||||
})
|
let taken = format!("An account already uses {}.", signup.email);
|
||||||
.terms("I agree to the terms of service")
|
change(&tried, cx, |state| state.error = Some(taken.into()))
|
||||||
.on_sign_in(|_, _| log::info!("gallery: sign in"));
|
})
|
||||||
|
.on_sign_in(|_, _| log::info!("gallery: sign in"));
|
||||||
if let Some(error) = error {
|
if let Some(error) = error {
|
||||||
form = form.error(error);
|
form = form.error(error);
|
||||||
}
|
}
|
||||||
@@ -113,8 +115,9 @@ pub fn by_email(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
"account-magic",
|
"account-magic",
|
||||||
div().w(px(360.)).child(
|
div().w(px(360.)).child(
|
||||||
Card::new().header(CardHeader::new("Sign in by email")).child(
|
Card::new().header(CardHeader::new("Sign in by email")).child(
|
||||||
MagicLinkForm::new("account-magic", move |_, _, cx| change(&sent, cx, |state| state.magic = true))
|
MagicLinkForm::new("account-magic")
|
||||||
.sent(magic)
|
.sent(magic)
|
||||||
|
.on_send(move |_, _, cx| change(&sent, cx, |state| state.magic = true))
|
||||||
.on_other(move |_, cx| change(&other, cx, |state| state.magic = false)),
|
.on_other(move |_, cx| change(&other, cx, |state| state.magic = false)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -122,8 +125,9 @@ pub fn by_email(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
.child(
|
.child(
|
||||||
div().w(px(360.)).child(
|
div().w(px(360.)).child(
|
||||||
Card::new().header(CardHeader::new("Reset your password")).child(
|
Card::new().header(CardHeader::new("Reset your password")).child(
|
||||||
ForgotPassword::new("account-forgot", move |_, _, cx| change(&asked, cx, |state| state.reset = true))
|
ForgotPassword::new("account-forgot")
|
||||||
.sent(reset)
|
.sent(reset)
|
||||||
|
.on_send(move |_, _, cx| change(&asked, cx, |state| state.reset = true))
|
||||||
.on_back(move |_, cx| change(&back, cx, |state| state.reset = false)),
|
.on_back(move |_, cx| change(&back, cx, |state| state.reset = false)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -157,13 +161,14 @@ pub fn second_factor(window: &mut Window, cx: &mut App) -> impl IntoElement + us
|
|||||||
Card::new()
|
Card::new()
|
||||||
.header(CardHeader::new("Two-step sign-in"))
|
.header(CardHeader::new("Two-step sign-in"))
|
||||||
.child(
|
.child(
|
||||||
TwoFactorInput::new("account-factor", move |code, _, cx| {
|
TwoFactorInput::new("account-factor")
|
||||||
|
.failed(failed)
|
||||||
|
.on_code(move |code, _, cx| {
|
||||||
change(&checked, cx, |state| match code.as_ref() == CODE {
|
change(&checked, cx, |state| match code.as_ref() == CODE {
|
||||||
true => state.passed = true,
|
true => state.passed = true,
|
||||||
false => state.failed += 1,
|
false => state.failed += 1,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.failed(failed)
|
|
||||||
.on_recovery(|_, _| log::info!("gallery: recovery code")),
|
.on_recovery(|_, _| log::info!("gallery: recovery code")),
|
||||||
)
|
)
|
||||||
.children(passed.then(|| InlineMessage::new(Severity::Success, "Signed in."))),
|
.children(passed.then(|| InlineMessage::new(Severity::Success, "Signed in."))),
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ pub fn small_buttons(cx: &App) -> impl IntoElement + use<> {
|
|||||||
"share-button",
|
"share-button",
|
||||||
ShareButton::new("share-page")
|
ShareButton::new("share-page")
|
||||||
.text("Ely, components for GPUI")
|
.text("Ely, components for GPUI")
|
||||||
.url("https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components"),
|
.url("https://github.com/ZacharyZhang-NY/Ely-GPUI-Components"),
|
||||||
),
|
),
|
||||||
cx,
|
cx,
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -221,26 +221,18 @@ pub fn mind(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
change(&added, cx, |mind| {
|
change(&added, cx, |mind| {
|
||||||
mind.made += 1;
|
mind.made += 1;
|
||||||
let key = SharedString::from(format!("idea-{}", mind.made));
|
let key = SharedString::from(format!("idea-{}", mind.made));
|
||||||
if let Some(root) = mind.root.adding(parent, Topic::new(key.clone(), "New idea")) {
|
mind.root = mind.root.adding(parent, Topic::new(key.clone(), "New idea"));
|
||||||
mind.root = root;
|
mind.selected = Some(key);
|
||||||
mind.selected = Some(key);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.on_remove(move |key, _, cx| {
|
.on_remove(move |key, _, cx| {
|
||||||
change(&removed, cx, |mind| {
|
change(&removed, cx, |mind| {
|
||||||
if let Some(root) = mind.root.removing(key) {
|
mind.selected = mind.root.parent_of(key);
|
||||||
mind.selected = mind.root.parent_of(key);
|
mind.root = mind.root.removing(key);
|
||||||
mind.root = root;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.on_rename(move |key, text, _, cx| {
|
.on_rename(move |key, text, _, cx| {
|
||||||
change(&renamed, cx, |mind| {
|
change(&renamed, cx, |mind| mind.root = mind.root.renaming(key, text.clone()))
|
||||||
if let Some(root) = mind.root.renaming(key, text.clone()) {
|
|
||||||
mind.root = root;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.on_viewport(move |next, _, cx| change(&viewed, cx, |mind| mind.view = next)),
|
.on_viewport(move |next, _, cx| change(&viewed, cx, |mind| mind.view = next)),
|
||||||
cx,
|
cx,
|
||||||
|
|||||||
@@ -121,8 +121,9 @@ pub fn styles(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
.gap_6()
|
.gap_6()
|
||||||
.child(
|
.child(
|
||||||
div().w(px(260.)).child(
|
div().w(px(260.)).child(
|
||||||
ColorPanel::new("canvas-color", color, move |next, _, cx| change(&colored, cx, |look| look.color = next))
|
ColorPanel::new("canvas-color", color)
|
||||||
.swatches(SWATCHES.map(|(name, hex)| (name, Hsla::from(rgb(hex))))),
|
.swatches(SWATCHES.map(|(name, hex)| (name, Hsla::from(rgb(hex)))))
|
||||||
|
.on_change(move |next, _, cx| change(&colored, cx, |look| look.color = next)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -136,7 +137,8 @@ pub fn styles(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
.on_change(move |next, _, cx| change(&graded, cx, |look| look.stops = next.to_vec())),
|
.on_change(move |next, _, cx| change(&graded, cx, |look| look.stops = next.to_vec())),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
ShadowEditor::new("canvas-shadows", shadows, move |next, _, cx| change(&shaded, cx, |look| look.shadows = next)),
|
ShadowEditor::new("canvas-shadows", shadows)
|
||||||
|
.on_change(move |next, _, cx| change(&shaded, cx, |look| look.shadows = next)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -146,13 +148,15 @@ pub fn styles(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_6()
|
.gap_6()
|
||||||
.child(
|
.child(
|
||||||
BorderEditor::new("canvas-border", border, move |next, _, cx| change(&bordered, cx, |look| look.border = next)),
|
BorderEditor::new("canvas-border", border)
|
||||||
|
.on_change(move |next, _, cx| change(&bordered, cx, |look| look.border = next)),
|
||||||
)
|
)
|
||||||
.child(div().p_6().child(card)),
|
.child(div().p_6().child(card)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().w(px(280.)).child(
|
div().w(px(280.)).child(
|
||||||
TypographyPanel::new("canvas-type", style, &search, move |next, _, cx| change(&typed, cx, |look| look.style = next)),
|
TypographyPanel::new("canvas-type", style, &search)
|
||||||
|
.on_change(move |next, _, cx| change(&typed, cx, |look| look.style = next)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -278,7 +282,8 @@ pub fn rules(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
.child(
|
.child(
|
||||||
ConstraintsEditor::new("canvas-constraints", constraints, move |next, _, cx| change(&pinned, cx, |rules| rules.constraints = next)),
|
ConstraintsEditor::new("canvas-constraints", constraints)
|
||||||
|
.on_change(move |next, _, cx| change(&pinned, cx, |rules| rules.constraints = next)),
|
||||||
)
|
)
|
||||||
.child(pinned_preview),
|
.child(pinned_preview),
|
||||||
)
|
)
|
||||||
@@ -289,7 +294,8 @@ pub fn rules(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
.child(
|
.child(
|
||||||
AutoLayoutControls::new("canvas-auto-layout", layout, move |next, _, cx| change(&laid, cx, |rules| rules.layout = next)),
|
AutoLayoutControls::new("canvas-auto-layout", layout)
|
||||||
|
.on_change(move |next, _, cx| change(&laid, cx, |rules| rules.layout = next)),
|
||||||
)
|
)
|
||||||
.child(laid_preview),
|
.child(laid_preview),
|
||||||
)
|
)
|
||||||
@@ -300,7 +306,9 @@ pub fn rules(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
.child(
|
.child(
|
||||||
ExportPanel::new("canvas-export", "Card", exports, move |next, _, cx| change(&set, cx, |rules| rules.exports = next), move |files, _, cx| change(&sent, cx, |rules| rules.exported = files)),
|
ExportPanel::new("canvas-export", "Card", exports)
|
||||||
|
.on_change(move |next, _, cx| change(&set, cx, |rules| rules.exports = next))
|
||||||
|
.on_export(move |files, _, cx| change(&sent, cx, |rules| rules.exported = files)),
|
||||||
)
|
)
|
||||||
.when(!exported.is_empty(), |panel| {
|
.when(!exported.is_empty(), |panel| {
|
||||||
panel.child(note(format!("Exported {}", exported.join(", ")).into()))
|
panel.child(note(format!("Exported {}", exported.join(", ")).into()))
|
||||||
|
|||||||
@@ -134,18 +134,19 @@ pub fn arrange(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
studio.record(align.words().into());
|
studio.record(align.words().into());
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
let inspector = InspectorPanel::new("canvas-inspector", chosen, move |shape, _, cx| {
|
let inspector =
|
||||||
change(&edited, cx, |studio| {
|
InspectorPanel::new("canvas-inspector", chosen).on_change(move |shape, _, cx| {
|
||||||
let at = studio
|
change(&edited, cx, |studio| {
|
||||||
.shapes
|
let at = studio
|
||||||
.iter()
|
.shapes
|
||||||
.position(|each| each.key == shape.key)
|
.iter()
|
||||||
.expect("a listed shape");
|
.position(|each| each.key == shape.key)
|
||||||
let words = format!("Edit {}", shape.name);
|
.expect("a listed shape");
|
||||||
studio.shapes[at] = shape;
|
let words = format!("Edit {}", shape.name);
|
||||||
studio.record(words);
|
studio.shapes[at] = shape;
|
||||||
})
|
studio.record(words);
|
||||||
});
|
})
|
||||||
|
});
|
||||||
let assets = AssetPanel::new("canvas-assets")
|
let assets = AssetPanel::new("canvas-assets")
|
||||||
.group("Shapes", shapes())
|
.group("Shapes", shapes())
|
||||||
.group("Notes", notes())
|
.group("Notes", notes())
|
||||||
|
|||||||
@@ -261,10 +261,7 @@ const BASE: &str = "A lift blends color toward white. Half a lift sits midway be
|
|||||||
fn applied(base: &str, suggestions: Vec<Suggestion>) -> String {
|
fn applied(base: &str, suggestions: Vec<Suggestion>) -> String {
|
||||||
let (mut text, mut left) = (base.to_string(), suggestions);
|
let (mut text, mut left) = (base.to_string(), suggestions);
|
||||||
while !left.is_empty() {
|
while !left.is_empty() {
|
||||||
let Some(taken) = accept(&text, &left, 0) else {
|
(text, left) = accept(&text, &left, 0);
|
||||||
break;
|
|
||||||
};
|
|
||||||
(text, left) = taken;
|
|
||||||
}
|
}
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
@@ -309,16 +306,11 @@ pub fn changes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
let pending = suggested(&base_text, field.read(cx).text(), &you());
|
let pending = suggested(&base_text, field.read(cx).text(), &you());
|
||||||
match decision {
|
match decision {
|
||||||
Decision::Accept(ix) => {
|
Decision::Accept(ix) => {
|
||||||
if let Some((text, _)) = accept(&base_text, &pending, ix) {
|
set(&decided, accept(&base_text, &pending, ix).0, cx)
|
||||||
set(&decided, text, cx)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Decision::AcceptAll => set(&decided, applied(&base_text, pending), cx),
|
Decision::AcceptAll => set(&decided, applied(&base_text, pending), cx),
|
||||||
Decision::Reject(ix) => {
|
Decision::Reject(ix) => {
|
||||||
let mut rest = pending;
|
let mut rest = pending;
|
||||||
if ix >= rest.len() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rest.remove(ix);
|
rest.remove(ix);
|
||||||
let text = applied(&base_text, rest);
|
let text = applied(&base_text, rest);
|
||||||
field.update(cx, |input, cx| input.set_text(text, cx));
|
field.update(cx, |input, cx| input.set_text(text, cx));
|
||||||
|
|||||||
@@ -82,51 +82,50 @@ pub fn grid(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
picked: service,
|
picked: service,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
let grid = DashboardGrid::new("dashboard-grid", 12, tiles, move |next, _, cx| {
|
let grid = DashboardGrid::new("dashboard-grid", 12, tiles)
|
||||||
change(&moved, cx, |wall| wall.tiles = next)
|
.card(
|
||||||
})
|
"requests",
|
||||||
.card(
|
DashboardCard::new("Requests").subtitle("per minute").body(
|
||||||
"requests",
|
Statistic::new("dashboard-requests", "This hour", 12_400.0)
|
||||||
DashboardCard::new("Requests").subtitle("per minute").body(
|
.trend(TrendIndicator::new(0.082)),
|
||||||
Statistic::new("dashboard-requests", "This hour", 12_400.0)
|
),
|
||||||
.trend(TrendIndicator::new(0.082)),
|
)
|
||||||
),
|
.card(
|
||||||
)
|
"errors",
|
||||||
.card(
|
DashboardCard::new("Errors").subtitle("5xx share").body(
|
||||||
"errors",
|
Statistic::new("dashboard-errors", "This hour", 0.42)
|
||||||
DashboardCard::new("Errors").subtitle("5xx share").body(
|
.decimals(2)
|
||||||
Statistic::new("dashboard-errors", "This hour", 0.42)
|
.suffix("%")
|
||||||
.decimals(2)
|
.trend(TrendIndicator::new(-0.12)),
|
||||||
.suffix("%")
|
),
|
||||||
.trend(TrendIndicator::new(-0.12)),
|
)
|
||||||
),
|
.card(
|
||||||
)
|
"cpu",
|
||||||
.card(
|
DashboardCard::new("CPU").body(Gauge::new(
|
||||||
"cpu",
|
"dashboard-cpu",
|
||||||
DashboardCard::new("CPU").body(Gauge::new(
|
"percent busy",
|
||||||
"dashboard-cpu",
|
62.0,
|
||||||
"percent busy",
|
0.0,
|
||||||
62.0,
|
100.0,
|
||||||
0.0,
|
)),
|
||||||
100.0,
|
)
|
||||||
)),
|
.card(
|
||||||
)
|
"latency",
|
||||||
.card(
|
DashboardCard::new("Latency")
|
||||||
"latency",
|
.subtitle("p95, milliseconds")
|
||||||
DashboardCard::new("Latency")
|
.body(LineChart::new("dashboard-latency", hours).series(latency)),
|
||||||
.subtitle("p95, milliseconds")
|
)
|
||||||
.body(LineChart::new("dashboard-latency", hours).series(latency)),
|
.card(
|
||||||
)
|
"saturation",
|
||||||
.card(
|
DashboardCard::new("Saturation").body(Gauge::new(
|
||||||
"saturation",
|
"dashboard-memory",
|
||||||
DashboardCard::new("Saturation").body(Gauge::new(
|
"memory in use",
|
||||||
"dashboard-memory",
|
81.0,
|
||||||
"memory in use",
|
0.0,
|
||||||
81.0,
|
100.0,
|
||||||
0.0,
|
)),
|
||||||
100.0,
|
)
|
||||||
)),
|
.on_change(move |next, _, cx| change(&moved, cx, |wall| wall.tiles = next));
|
||||||
);
|
|
||||||
section(
|
section(
|
||||||
"Dashboard Grid · Widget / DashboardCard · RefreshIntervalSelector · DashboardFilterBar · StatCard → data_display::Statistic · MetricsChart → charts::LineChart · ResourceGauge → data_display::Gauge",
|
"Dashboard Grid · Widget / DashboardCard · RefreshIntervalSelector · DashboardFilterBar · StatCard → data_display::Statistic · MetricsChart → charts::LineChart · ResourceGauge → data_display::Gauge",
|
||||||
"Widgets on a grid of twelve columns: a tile's top strip drags it and its corner resizes it, a cell at a time, while the others make room and rise to fill gaps; a focused tile steps with the arrows. Over it, the window of time and the filters, and how often it refreshes.",
|
"Widgets on a grid of twelve columns: a tile's top strip drags it and its corner resizes it, a cell at a time, while the others make room and rise to fill gaps; a focused tile steps with the arrows. Over it, the window of time and the filters, and how often it refreshes.",
|
||||||
@@ -144,7 +143,9 @@ pub fn grid(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
.justify_between()
|
.justify_between()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
.child(
|
.child(
|
||||||
DashboardFilterBar::new("dashboard-filters", time, filters, move |next, _, cx| change(&windowed, cx, |wall| wall.window = next), move |key, value, _, cx| {
|
DashboardFilterBar::new("dashboard-filters", time, filters)
|
||||||
|
.on_window(move |next, _, cx| change(&windowed, cx, |wall| wall.window = next))
|
||||||
|
.on_filter(move |key, value, _, cx| {
|
||||||
change(&filtered, cx, |wall| match key.as_ref() {
|
change(&filtered, cx, |wall| match key.as_ref() {
|
||||||
"environment" => wall.environment = value,
|
"environment" => wall.environment = value,
|
||||||
_ => wall.service = value,
|
_ => wall.service = value,
|
||||||
@@ -152,7 +153,9 @@ pub fn grid(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
RefreshIntervalSelector::new("dashboard-refresh", every, clock - jiff::SignedDuration::from_secs(since), clock, move |next, _, cx| change(&timed, cx, |wall| wall.every = next), move |_, cx| change(&refreshed, cx, |wall| wall.since = 0)),
|
RefreshIntervalSelector::new("dashboard-refresh", every, clock - jiff::SignedDuration::from_secs(since), clock)
|
||||||
|
.on_change(move |next, _, cx| change(&timed, cx, |wall| wall.every = next))
|
||||||
|
.on_refresh(move |_, cx| change(&refreshed, cx, |wall| wall.since = 0)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(div().w(px(900.)).child(grid)),
|
.child(div().w(px(900.)).child(grid)),
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub fn codes(cx: &mut App) -> impl IntoElement + use<> {
|
pub fn codes(cx: &mut App) -> impl IntoElement + use<> {
|
||||||
let qr = QrCode::new("https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components")
|
let qr = QrCode::new("https://github.com/ZacharyZhang-NY/Ely-GPUI-Components")
|
||||||
.expect("a short link fits a QR code");
|
.expect("a short link fits a QR code");
|
||||||
let label = Barcode::code128("ELY-2419").expect("printable ASCII");
|
let label = Barcode::code128("ELY-2419").expect("printable ASCII");
|
||||||
let retail = Barcode::ean13("400638133393").expect("twelve digits");
|
let retail = Barcode::ean13("400638133393").expect("twelve digits");
|
||||||
|
|||||||
@@ -156,7 +156,8 @@ pub fn requests(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_4()
|
.gap_4()
|
||||||
.child(
|
.child(
|
||||||
EnvironmentSelector::new("devtools-environments", environments(), environment, move |key, _, cx| change(&chose, cx, |desk| desk.environment = key.clone())),
|
EnvironmentSelector::new("devtools-environments", environments(), environment)
|
||||||
|
.on_change(move |key, _, cx| change(&chose, cx, |desk| desk.environment = key.clone())),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -177,11 +178,7 @@ pub fn requests(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.on_move(move |key, target, at, _, cx| {
|
.on_move(move |key, target, at, _, cx| {
|
||||||
change(&dragged, cx, |desk| {
|
change(&dragged, cx, |desk| desk.collection = moved(&desk.collection, key, target, at))
|
||||||
if let Some(collection) = moved(&desk.collection, key, target, at) {
|
|
||||||
desk.collection = collection;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -258,7 +255,8 @@ pub fn socket(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().w(px(620.)).child(
|
div().w(px(620.)).child(
|
||||||
WebSocketConsole::new("devtools-socket", "wss://stream.example.com/orders", state, messages, &draft, clock, move |text, _, cx| {
|
WebSocketConsole::new("devtools-socket", "wss://stream.example.com/orders", state, messages, &draft, clock)
|
||||||
|
.on_send(move |text, _, cx| {
|
||||||
sent.update(cx, |line, cx| {
|
sent.update(cx, |line, cx| {
|
||||||
let at = line.now;
|
let at = line.now;
|
||||||
line.messages.push(SocketMessage { direction: Direction::Sent, text, at });
|
line.messages.push(SocketMessage { direction: Direction::Sent, text, at });
|
||||||
@@ -335,14 +333,8 @@ pub fn graphql(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
fields: vec![("OPEN".into(), "".into()), ("SHIPPED".into(), "".into())],
|
fields: vec![("OPEN".into(), "".into()), ("SHIPPED".into(), "".into())],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
let mut explorer = GraphQLExplorer::new(
|
let mut explorer = GraphQLExplorer::new("devtools-graphql", types, &query, &variables, &search)
|
||||||
"devtools-graphql",
|
.on_run(move |_, _, _, cx| change(&ran, cx, |graph| graph.answer = Some(ANSWER.into())));
|
||||||
types,
|
|
||||||
&query,
|
|
||||||
&variables,
|
|
||||||
&search,
|
|
||||||
move |_, _, _, cx| change(&ran, cx, |graph| graph.answer = Some(ANSWER.into())),
|
|
||||||
);
|
|
||||||
if let Some(answer) = answer {
|
if let Some(answer) = answer {
|
||||||
explorer = explorer.answer(answer);
|
explorer = explorer.answer(answer);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -270,12 +270,15 @@ pub fn structure(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
|
|||||||
.flex_wrap()
|
.flex_wrap()
|
||||||
.gap_6()
|
.gap_6()
|
||||||
.child(
|
.child(
|
||||||
div().w(px(660.)).child(TableStructureEditor::new("devtools-structure", fields, move |next, _, cx| change(&edited, cx, |build| build.fields = next)),
|
div().w(px(660.)).child(TableStructureEditor::new("devtools-structure", fields)
|
||||||
|
.on_change(move |next, _, cx| change(&edited, cx, |build| build.fields = next)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().w(px(300.)).child(
|
div().w(px(300.)).child(
|
||||||
IndexManager::new("devtools-indexes", indexes, names, move |index, _, cx| change(&created, cx, |build| build.indexes.push(index)), move |name, _, cx| change(&dropped, cx, |build| build.indexes.retain(|index| index.name != *name))),
|
IndexManager::new("devtools-indexes", indexes, names)
|
||||||
|
.on_drop(move |name, _, cx| change(&dropped, cx, |build| build.indexes.retain(|index| index.name != *name)))
|
||||||
|
.on_create(move |index, _, cx| change(&created, cx, |build| build.indexes.push(index))),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ pub fn text(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
.child(div().w(px(380.)).child(Encoder::new("devtools-encoder", "Hello, 世界")))
|
.child(div().w(px(380.)).child(Encoder::new("devtools-encoder", "Hello, 世界")))
|
||||||
.child(
|
.child(
|
||||||
div().w(px(360.)).child(
|
div().w(px(360.)).child(
|
||||||
ColorContrastChecker::new("devtools-contrast", fore, back, move |fore, back, _, cx| change(&colors, cx, |colors| *colors = (fore, back))),
|
ColorContrastChecker::new("devtools-contrast", fore, back)
|
||||||
|
.on_change(move |fore, back, _, cx| change(&colors, cx, |colors| *colors = (fore, back))),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -202,10 +202,7 @@ pub fn editing(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
"Blocks like a notebook's. Hover one for its handle: drag it to move the block, press it to turn, copy or delete it. / on an empty line lists every kind, and an empty line says so. Undo covers text and blocks alike.",
|
"Blocks like a notebook's. Hover one for its handle: drag it to move the block, press it to turn, copy or delete it. / on an empty line lists every kind, and an empty line says so. Undo covers text and blocks alike.",
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.child(probe(
|
.child(probe("doc-blocks", div().w(px(760.)).child(notes))),
|
||||||
"doc-blocks",
|
|
||||||
div().w(px(760.)).h(px(360.)).child(notes),
|
|
||||||
)),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
section(
|
section(
|
||||||
@@ -213,7 +210,7 @@ pub fn editing(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
"Code colored as code, math set as it is typed, diagrams the app draws from their source, dividers, tables that grow and merge cells, pictures to resize and align, pages to open, columns, and synced copies that change together.",
|
"Code colored as code, math set as it is typed, diagrams the app draws from their source, dividers, tables that grow and merge cells, pictures to resize and align, pages to open, columns, and synced copies that change together.",
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.child(div().w(px(760.)).h(px(560.)).child(kinds)),
|
.child(div().w(px(760.)).child(kinds)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
section(
|
section(
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ pub fn merges(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().w(px(840.)).h(px(320.)).child(
|
div().w(px(840.)).child(
|
||||||
ThreeWayMerge::new("git-merge", ["main", "base", "feature/lift"], Rc::new(regions(BASE, OURS, THEIRS)))
|
ThreeWayMerge::new("git-merge", ["main", "base", "feature/lift"], Rc::new(regions(BASE, OURS, THEIRS)))
|
||||||
.takes(now_takes)
|
.takes(now_takes)
|
||||||
.on_take(move |conflict, side, _, cx| {
|
.on_take(move |conflict, side, _, cx| {
|
||||||
|
|||||||
@@ -37,14 +37,7 @@ pub fn blame(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
];
|
];
|
||||||
let history_rows: Vec<_> = history()
|
let history_rows: Vec<_> = history()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.zip([
|
.zip([(12, 4), (3, 3), (9, 1), (18, 0), (2, 1), (40, 0)])
|
||||||
Some((12, 4)),
|
|
||||||
Some((3, 3)),
|
|
||||||
Some((9, 1)),
|
|
||||||
Some((18, 0)),
|
|
||||||
None,
|
|
||||||
Some((40, 0)),
|
|
||||||
])
|
|
||||||
.collect();
|
.collect();
|
||||||
let (pick, select) = (said.clone(), said.clone());
|
let (pick, select) = (said.clone(), said.clone());
|
||||||
section(
|
section(
|
||||||
@@ -53,7 +46,7 @@ pub fn blame(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().w(px(840.)).h(px(240.)).child(
|
div().w(px(840.)).child(
|
||||||
BlameView::new("git-blame", CODE, [0, 1, 1, 1, 0, 0, 2, 2, 0, 0], blames)
|
BlameView::new("git-blame", CODE, [0, 1, 1, 1, 0, 0, 2, 2, 0, 0], blames)
|
||||||
.current(2)
|
.current(2)
|
||||||
.on_commit(move |commit, _, cx| set(&pick, Some(format!("Picked {}.", &commit[..7]).into()), cx)),
|
.on_commit(move |commit, _, cx| set(&pick, Some(format!("Picked {}.", &commit[..7]).into()), cx)),
|
||||||
@@ -61,8 +54,8 @@ pub fn blame(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
)
|
)
|
||||||
.children(told.map(Caption::new))
|
.children(told.map(Caption::new))
|
||||||
.child(
|
.child(
|
||||||
div().w(px(520.)).h(px(360.)).child(
|
div().w(px(520.)).child(
|
||||||
FileHistory::new("git-file-history", std::rc::Rc::new(history_rows))
|
FileHistory::new("git-file-history", history_rows)
|
||||||
.selected(history()[1].id.clone())
|
.selected(history()[1].id.clone())
|
||||||
.on_pick(move |commit, _, cx| set(&select, Some(format!("Opened {}.", &commit[..7]).into()), cx)),
|
.on_pick(move |commit, _, cx| set(&select, Some(format!("Opened {}.", &commit[..7]).into()), cx)),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ use ely_gpui_component::{
|
|||||||
forms::TextInput,
|
forms::TextInput,
|
||||||
git::{
|
git::{
|
||||||
Branch, BranchList, BranchSelector, ChangeAction, Changed, ChangesList, Commit,
|
Branch, BranchList, BranchSelector, ChangeAction, Changed, ChangesList, Commit,
|
||||||
CommitInput, CommitList, GitTag, GraphRow, Stash, StashAction, StashList, TagList, lanes,
|
CommitInput, CommitList, GitTag, Stash, StashAction, StashList, TagList,
|
||||||
},
|
},
|
||||||
lists::GitStatus,
|
lists::GitStatus,
|
||||||
typography::Caption,
|
typography::Caption,
|
||||||
};
|
};
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use gpui::{App, Entity, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
use gpui::{App, Entity, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -21,7 +19,8 @@ fn changed(path: &str, status: GitStatus, added: usize, removed: usize) -> Chang
|
|||||||
Changed {
|
Changed {
|
||||||
path: path.to_string().into(),
|
path: path.to_string().into(),
|
||||||
status,
|
status,
|
||||||
lines: Some((added, removed)),
|
added,
|
||||||
|
removed,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +86,7 @@ pub fn changes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
.child(
|
.child(
|
||||||
div().w(px(420.)).child(
|
div().w(px(420.)).child(
|
||||||
ChangesList::new("git-changes", now_staged, now_unstaged)
|
ChangesList::new("git-changes", now_staged, now_unstaged)
|
||||||
.on_action(move |path, _, action, _, cx| {
|
.on_action(move |path, action, _, cx| {
|
||||||
let (from, to) = match action {
|
let (from, to) = match action {
|
||||||
ChangeAction::Stage => (&act_unstaged, &act_staged),
|
ChangeAction::Stage => (&act_unstaged, &act_staged),
|
||||||
ChangeAction::Unstage => (&act_staged, &act_unstaged),
|
ChangeAction::Unstage => (&act_staged, &act_unstaged),
|
||||||
@@ -212,21 +211,6 @@ pub(super) fn history() -> Vec<Commit> {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The sample history's lanes.
|
|
||||||
fn graph() -> Vec<GraphRow> {
|
|
||||||
let commits = history();
|
|
||||||
let parents: Vec<Vec<&str>> = commits
|
|
||||||
.iter()
|
|
||||||
.map(|commit| commit.parents.iter().map(|id| id.as_ref()).collect())
|
|
||||||
.collect();
|
|
||||||
lanes(
|
|
||||||
commits
|
|
||||||
.iter()
|
|
||||||
.zip(&parents)
|
|
||||||
.map(|(commit, parents)| (commit.id.as_ref(), parents.as_slice())),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn commits(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
pub fn commits(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||||
let picked = keep("git-picked", || history()[0].id.clone(), window, cx);
|
let picked = keep("git-picked", || history()[0].id.clone(), window, cx);
|
||||||
let now = picked.read(cx).clone();
|
let now = picked.read(cx).clone();
|
||||||
@@ -237,9 +221,9 @@ pub fn commits(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().w(px(840.)).h(px(320.)).child(
|
div().w(px(840.)).child(
|
||||||
CommitList::new("git-graph", Rc::new(history()))
|
CommitList::new("git-graph", history())
|
||||||
.graph(Rc::new(graph()))
|
.graph()
|
||||||
.selected(now)
|
.selected(now)
|
||||||
.on_pick(move |id, _, cx| set(&pick, id.clone(), cx)),
|
.on_pick(move |id, _, cx| set(&pick, id.clone(), cx)),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -246,15 +246,12 @@ pub fn focus(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
("strike", IconName::Strikethrough),
|
("strike", IconName::Strikethrough),
|
||||||
];
|
];
|
||||||
let toolbar = tools.into_iter().fold(
|
let toolbar = tools.into_iter().fold(
|
||||||
RovingFocus::new("interaction-tools", Axis::Horizontal, move |key, _, cx| {
|
RovingFocus::new("interaction-tools", Axis::Horizontal)
|
||||||
let key = key.clone();
|
.gap_0p5()
|
||||||
change(&press, cx, |focusing| focusing.tool = key)
|
.p_0p5()
|
||||||
})
|
.rounded_lg()
|
||||||
.gap_0p5()
|
.border_1()
|
||||||
.p_0p5()
|
.border_color(theme.colors.border),
|
||||||
.rounded_lg()
|
|
||||||
.border_1()
|
|
||||||
.border_color(theme.colors.border),
|
|
||||||
|bar, (key, icon)| {
|
|bar, (key, icon)| {
|
||||||
let on = tool.as_ref() == key;
|
let on = tool.as_ref() == key;
|
||||||
bar.item(
|
bar.item(
|
||||||
@@ -311,7 +308,10 @@ pub fn focus(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
"RovingFocus",
|
"RovingFocus",
|
||||||
probe(
|
probe(
|
||||||
"interaction-tools",
|
"interaction-tools",
|
||||||
toolbar,
|
toolbar.on_press(move |key, _, cx| {
|
||||||
|
let key = key.clone();
|
||||||
|
change(&press, cx, |focusing| focusing.tool = key)
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
cx,
|
cx,
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -162,18 +162,15 @@ pub fn shapes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let [resize, turn, pick, sort, double, press, long] = [(); 7].map(|_| state.clone());
|
let [resize, turn, pick, sort, double, press, long] = [(); 7].map(|_| state.clone());
|
||||||
let tiles = (0..12).fold(
|
let tiles = (0..12).fold(
|
||||||
SelectionArea::new("interaction-tiles", move |keys, _, cx| {
|
SelectionArea::new("interaction-tiles")
|
||||||
let keys = keys.to_vec();
|
.w(px(300.))
|
||||||
change(&pick, cx, |shapes| shapes.picked = keys)
|
.flex()
|
||||||
})
|
.flex_wrap()
|
||||||
.w(px(300.))
|
.gap(px(8.))
|
||||||
.flex()
|
.p_3()
|
||||||
.flex_wrap()
|
.rounded_lg()
|
||||||
.gap(px(8.))
|
.border_1()
|
||||||
.p_3()
|
.border_color(theme.colors.border),
|
||||||
.rounded_lg()
|
|
||||||
.border_1()
|
|
||||||
.border_color(theme.colors.border),
|
|
||||||
|area, ix| {
|
|area, ix| {
|
||||||
area.item(
|
area.item(
|
||||||
SharedString::from(format!("tile-{ix}")),
|
SharedString::from(format!("tile-{ix}")),
|
||||||
@@ -207,7 +204,8 @@ pub fn shapes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
"Resizable",
|
"Resizable",
|
||||||
probe(
|
probe(
|
||||||
"interaction-resizable",
|
"interaction-resizable",
|
||||||
Resizable::new("interaction-resizable", size_now, size(px(160.), px(96.)), size(px(360.), px(220.)), move |next, _, cx| change(&resize, cx, |shapes| shapes.size = next))
|
Resizable::new("interaction-resizable", size_now, size(px(160.), px(96.)), size(px(360.), px(220.)))
|
||||||
|
.on_resize(move |next, _, cx| change(&resize, cx, |shapes| shapes.size = next))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.size_full()
|
.size_full()
|
||||||
@@ -227,7 +225,8 @@ pub fn shapes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
"Rotatable",
|
"Rotatable",
|
||||||
probe(
|
probe(
|
||||||
"interaction-rotatable",
|
"interaction-rotatable",
|
||||||
Rotatable::new("interaction-rotatable", "icons/arrow-up.svg", px(120.), angle, move |next, _, cx| change(&turn, cx, |shapes| shapes.angle = next)),
|
Rotatable::new("interaction-rotatable", "icons/arrow-up.svg", px(120.), angle)
|
||||||
|
.on_turn(move |next, _, cx| change(&turn, cx, |shapes| shapes.angle = next)),
|
||||||
),
|
),
|
||||||
cx,
|
cx,
|
||||||
)),
|
)),
|
||||||
@@ -238,7 +237,10 @@ pub fn shapes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
"SelectionArea",
|
"SelectionArea",
|
||||||
probe(
|
probe(
|
||||||
"interaction-tiles",
|
"interaction-tiles",
|
||||||
tiles.selected(picked),
|
tiles.selected(picked).on_change(move |keys, _, cx| {
|
||||||
|
let keys = keys.to_vec();
|
||||||
|
change(&pick, cx, |shapes| shapes.picked = keys)
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
cx,
|
cx,
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -211,24 +211,6 @@ pub fn hosts(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
pub fn context(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
pub fn context(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||||
let (last, caption) = ran("context-ran", window, cx);
|
let (last, caption) = ran("context-ran", window, cx);
|
||||||
let menu = Menu::new()
|
let menu = Menu::new()
|
||||||
.buttons([
|
|
||||||
MenuItem::new("Cut")
|
|
||||||
.icon(IconName::Scissors)
|
|
||||||
.keys("secondary-x")
|
|
||||||
.on_click(run(&last, "cut")),
|
|
||||||
MenuItem::new("Copy")
|
|
||||||
.icon(IconName::Copy)
|
|
||||||
.keys("secondary-c")
|
|
||||||
.on_click(run(&last, "copy")),
|
|
||||||
MenuItem::new("Paste")
|
|
||||||
.icon(IconName::Clipboard)
|
|
||||||
.keys("secondary-v")
|
|
||||||
.on_click(run(&last, "paste")),
|
|
||||||
MenuItem::new("Delete")
|
|
||||||
.icon(IconName::Trash2)
|
|
||||||
.disabled(true),
|
|
||||||
])
|
|
||||||
.separator()
|
|
||||||
.item(
|
.item(
|
||||||
MenuItem::new("Back")
|
MenuItem::new("Back")
|
||||||
.icon(IconName::ArrowLeft)
|
.icon(IconName::ArrowLeft)
|
||||||
@@ -260,7 +242,7 @@ pub fn context(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
section(
|
section(
|
||||||
"ContextMenu",
|
"ContextMenu",
|
||||||
"A right click opens the menu where it lands; it stays inside the window. A row of icon buttons heads it; arrows cross the row.",
|
"A right click opens the menu where it lands; it stays inside the window.",
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.child(specimen(
|
.child(specimen(
|
||||||
|
|||||||
@@ -20,19 +20,19 @@ pub fn render(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
"poll",
|
"poll",
|
||||||
"Which chart should come next?",
|
"Which chart should come next?",
|
||||||
["Sankey", "Sunburst", "Candlestick"],
|
["Sankey", "Sunburst", "Candlestick"],
|
||||||
move |vote, _, cx| {
|
|
||||||
tally.update(cx, |tally, cx| {
|
|
||||||
match (vote, tally.voted) {
|
|
||||||
(Some(ix), _) => tally.votes[ix] += 1,
|
|
||||||
(None, Some(ix)) => tally.votes[ix] -= 1,
|
|
||||||
(None, None) => panic!("gallery poll: no vote to take back"),
|
|
||||||
}
|
|
||||||
tally.voted = vote;
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
.votes(votes);
|
.votes(votes)
|
||||||
|
.on_vote(move |vote, _, cx| {
|
||||||
|
tally.update(cx, |tally, cx| {
|
||||||
|
match (vote, tally.voted) {
|
||||||
|
(Some(ix), _) => tally.votes[ix] += 1,
|
||||||
|
(None, Some(ix)) => tally.votes[ix] -= 1,
|
||||||
|
(None, None) => panic!("gallery poll: no vote to take back"),
|
||||||
|
}
|
||||||
|
tally.voted = vote;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
});
|
||||||
let poll = match voted {
|
let poll = match voted {
|
||||||
Some(ix) => poll.voted(ix),
|
Some(ix) => poll.voted(ix),
|
||||||
None => poll,
|
None => poll,
|
||||||
@@ -62,7 +62,9 @@ pub fn render(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
vec!["Forms".into(), "Charts".into(), "Tables".into()],
|
vec!["Forms".into(), "Charts".into(), "Tables".into()],
|
||||||
),
|
),
|
||||||
Question::Text("What should we build next?".into()),
|
Question::Text("What should we build next?".into()),
|
||||||
], |answers, _, _| log::info!("gallery: survey {answers:?}")),
|
],
|
||||||
|
)
|
||||||
|
.on_submit(|answers, _, _| log::info!("gallery: survey {answers:?}")),
|
||||||
),
|
),
|
||||||
cx,
|
cx,
|
||||||
)),
|
)),
|
||||||
|
|||||||
@@ -53,8 +53,9 @@ pub fn render(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().w(px(280.0)).child(
|
div().w(px(280.0)).child(
|
||||||
QrCodeScanner::new("scanner", 1.5, |text, _, _| log::info!("gallery: scanned {text}"))
|
QrCodeScanner::new("scanner", 1.5)
|
||||||
.frame(frame.read(cx).clone()),
|
.frame(frame.read(cx).clone())
|
||||||
|
.on_scan(|text, _, _| log::info!("gallery: scanned {text}")),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -67,7 +68,9 @@ pub fn render(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
.child(probe(
|
.child(probe(
|
||||||
"captcha",
|
"captcha",
|
||||||
div().w(px(280.0)).child(
|
div().w(px(280.0)).child(
|
||||||
Captcha::new("captcha", picture, 3.0, move |text, _, cx| {
|
Captcha::new("captcha", picture, 3.0)
|
||||||
|
.state(state)
|
||||||
|
.on_answer(move |text, _, cx| {
|
||||||
checked.update(cx, |desk, cx| {
|
checked.update(cx, |desk, cx| {
|
||||||
desk.1 = match text.eq_ignore_ascii_case(answer) {
|
desk.1 = match text.eq_ignore_ascii_case(answer) {
|
||||||
true => CaptchaState::Passed,
|
true => CaptchaState::Passed,
|
||||||
@@ -76,7 +79,6 @@ pub fn render(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.state(state)
|
|
||||||
.on_refresh(move |_, cx| {
|
.on_refresh(move |_, cx| {
|
||||||
refreshed.update(cx, |desk, cx| {
|
refreshed.update(cx, |desk, cx| {
|
||||||
*desk = ((desk.0 + 1) % ANSWERS.len(), CaptchaState::Asking);
|
*desk = ((desk.0 + 1) % ANSWERS.len(), CaptchaState::Asking);
|
||||||
|
|||||||
@@ -16,8 +16,12 @@ fn packages() -> [Package; 4] {
|
|||||||
Package::new(
|
Package::new(
|
||||||
"Ely GPUI Component",
|
"Ely GPUI Component",
|
||||||
"0.1.0",
|
"0.1.0",
|
||||||
"MIT",
|
"MIT OR Apache-2.0",
|
||||||
include_str!("../../../../LICENSE"),
|
concat!(
|
||||||
|
include_str!("../../../../LICENSE-MIT"),
|
||||||
|
"\n\n",
|
||||||
|
include_str!("../../../../LICENSE-APACHE")
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Package::new(
|
Package::new(
|
||||||
"Lucide",
|
"Lucide",
|
||||||
@@ -110,7 +114,11 @@ pub fn render(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
div().w(px(360.0)).child(
|
div().w(px(360.0)).child(
|
||||||
CookieBanner::new(
|
CookieBanner::new(
|
||||||
"cookies",
|
"cookies",
|
||||||
"The help pages set cookies. Necessary ones keep them working; the rest are up to you.", move |kinds, _, cx| {
|
"The help pages set cookies. Necessary ones keep them working; the rest are up to you.",
|
||||||
|
)
|
||||||
|
.kind("Analytics", "Counts which pages help, without names.")
|
||||||
|
.kind("Video", "Lets embedded tutorials play and remember where you stopped.")
|
||||||
|
.on_choose(move |kinds, _, cx| {
|
||||||
let words = match kinds.len() {
|
let words = match kinds.len() {
|
||||||
0 => "Only necessary cookies.".to_string(),
|
0 => "Only necessary cookies.".to_string(),
|
||||||
_ => format!("Cookies on: {}.", kinds.join(", ")),
|
_ => format!("Cookies on: {}.", kinds.join(", ")),
|
||||||
@@ -119,9 +127,7 @@ pub fn render(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
*said = words.into();
|
*said = words.into();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
})
|
}),
|
||||||
.kind("Analytics", "Counts which pages help, without names.")
|
|
||||||
.kind("Video", "Lets embedded tutorials play and remember where you stopped."),
|
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
.child(
|
.child(
|
||||||
|
|||||||
@@ -58,16 +58,14 @@ pub fn pagination(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
|
|||||||
let now = *page.read(cx);
|
let now = *page.read(cx);
|
||||||
section(
|
section(
|
||||||
"Pagination",
|
"Pagination",
|
||||||
"The first, the last, and the pages around this one. A gap stands for two or more pages. Type a page to go there.",
|
"The first, the last, and the pages around this one. A gap stands for two or more pages.",
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.child(specimen(
|
.child(specimen(
|
||||||
format!("page {now} of 20"),
|
format!("page {now} of 20"),
|
||||||
probe(
|
probe(
|
||||||
"pagination",
|
"pagination",
|
||||||
Pagination::new("pagination", now, 20)
|
Pagination::new("pagination", now, 20).on_change(move |to, _, cx| set(&page, to, cx)),
|
||||||
.jump()
|
|
||||||
.on_change(move |to, _, cx| set(&page, to, cx)),
|
|
||||||
),
|
),
|
||||||
cx,
|
cx,
|
||||||
))
|
))
|
||||||
@@ -108,14 +106,10 @@ pub fn steps(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
probe(
|
probe(
|
||||||
"wizard",
|
"wizard",
|
||||||
div().w(px(560.0)).child(
|
div().w(px(560.0)).child(
|
||||||
Wizard::new(
|
Wizard::new("wizard", setup_steps(), now)
|
||||||
"wizard",
|
.content(div().h(px(72.0)).child(Paragraph::new(body)))
|
||||||
setup_steps(),
|
.on_step(move |to, _, cx| set(&walk, to, cx))
|
||||||
now,
|
.on_finish(move |_, cx| set(&finish, true, cx)),
|
||||||
move |to, _, cx| set(&walk, to, cx),
|
|
||||||
move |_, cx| set(&finish, true, cx),
|
|
||||||
)
|
|
||||||
.content(div().h(px(72.0)).child(Paragraph::new(body))),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
cx,
|
cx,
|
||||||
|
|||||||
@@ -88,17 +88,13 @@ pub fn wizard(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
false => OnboardingWizard::new(
|
false => OnboardingWizard::new("onboarding-wizard", steps, step)
|
||||||
"onboarding-wizard",
|
.content(content)
|
||||||
steps,
|
.ready(ready)
|
||||||
step,
|
.on_step(move |to, _, cx| change(&walk, cx, |welcome| welcome.step = to))
|
||||||
move |to, _, cx| change(&walk, cx, |welcome| welcome.step = to),
|
.on_finish(move |_, cx| change(&finish, cx, |welcome| welcome.finished = true))
|
||||||
move |_, cx| change(&finish, cx, |welcome| welcome.finished = true),
|
.on_skip(move |_, cx| change(&skip, cx, |welcome| welcome.finished = true))
|
||||||
)
|
.into_any_element(),
|
||||||
.content(content)
|
|
||||||
.ready(ready)
|
|
||||||
.on_skip(move |_, cx| change(&skip, cx, |welcome| welcome.finished = true))
|
|
||||||
.into_any_element(),
|
|
||||||
};
|
};
|
||||||
section(
|
section(
|
||||||
"OnboardingWizard · WelcomeTour",
|
"OnboardingWizard · WelcomeTour",
|
||||||
@@ -126,15 +122,17 @@ pub fn highlights(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
|
|||||||
.relative()
|
.relative()
|
||||||
.child(tool("onboarding-pin", IconName::Pin))
|
.child(tool("onboarding-pin", IconName::Pin))
|
||||||
.when(!spot_seen, |pin| {
|
.when(!spot_seen, |pin| {
|
||||||
pin.child(div().absolute().top_neg_1p5().right_neg_1p5().child(probe(
|
pin.child(
|
||||||
"onboarding-hotspot",
|
div().absolute().top_neg_1p5().right_neg_1p5().child(probe(
|
||||||
Hotspot::new(
|
|
||||||
"onboarding-hotspot",
|
"onboarding-hotspot",
|
||||||
"Pin a view",
|
Hotspot::new(
|
||||||
"Keep the views you use most one press away.",
|
"onboarding-hotspot",
|
||||||
move |_, cx| change(&spot, cx, |seen| seen.hotspot = true),
|
"Pin a view",
|
||||||
),
|
"Keep the views you use most one press away.",
|
||||||
)))
|
)
|
||||||
|
.on_dismiss(move |_, cx| change(&spot, cx, |seen| seen.hotspot = true)),
|
||||||
|
)),
|
||||||
|
)
|
||||||
});
|
});
|
||||||
let toolbar = div()
|
let toolbar = div()
|
||||||
.flex()
|
.flex()
|
||||||
@@ -153,9 +151,9 @@ pub fn highlights(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
|
|||||||
IconName::Columns2,
|
IconName::Columns2,
|
||||||
"Split view",
|
"Split view",
|
||||||
"Work on two files side by side, each with its own history.",
|
"Work on two files side by side, each with its own history.",
|
||||||
|_, _| log::info!("gallery: split view tried"),
|
|
||||||
move |_, cx| change(&card, cx, |seen| seen.highlight = true),
|
|
||||||
)
|
)
|
||||||
|
.on_try(|_, _| log::info!("gallery: split view tried"))
|
||||||
|
.on_dismiss(move |_, cx| change(&card, cx, |seen| seen.highlight = true))
|
||||||
});
|
});
|
||||||
let menu = List::new()
|
let menu = List::new()
|
||||||
.child(
|
.child(
|
||||||
@@ -249,20 +247,16 @@ pub fn checklist(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
false => SetupChecklist::new(
|
false => SetupChecklist::new("onboarding-setup", "Get started", tasks)
|
||||||
"onboarding-setup",
|
.on_start(move |key, _, cx| {
|
||||||
"Get started",
|
|
||||||
tasks,
|
|
||||||
move |key, _, cx| {
|
|
||||||
let at = TASKS
|
let at = TASKS
|
||||||
.iter()
|
.iter()
|
||||||
.position(|(each, _, _)| *each == key.as_ref())
|
.position(|(each, _, _)| *each == key.as_ref())
|
||||||
.expect("a setup task");
|
.expect("a setup task");
|
||||||
change(&start, cx, |setup| setup.done[at] = true)
|
change(&start, cx, |setup| setup.done[at] = true)
|
||||||
},
|
})
|
||||||
)
|
.on_dismiss(move |_, cx| change(&hide, cx, |setup| setup.hidden = true))
|
||||||
.on_dismiss(move |_, cx| change(&hide, cx, |setup| setup.hidden = true))
|
.into_any_element(),
|
||||||
.into_any_element(),
|
|
||||||
};
|
};
|
||||||
section(
|
section(
|
||||||
"Checklist (first-run tasks)",
|
"Checklist (first-run tasks)",
|
||||||
|
|||||||
@@ -176,21 +176,16 @@ pub fn panel(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
.border_color(theme.colors.border)
|
.border_color(theme.colors.border)
|
||||||
.bg(theme.colors.surface)
|
.bg(theme.colors.surface)
|
||||||
.child(
|
.child(
|
||||||
HelpPanel::new(
|
HelpPanel::new("onboarding-help-panel", articles(), &search, open)
|
||||||
"onboarding-help-panel",
|
.on_open(move |key, _, cx| {
|
||||||
articles(),
|
|
||||||
&search,
|
|
||||||
open,
|
|
||||||
move |key, _, cx| {
|
|
||||||
let key = key.cloned();
|
let key = key.cloned();
|
||||||
change(&opener, cx, |help| help.open = key)
|
change(&opener, cx, |help| help.open = key)
|
||||||
},
|
})
|
||||||
)
|
.on_contact(|_, _| log::info!("gallery: contact support"))
|
||||||
.on_contact(|_, _| log::info!("gallery: contact support"))
|
.on_shortcuts(move |_, cx| {
|
||||||
.on_shortcuts(move |_, cx| {
|
change(&keys, cx, |help| help.dialog = Some(Dialog::Shortcuts))
|
||||||
change(&keys, cx, |help| help.dialog = Some(Dialog::Shortcuts))
|
})
|
||||||
})
|
.on_close(move |_, cx| change(&close, cx, |help| help.hidden = true)),
|
||||||
.on_close(move |_, cx| change(&close, cx, |help| help.hidden = true)),
|
|
||||||
)
|
)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
};
|
};
|
||||||
@@ -295,7 +290,8 @@ pub fn feedback(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
.gap_6()
|
.gap_6()
|
||||||
.child(probe(
|
.child(probe(
|
||||||
"onboarding-feedback",
|
"onboarding-feedback",
|
||||||
FeedbackWidget::new("onboarding-feedback", |sentiment, note, _, _| log::info!("gallery: feedback {} {note}", sentiment.key())),
|
FeedbackWidget::new("onboarding-feedback")
|
||||||
|
.on_send(|sentiment, note, _, _| log::info!("gallery: feedback {} {note}", sentiment.key())),
|
||||||
))
|
))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -304,11 +300,12 @@ pub fn feedback(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
.child(
|
.child(
|
||||||
ContactSupport::new("onboarding-support", topics, move |topic, _, _, cx| {
|
ContactSupport::new("onboarding-support", topics)
|
||||||
|
.address("help@example.com")
|
||||||
|
.on_send(move |topic, _, _, cx| {
|
||||||
log::info!("gallery: support about {topic}");
|
log::info!("gallery: support about {topic}");
|
||||||
change(&done, cx, |help| help.sent = true)
|
change(&done, cx, |help| help.sent = true)
|
||||||
})
|
}),
|
||||||
.address("help@example.com"),
|
|
||||||
)
|
)
|
||||||
.when(sent, |column| column.child(div().text_sm().child("Sent. We answer within a day."))),
|
.when(sent, |column| column.child(div().text_sm().child("Sent. We answer within a day."))),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -38,36 +38,36 @@ fn appearance(prefs: &Entity<Prefs>, fields: &Fields, cx: &App) -> AnyElement {
|
|||||||
let [theme, accent, size, density, font] = [(); 5].map(|_| prefs.clone());
|
let [theme, accent, size, density, font] = [(); 5].map(|_| prefs.clone());
|
||||||
SettingsSection::new("Appearance")
|
SettingsSection::new("Appearance")
|
||||||
.description("How the app looks.")
|
.description("How the app looks.")
|
||||||
.row(SettingsRow::new("Theme").control(ThemeSelector::new(
|
.row(
|
||||||
"settings-theme",
|
SettingsRow::new("Theme").control(
|
||||||
now.appearance,
|
ThemeSelector::new("settings-theme", now.appearance).on_change(
|
||||||
move |next, _, cx| change(&theme, cx, |prefs| prefs.appearance = next),
|
move |next, _, cx| change(&theme, cx, |prefs| prefs.appearance = next),
|
||||||
)))
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
.row(
|
.row(
|
||||||
SettingsRow::new("Accent color")
|
SettingsRow::new("Accent color")
|
||||||
.description("Buttons, links and what is chosen.")
|
.description("Buttons, links and what is chosen.")
|
||||||
.control(AccentColorPicker::new(
|
.control(
|
||||||
"settings-accent",
|
AccentColorPicker::new("settings-accent", now.accent, presets).on_change(
|
||||||
now.accent,
|
move |next, _, cx| change(&accent, cx, |prefs| prefs.accent = next),
|
||||||
presets,
|
),
|
||||||
move |next, _, cx| change(&accent, cx, |prefs| prefs.accent = next),
|
),
|
||||||
)),
|
)
|
||||||
|
.row(
|
||||||
|
SettingsRow::new("Font size").control(
|
||||||
|
FontSizeControl::new("settings-size", now.size, SIZE, (11.0, 20.0))
|
||||||
|
.on_change(move |next, _, cx| change(&size, cx, |prefs| prefs.size = next)),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.row(SettingsRow::new("Font size").control(FontSizeControl::new(
|
|
||||||
"settings-size",
|
|
||||||
now.size,
|
|
||||||
SIZE,
|
|
||||||
(11.0, 20.0),
|
|
||||||
move |next, _, cx| change(&size, cx, |prefs| prefs.size = next),
|
|
||||||
)))
|
|
||||||
.row(
|
.row(
|
||||||
SettingsRow::new("Density")
|
SettingsRow::new("Density")
|
||||||
.description("How close things sit.")
|
.description("How close things sit.")
|
||||||
.control(DensitySelector::new(
|
.control(
|
||||||
"settings-density",
|
DensitySelector::new("settings-density", now.density).on_change(
|
||||||
now.density,
|
move |next, _, cx| change(&density, cx, |prefs| prefs.density = next),
|
||||||
move |next, _, cx| change(&density, cx, |prefs| prefs.density = next),
|
),
|
||||||
)),
|
),
|
||||||
)
|
)
|
||||||
.row(
|
.row(
|
||||||
SettingsRow::new("Font")
|
SettingsRow::new("Font")
|
||||||
@@ -126,32 +126,25 @@ fn keyboard(prefs: &Entity<Prefs>, fields: &Fields, cx: &App) -> AnyElement {
|
|||||||
fn network(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
|
fn network(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
|
||||||
let proxy = prefs.clone();
|
let proxy = prefs.clone();
|
||||||
SettingsSection::new("Network")
|
SettingsSection::new("Network")
|
||||||
.row(ProxySettings::new(
|
.row(
|
||||||
"settings-proxy",
|
ProxySettings::new("settings-proxy", prefs.read(cx).proxy.clone())
|
||||||
prefs.read(cx).proxy.clone(),
|
.on_apply(move |next, _, cx| change(&proxy, cx, |prefs| prefs.proxy = next)),
|
||||||
move |next, _, cx| change(&proxy, cx, |prefs| prefs.proxy = next),
|
)
|
||||||
))
|
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn privacy(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
|
fn privacy(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
|
||||||
let privacy = prefs.clone();
|
let privacy = prefs.clone();
|
||||||
PrivacySettings::new(
|
PrivacySettings::new("settings-privacy", prefs.read(cx).privacy)
|
||||||
"settings-privacy",
|
.on_change(move |next, _, cx| change(&privacy, cx, |prefs| prefs.privacy = next))
|
||||||
prefs.read(cx).privacy,
|
.into_any_element()
|
||||||
move |next, _, cx| change(&privacy, cx, |prefs| prefs.privacy = next),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn notifications(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
|
fn notifications(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
|
||||||
let notices = prefs.clone();
|
let notices = prefs.clone();
|
||||||
NotificationSettings::new(
|
NotificationSettings::new("settings-notices", prefs.read(cx).notices.clone())
|
||||||
"settings-notices",
|
.on_change(move |next, _, cx| change(¬ices, cx, |prefs| prefs.notices = next))
|
||||||
prefs.read(cx).notices.clone(),
|
.into_any_element()
|
||||||
move |next, _, cx| change(¬ices, cx, |prefs| prefs.notices = next),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn startup(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
|
fn startup(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
|
||||||
@@ -161,13 +154,9 @@ fn startup(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
|
|||||||
Choice::new("home", "The home page"),
|
Choice::new("home", "The home page"),
|
||||||
Choice::new("blank", "A blank window"),
|
Choice::new("blank", "A blank window"),
|
||||||
];
|
];
|
||||||
StartupSettings::new(
|
StartupSettings::new("settings-startup", prefs.read(cx).startup.clone(), pages)
|
||||||
"settings-startup",
|
.on_change(move |next, _, cx| change(&startup, cx, |prefs| prefs.startup = next))
|
||||||
prefs.read(cx).startup.clone(),
|
.into_any_element()
|
||||||
pages,
|
|
||||||
move |next, _, cx| change(&startup, cx, |prefs| prefs.startup = next),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn storage(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
|
fn storage(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
|
||||||
@@ -186,36 +175,37 @@ fn storage(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
|
|||||||
.child(
|
.child(
|
||||||
SettingsSection::new("Storage")
|
SettingsSection::new("Storage")
|
||||||
.description("What the app keeps on this device.")
|
.description("What the app keeps on this device.")
|
||||||
.row(StorageSettings::new(
|
.row(
|
||||||
"settings-storage",
|
StorageSettings::new("settings-storage", now.stores.clone()).on_clear(
|
||||||
now.stores.clone(),
|
move |key, _, cx| {
|
||||||
move |key, _, cx| {
|
change(&cleared, cx, |prefs| {
|
||||||
change(&cleared, cx, |prefs| {
|
prefs
|
||||||
prefs
|
.stores
|
||||||
.stores
|
.iter_mut()
|
||||||
.iter_mut()
|
.filter(|store| store.key == *key)
|
||||||
.filter(|store| store.key == *key)
|
.for_each(|store| store.bytes = 0)
|
||||||
.for_each(|store| store.bytes = 0)
|
})
|
||||||
})
|
},
|
||||||
},
|
),
|
||||||
)),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
SettingsSection::new("Import and export").row(ImportExportSettings::new(
|
SettingsSection::new("Import and export").row(
|
||||||
"settings-files",
|
ImportExportSettings::new("settings-files")
|
||||||
|_, _| log::info!("gallery: settings exported"),
|
.on_export(|_, _| log::info!("gallery: settings exported"))
|
||||||
|path, _, _| log::info!("gallery: settings imported from {}", path.display()),
|
.on_import(|path, _, _| {
|
||||||
)),
|
log::info!("gallery: settings imported from {}", path.display())
|
||||||
|
}),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
SettingsSection::new("Defaults")
|
SettingsSection::new("Defaults")
|
||||||
.description("A larger font size shows here.")
|
.description("A larger font size shows here.")
|
||||||
.row(ResetToDefault::new(
|
.row(
|
||||||
"settings-reset",
|
ResetToDefault::new("settings-reset", changed)
|
||||||
changed,
|
.on_reset(move |_, _, cx| change(&reset, cx, |prefs| prefs.size = SIZE))
|
||||||
move |_, _, cx| change(&reset, cx, |prefs| prefs.size = SIZE),
|
.on_reset_all(move |_, cx| change(&all, cx, |prefs| prefs.size = SIZE)),
|
||||||
move |_, cx| change(&all, cx, |prefs| prefs.size = SIZE),
|
),
|
||||||
)),
|
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
@@ -230,25 +220,22 @@ fn advanced(prefs: &Entity<Prefs>, fields: &Fields, cx: &App) -> AnyElement {
|
|||||||
.child(
|
.child(
|
||||||
SettingsSection::new("Features")
|
SettingsSection::new("Features")
|
||||||
.description("AdvancedSettings / FeatureFlags")
|
.description("AdvancedSettings / FeatureFlags")
|
||||||
.row(FeatureFlags::new(
|
.row(
|
||||||
"settings-flags",
|
FeatureFlags::new("settings-flags", now.flags.clone(), &fields.flags)
|
||||||
now.flags.clone(),
|
.on_toggle(move |key, on, _, cx| {
|
||||||
&fields.flags,
|
change(&toggled, cx, |prefs| {
|
||||||
move |key, on, _, cx| {
|
prefs
|
||||||
change(&toggled, cx, |prefs| {
|
.flags
|
||||||
prefs
|
.iter_mut()
|
||||||
.flags
|
.filter(|flag| flag.key == *key)
|
||||||
.iter_mut()
|
.for_each(|flag| flag.on = on)
|
||||||
.filter(|flag| flag.key == *key)
|
})
|
||||||
.for_each(|flag| flag.on = on)
|
}),
|
||||||
})
|
),
|
||||||
},
|
)
|
||||||
)),
|
.child(
|
||||||
|
DeveloperModeToggle::new("settings-developer", now.developer)
|
||||||
|
.on_change(move |on, _, cx| change(&developer, cx, |prefs| prefs.developer = on)),
|
||||||
)
|
)
|
||||||
.child(DeveloperModeToggle::new(
|
|
||||||
"settings-developer",
|
|
||||||
now.developer,
|
|
||||||
move |on, _, cx| change(&developer, cx, |prefs| prefs.developer = on),
|
|
||||||
))
|
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
@@ -259,13 +259,14 @@ pub fn settings(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_6()
|
.gap_6()
|
||||||
.child(div().w(px(360.)).child(SettingsSearch::new("settings-search", entries, &search,
|
.child(div().w(px(360.)).child(SettingsSearch::new("settings-search", entries, &search).on_pick(
|
||||||
move |key, _, cx| change(&found, cx, |prefs| prefs.section = home(key).into()),
|
move |key, _, cx| change(&found, cx, |prefs| prefs.section = home(key).into()),
|
||||||
)))
|
)))
|
||||||
.child(probe(
|
.child(probe(
|
||||||
"settings-layout",
|
"settings-layout",
|
||||||
div().w(px(880.)).child(
|
div().w(px(880.)).child(
|
||||||
SettingsLayout::new("settings-layout", SECTIONS, selected, move |key, _, cx| change(&picked, cx, |prefs| prefs.section = key.clone()))
|
SettingsLayout::new("settings-layout", SECTIONS, selected)
|
||||||
|
.on_select(move |key, _, cx| change(&picked, cx, |prefs| prefs.section = key.clone()))
|
||||||
.page(page),
|
.page(page),
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ fn about() -> AboutDialog {
|
|||||||
.description("A component library for GPUI, in light and dark.")
|
.description("A component library for GPUI, in light and dark.")
|
||||||
.link(
|
.link(
|
||||||
"Website",
|
"Website",
|
||||||
"https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components",
|
"https://github.com/ZacharyZhang-NY/Ely-GPUI-Components",
|
||||||
)
|
)
|
||||||
.link(
|
.link(
|
||||||
"License",
|
"License",
|
||||||
"https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components#license",
|
"https://github.com/ZacharyZhang-NY/Ely-GPUI-Components#license",
|
||||||
)
|
)
|
||||||
.copyright("© 2026 Ely contributors")
|
.copyright("© 2026 Ely contributors")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ pub fn editor(_: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_4()
|
.gap_4()
|
||||||
.child(ThemeEditor::new("theme-editor", draft, |draft, _, cx| apply(draft, cx)))
|
.child(ThemeEditor::new("theme-editor", draft).on_change(|draft, _, cx| apply(draft, cx)))
|
||||||
.child(
|
.child(
|
||||||
div().flex().child(
|
div().flex().child(
|
||||||
Button::new("theme-restore", "Back to Ely's theme")
|
Button::new("theme-restore", "Back to Ely's theme")
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ pub fn importing(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
|
|||||||
specimens()
|
specimens()
|
||||||
.child(specimen(
|
.child(specimen(
|
||||||
"ThemeImporter",
|
"ThemeImporter",
|
||||||
div().w(px(360.)).child(ThemeImporter::new("theme-importer", |read, _, cx| {
|
div().w(px(360.)).child(ThemeImporter::new("theme-importer").on_apply(|read, _, cx| {
|
||||||
Theme::set_palette(read.mode, Some(read.colors.clone()), cx);
|
Theme::set_palette(read.mode, Some(read.colors.clone()), cx);
|
||||||
Theme::set_mode(read.mode, cx);
|
Theme::set_mode(read.mode, cx);
|
||||||
})),
|
})),
|
||||||
@@ -29,7 +29,7 @@ pub fn importing(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
|
|||||||
))
|
))
|
||||||
.child(specimen(
|
.child(specimen(
|
||||||
"SyntaxThemePicker",
|
"SyntaxThemePicker",
|
||||||
div().w(px(360.)).child(SyntaxThemePicker::new("theme-syntax", syntax_themes(), name,
|
div().w(px(360.)).child(SyntaxThemePicker::new("theme-syntax", syntax_themes(), name).on_change(
|
||||||
move |picked, _, cx| {
|
move |picked, _, cx| {
|
||||||
let theme = syntax_themes()
|
let theme = syntax_themes()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -211,9 +211,9 @@ pub fn link(cx: &App) -> impl IntoElement + use<> {
|
|||||||
.child(
|
.child(
|
||||||
row()
|
row()
|
||||||
.child("Read the")
|
.child("Read the")
|
||||||
.child(Link::new("guide", "style guide", |_, _, _| {
|
.child(
|
||||||
log::info!("gallery: link")
|
Link::new("guide", "style guide").on_click(|_, _, _| log::info!("gallery: link")),
|
||||||
}))
|
)
|
||||||
.child("before you start."),
|
.child("before you start."),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,27 +201,26 @@ pub fn emoji(cx: &App) -> impl IntoElement + use<> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn latex(cx: &App) -> impl IntoElement + use<> {
|
pub fn latex(cx: &App) -> impl IntoElement + use<> {
|
||||||
let math =
|
let math = |source: &str| Latex::parse(source).expect("gallery formula parses");
|
||||||
|id: &'static str, source: &str| Latex::parse(id, source).expect("gallery formula parses");
|
|
||||||
section(
|
section(
|
||||||
"Latex / MathInline",
|
"Latex / MathInline",
|
||||||
"TeX typeset by RaTeX, in the text's color.",
|
"A TeX subset: scripts, fractions, roots, Greek.",
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
specimens()
|
specimens()
|
||||||
.gap_10()
|
.gap_10()
|
||||||
.child(specimen("euler", math("euler", r"e^{i\pi} + 1 = 0"), cx))
|
.child(specimen("euler", math(r"e^{i\pi} + 1 = 0"), cx))
|
||||||
.child(specimen(
|
.child(specimen(
|
||||||
"sum",
|
"sum",
|
||||||
math("sum", r"\sum_{k=1}^{n} k = \frac{n(n+1)}{2}"),
|
math(r"\sum_{k=1}^{n} k = \frac{n(n+1)}{2}"),
|
||||||
cx,
|
cx,
|
||||||
))
|
))
|
||||||
.child(specimen(
|
.child(specimen(
|
||||||
"root",
|
"root",
|
||||||
math("root", r"x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}"),
|
math(r"x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}"),
|
||||||
cx,
|
cx,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.child(code("Latex::parse(\"euler\", r\"e^{i\\pi} + 1 = 0\")?", cx))
|
.child(code("Latex::parse(r\"e^{i\\pi} + 1 = 0\")?", cx))
|
||||||
}
|
}
|
||||||
@@ -106,10 +106,7 @@ pub fn start() {
|
|||||||
if let Some(choice) = EARLY.take() {
|
if let Some(choice) = EARLY.take() {
|
||||||
start.choice = choice;
|
start.choice = choice;
|
||||||
}
|
}
|
||||||
let window = match launch(start, cx) {
|
let window = launch(start, cx);
|
||||||
Ok(window) => window,
|
|
||||||
Err(error) => return log::error!("gallery: {error:#}"),
|
|
||||||
};
|
|
||||||
HOST.with(|host| *host.borrow_mut() = Some((cx.to_async(), window)));
|
HOST.with(|host| *host.borrow_mut() = Some((cx.to_async(), window)));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
// Writes the showcase catalog for the site: the pinned commit, or the checkout ELY_SHOWCASE names.
|
// Writes the showcase catalog for the site: the pinned commit, or the checkout ELY_SHOWCASE names.
|
||||||
import { execFileSync } from "node:child_process";
|
import { execFileSync } from "node:child_process";
|
||||||
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join, resolve } from "node:path";
|
import { join, resolve } from "node:path";
|
||||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||||
import { Marked } from "marked";
|
import { Marked } from "marked";
|
||||||
|
|
||||||
const REPO = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Showcases";
|
const REPO = "ZacharyZhang-NY/Ely-GPUI-Showcases";
|
||||||
const site = fileURLToPath(new URL("..", import.meta.url));
|
const site = fileURLToPath(new URL("..", import.meta.url));
|
||||||
const pub = join(site, "public", "showcase");
|
const pub = join(site, "public", "showcase");
|
||||||
const out = join(site, "src", "showcase.json");
|
const out = join(site, "src", "showcase.json");
|
||||||
@@ -15,14 +15,12 @@ const stamp = join(pub, ".commit");
|
|||||||
/** The catalog at `commit`, unpacked with its validator's packages. */
|
/** The catalog at `commit`, unpacked with its validator's packages. */
|
||||||
async function fetchCatalog(commit) {
|
async function fetchCatalog(commit) {
|
||||||
const work = mkdtempSync(join(tmpdir(), "ely-showcase-"));
|
const work = mkdtempSync(join(tmpdir(), "ely-showcase-"));
|
||||||
const url = `${REPO}/archive/${commit}.tar.gz`;
|
const url = `https://codeload.github.com/${REPO}/tar.gz/${commit}`;
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
if (!res.ok) throw new Error(`showcase: ${url} answered ${res.status}`);
|
if (!res.ok) throw new Error(`showcase: ${url} answered ${res.status}`);
|
||||||
writeFileSync(join(work, "catalog.tar.gz"), Buffer.from(await res.arrayBuffer()));
|
writeFileSync(join(work, "catalog.tar.gz"), Buffer.from(await res.arrayBuffer()));
|
||||||
execFileSync("tar", ["-xzf", "catalog.tar.gz"], { cwd: work });
|
execFileSync("tar", ["-xzf", "catalog.tar.gz"], { cwd: work });
|
||||||
const [top] = readdirSync(work, { withFileTypes: true }).filter((e) => e.isDirectory());
|
const root = join(work, `${REPO.split("/")[1]}-${commit}`);
|
||||||
if (!top) throw new Error(`showcase: ${url} held no folder`);
|
|
||||||
const root = join(work, top.name);
|
|
||||||
const pnpm = process.env.npm_execpath;
|
const pnpm = process.env.npm_execpath;
|
||||||
if (!pnpm) throw new Error("showcase: run through pnpm, which installs the catalog's validator");
|
if (!pnpm) throw new Error("showcase: run through pnpm, which installs the catalog's validator");
|
||||||
execFileSync(process.execPath, [pnpm, "install", "--frozen-lockfile", "--ignore-scripts", "--silent"], { cwd: root, stdio: "inherit" });
|
execFileSync(process.execPath, [pnpm, "install", "--frozen-lockfile", "--ignore-scripts", "--silent"], { cwd: root, stdio: "inherit" });
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
ca872b7708d200dafb3027364122aa5895e43299
|
0f8f5b9cc392d4741c239b1c1c0f6071dfc6faa5
|
||||||
@@ -36,8 +36,8 @@ function Header() {
|
|||||||
<button className="tool" aria-label={dark ? "Light mode" : "Dark mode"} onClick={() => setMode(dark ? "light" : "dark")}>
|
<button className="tool" aria-label={dark ? "Light mode" : "Dark mode"} onClick={() => setMode(dark ? "light" : "dark")}>
|
||||||
<Icon name={dark ? "Sun" : "Moon"} />
|
<Icon name={dark ? "Sun" : "Moon"} />
|
||||||
</button>
|
</button>
|
||||||
<a className="tool" href={REPO} rel="noopener" aria-label="Ely on Gitea">
|
<a className="tool" href={REPO} rel="noopener" aria-label="Ely on GitHub">
|
||||||
<Icon name="Gitea" />
|
<Icon name="GitHub" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -47,7 +47,7 @@ function Header() {
|
|||||||
function Footer() {
|
function Footer() {
|
||||||
return (
|
return (
|
||||||
<footer className="foot">
|
<footer className="foot">
|
||||||
<span>MIT</span>
|
<span>MIT or Apache-2.0</span>
|
||||||
<a href={REPO} rel="noopener">
|
<a href={REPO} rel="noopener">
|
||||||
Source
|
Source
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export interface Catalog {
|
|||||||
|
|
||||||
export type Sort = "newest" | "stars";
|
export type Sort = "newest" | "stars";
|
||||||
|
|
||||||
export const SUBMIT = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Showcases#submit-an-app";
|
export const SUBMIT = "https://github.com/ZacharyZhang-NY/Ely-GPUI-Showcases#submit-an-app";
|
||||||
|
|
||||||
/** Newest first, then most stars; or most stars, then newest. */
|
/** Newest first, then most stars; or most stars, then newest. */
|
||||||
export function sorted(apps: App[], sort: Sort): App[] {
|
export function sorted(apps: App[], sort: Sort): App[] {
|
||||||
|
|||||||
@@ -37,6 +37,6 @@ export function search(query: string): Chapter[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const REPO = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components";
|
export const REPO = "https://github.com/ZacharyZhang-NY/Ely-GPUI-Components";
|
||||||
|
|
||||||
export const pad = (n: number) => String(n).padStart(2, "0");
|
export const pad = (n: number) => String(n).padStart(2, "0");
|
||||||
@@ -4,7 +4,7 @@ Every Ely control that takes a press is a Tab stop. Focus moves with Tab and Shi
|
|||||||
|
|
||||||
## What `init` binds
|
## What `init` binds
|
||||||
|
|
||||||
`init` binds `tab` to the `FocusNext` action and `shift-tab` to `FocusPrev`, under `ElyFocus`, the key context every `FocusScope` sets. A `FocusScope` turns the two actions into focus moves. A context nested deeper keeps its own Tab, as a code editor that indents does, whichever library's `init` ran last.
|
`init` binds `tab` to the `FocusNext` action and `shift-tab` to `FocusPrev`, with no key context, so they reach every window. A `FocusScope` turns the two actions into focus moves.
|
||||||
|
|
||||||
## The root scope
|
## The root scope
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ An Ely app is a GPUI app with four more parts: Ely's assets, one call to `init`,
|
|||||||
|
|
||||||
| Part | Why |
|
| Part | Why |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `gpui_platform::application().with_assets(Assets)` | Ely embeds its icons and fonts. `init` returns an error when the source cannot load one of them. |
|
| `gpui_platform::application().with_assets(Assets)` | Ely embeds its icons and fonts. `init` panics without them. |
|
||||||
| `ely_gpui_component::init(cx)?` | Call once, before any window. It returns an error if the asset source fails. It registers the fonts, sets the theme, and binds Tab, Shift-Tab and the keys of text fields, the code editor, documents and, natively, the terminal. |
|
| `ely_gpui_component::init(cx)` | Call once, before any window. It registers the fonts, sets the theme, and binds Tab, Shift-Tab and the keys of text fields, the code editor, documents and, natively, the terminal. |
|
||||||
| A root `FocusScope` | `FocusScope::new(&handle).root()` owns Tab for the window and takes focus back when the focused element leaves the tree. Focus its handle when the window opens. |
|
| A root `FocusScope` | `FocusScope::new(&handle).root()` owns Tab for the window and takes focus back when the focused element leaves the tree. Focus its handle when the window opens. |
|
||||||
| `cx.theme()` | Colors, sizes and radii come from the theme. Components read it; an app's own boxes read it too. |
|
| `cx.theme()` | Colors, sizes and radii come from the theme. Components read it; an app's own boxes read it too. |
|
||||||
|
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
# GPUI Kit
|
|
||||||
|
|
||||||
Ely runs beside Longbridge's [GPUI Kit](https://gpui-kit.com) in one app and one window. Both build on one GPUI: Ely pins `gpui-pre =0.3.8`, the snapshot Kit 0.7.1 pins.
|
|
||||||
|
|
||||||
## Add the dependencies
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[dependencies]
|
|
||||||
gpui-kit = "0.7"
|
|
||||||
ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components" }
|
|
||||||
```
|
|
||||||
|
|
||||||
Kit brings GPUI and puts it at its root, so the app writes `use gpui_kit::*` and lists no `gpui` line of its own. Ely follows Kit's default GPUI.
|
|
||||||
|
|
||||||
In an app, Cargo picks the newest Kit that shares Ely's GPUI. A Kit release on another `gpui-pre` waits until Ely's pin follows. Ely's CI asks for Kit's newest release on every push and fails on such a release, naming `gpui-pre`.
|
|
||||||
|
|
||||||
## Start both
|
|
||||||
|
|
||||||
| Step | Why |
|
|
||||||
| --- | --- |
|
|
||||||
| `gpui_kit::application().with_assets(Assets::before(gpui_kit::assets::Assets))` | GPUI takes one asset source. Ely's files load from Ely, every other path from Kit. `ely_gpui_component::init` returns an error if one of Ely's files does not load. |
|
|
||||||
| `gpui_kit::init(cx)` and `ely_gpui_component::init(cx)?` | In either order. |
|
|
||||||
| `gpui_kit::open_window(options, cx, build)` | Kit's `Root` hosts Kit's dialogs, sheets and notifications. |
|
|
||||||
| `FocusScope::new(&handle).root()` around the view, `handle` focused | Ely's Tab, focus return and overlays live in the scope. |
|
|
||||||
|
|
||||||
## Focus
|
|
||||||
|
|
||||||
Tab and Shift-Tab run through Ely's and Kit's stops in one order. Ely binds them under `ElyFocus`, the key context of its scopes, so Kit's code editor keeps Tab for indenting whichever `init` ran last. A Kit dialog opened from an Ely button keeps Tab inside and hands focus back when it closes; so does an Ely dialog over Kit's fields.
|
|
||||||
|
|
||||||
## Two themes
|
|
||||||
|
|
||||||
Each library keeps its own theme. A switch turns both:
|
|
||||||
|
|
||||||
| Ely | Kit |
|
|
||||||
| --- | --- |
|
|
||||||
| `Theme::set_mode(Mode::Dark, cx)` | `Theme::change(ThemeMode::Dark, Some(window), cx)` |
|
|
||||||
|
|
||||||
Locales work the same way: Ely reads its `I18n` global, Kit its `gpui_kit::component::set_locale`.
|
|
||||||
|
|
||||||
## Inspector
|
|
||||||
|
|
||||||
In debug builds Kit's `init` installs Kit's inspector. Call `ely_gpui_component::tooling::install_inspector(cx)` after it to draw Ely's.
|
|
||||||
|
|
||||||
## A whole app
|
|
||||||
|
|
||||||
[`compat/kit/examples/start.rs`](https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components/src/branch/main/compat/kit/examples/start.rs) puts Ely's heading and button beside Kit's field and dialog, and one button turns both themes. From a clone, run it with `cargo run --manifest-path compat/kit/Cargo.toml --example start`. The crate's tests hold each claim on this page.
|
|
||||||
@@ -6,7 +6,7 @@ import { Marked, type Tokens } from "marked";
|
|||||||
const at = (relative: string) => fileURLToPath(new URL(relative, import.meta.url));
|
const at = (relative: string) => fileURLToPath(new URL(relative, import.meta.url));
|
||||||
|
|
||||||
/** The guides in reading order, by file name. */
|
/** The guides in reading order, by file name. */
|
||||||
const ORDER = ["introduction", "installation", "getting-started", "gpui-kit", "theme", "focus", "motion", "icons", "overlays", "rendering", "i18n", "webassembly", "testing"];
|
const ORDER = ["introduction", "installation", "getting-started", "theme", "focus", "motion", "icons", "overlays", "rendering", "i18n", "webassembly", "testing"];
|
||||||
|
|
||||||
export interface Guide {
|
export interface Guide {
|
||||||
/** The path segment; the introduction has none. */
|
/** The path segment; the introduction has none. */
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Icons, fonts and assets
|
# Icons, fonts and assets
|
||||||
|
|
||||||
`Assets` embeds Ely's icons and fonts in the binary. Pass it to `with_assets`, or `Assets::before(other)` to serve Ely's files first and every other path from another source; `init` checks that each of Ely's files loads through the app's source, then registers the fonts.
|
`Assets` embeds Ely's icons and fonts in the binary. Pass it to `with_assets`; `init` registers the fonts and checks that `icons/check.svg` loads.
|
||||||
|
|
||||||
## Icons
|
## Icons
|
||||||
|
|
||||||
@@ -42,13 +42,13 @@ A name rule beats an extension rule, and both beat Ely's map.
|
|||||||
|
|
||||||
## An app's own assets
|
## An app's own assets
|
||||||
|
|
||||||
GPUI takes one asset source. An app with files of its own, or another library's, passes `Assets::before(theirs)`: Ely's paths load from Ely, every other path from `theirs`, whose errors pass through. `init` returns an error naming the first of Ely's files the source cannot load.
|
GPUI takes one asset source. An app with files of its own writes an `AssetSource` that answers its own paths and hands every other path to `Assets`. `init` panics unless `icons/check.svg` loads through it.
|
||||||
|
|
||||||
Use `Icon::from_path("app-icons/mark.svg")` to draw one of those SVG assets with Ely's themed sizing and color. It also accepts an owned `String` or `SharedString`. Paths are asset keys resolved by `AssetSource`; a path it lacks logs an error on first draw and shows a red broken-picture icon. SVGs render as single-color masks; `IconName` and `IconPicker` continue to list the bundled icons.
|
Use `Icon::from_path("app-icons/mark.svg")` to draw one of those SVG assets with Ely's themed sizing and color. It also accepts an owned `String` or `SharedString`. Paths are asset keys resolved by `AssetSource`; a path it lacks panics on first draw. SVGs render as single-color masks; `IconName` and `IconPicker` continue to list the bundled icons.
|
||||||
|
|
||||||
Compatible single-color SVGs can come from [Lucide](https://lucide.dev/), [Hugeicons](https://github.com/hugeicons/hugeicons), [Phosphor](https://github.com/phosphor-icons/core), [Remix Icon](https://github.com/Remix-Design/RemixIcon), or [Tabler](https://github.com/tabler/tabler-icons). The app supplies its chosen files and preserves their licenses; Ely does not download or register those catalogs.
|
Compatible single-color SVGs can come from [Lucide](https://lucide.dev/), [Hugeicons](https://github.com/hugeicons/hugeicons), [Phosphor](https://github.com/phosphor-icons/core), [Remix Icon](https://github.com/Remix-Design/RemixIcon), or [Tabler](https://github.com/tabler/tabler-icons). The app supplies its chosen files and preserves their licenses; Ely does not download or register those catalogs.
|
||||||
|
|
||||||
The example below includes its asset source. Copy [`examples/assets/mark.svg`](https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components/src/branch/main/examples/assets/mark.svg) with it, or supply your own SVG.
|
The example below includes its asset source. Copy [`examples/assets/mark.svg`](https://github.com/ZacharyZhang-NY/Ely-GPUI-Components/blob/main/examples/assets/mark.svg) with it, or supply your own SVG.
|
||||||
|
|
||||||
## A window of icons
|
## A window of icons
|
||||||
|
|
||||||
|
|||||||
@@ -8,19 +8,19 @@
|
|||||||
|
|
||||||
## Add the dependencies
|
## Add the dependencies
|
||||||
|
|
||||||
Ely is not on crates.io. Depend on it by git, with GPUI at the snapshot Ely pins. `gpui-pre` republishes Zed's GPUI on crates.io, and GPUI Kit pins the same one:
|
Ely is not on crates.io. Depend on it by git, with GPUI from Zed's repository at the revision Ely pins:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components" }
|
ely-gpui-component = { git = "https://github.com/ZacharyZhang-NY/Ely-GPUI-Components" }
|
||||||
gpui = { package = "gpui-pre", version = "=0.3.8" }
|
gpui = { git = "https://github.com/zed-industries/zed", rev = "1a28cff4b409169bac058bca40dfbfeb7621d19b" }
|
||||||
gpui_platform = { package = "gpui-pre-platform", version = "=0.3.8", features = ["font-kit"] }
|
gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "1a28cff4b409169bac058bca40dfbfeb7621d19b", features = ["font-kit"] }
|
||||||
```
|
```
|
||||||
|
|
||||||
| Line | Why |
|
| Line | Why |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `ely-gpui-component` | The library. `Cargo.lock` records the commit; add `rev = "<commit>"` to hold it in `Cargo.toml` as well. |
|
| `ely-gpui-component` | The library. `Cargo.lock` records the commit; add `rev = "<commit>"` to hold it in `Cargo.toml` as well. |
|
||||||
| `gpui` | The same snapshot as Ely's, under the name `gpui`. Another builds a second GPUI, and its types do not match the ones Ely takes. |
|
| `gpui` | The same revision as Ely's. Another revision builds a second GPUI, and its types do not match the ones Ely takes. |
|
||||||
| `gpui_platform` | Opens windows on each platform. Without `font-kit`, macOS falls back to GPUI's no-op text system and draws no text. |
|
| `gpui_platform` | Opens windows on each platform. Without `font-kit`, macOS falls back to GPUI's no-op text system and draws no text. |
|
||||||
|
|
||||||
## Shaders
|
## Shaders
|
||||||
@@ -41,8 +41,9 @@ A window opens with a counter and a Press button. From a clone of the repository
|
|||||||
|
|
||||||
| Symptom | Cause and fix |
|
| Symptom | Cause and fix |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `init` error: `ely: the asset source lacks N of Ely's files, first <path>: <reason>` | The app's source cannot load every file Ely bundles. Pass `Assets`, or `Assets::before(yours)` when the app has a source of its own. |
|
| Panic: `` ely: pass `ely_gpui_component::Assets` to `Application::with_assets` `` | `init` found no Ely assets. Start the app with `gpui_platform::application().with_assets(Assets)`. |
|
||||||
| `init` error: `ely: embedded fonts failed to register` | GPUI's text system refused Ely's fonts. File a bug with the log. |
|
| Panic: `ely: asset source failed` | The app's asset source returned an error. The panic carries it. |
|
||||||
|
| Panic: `ely: embedded fonts failed to register` | GPUI's text system refused Ely's fonts. File a bug with the log. |
|
||||||
| Windows open with no text on macOS | `gpui_platform` lacks the `font-kit` feature. |
|
| Windows open with no text on macOS | `gpui_platform` lacks the `font-kit` feature. |
|
||||||
| `mismatched types`, with the note ``there are multiple different versions of crate `gpui` in the dependency graph`` | `gpui` names another snapshot or a git source. Use `gpui-pre` at `=0.3.8`. |
|
| `mismatched types`, with the note ``there are multiple different versions of crate `gpui` in the dependency graph`` | `gpui` points at a revision other than Ely's. Use `1a28cff4b409169bac058bca40dfbfeb7621d19b`. |
|
||||||
| `metal shader compilation failed` | Default features are off and Xcode's Metal compiler is missing. Keep `runtime_shaders`. |
|
| `metal shader compilation failed` | Default features are off and Xcode's Metal compiler is missing. Keep `runtime_shaders`. |
|
||||||
@@ -33,4 +33,4 @@ The example opens a window with one counter and one Button. `cargo run --example
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT. The icons are Lucide's, under ISC. Inter, JetBrains Mono and IBM Plex Sans are under the SIL Open Font License 1.1.
|
MIT or Apache-2.0, at your option. The icons are Lucide's, under ISC. Inter, JetBrains Mono and IBM Plex Sans are under the SIL Open Font License 1.1.
|
||||||
@@ -8,7 +8,7 @@ Ely tests itself on GPUI's test platform: every chapter has tests, and `cargo te
|
|||||||
|
|
||||||
## Testing an app that uses Ely
|
## Testing an app that uses Ely
|
||||||
|
|
||||||
GPUI's `TestAppContext` serves no assets, so `ely_gpui_component::init` returns an error there. Call `ely_gpui_component::init_for_tests(cx)` instead, under the `test-support` feature: it sets the theme and key bindings without assets or fonts.
|
GPUI's `TestAppContext` serves no assets, so `ely_gpui_component::init` panics there, and Ely's theme has no setup outside `init`. An app cannot draw Ely's components in a GPUI unit test today. Test the app's own state there, and see Ely's components in the gallery and in Ely's own tests.
|
||||||
|
|
||||||
## Writing a test inside Ely
|
## Writing a test inside Ely
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ GPUI's `TestAppContext` serves no assets, so `ely_gpui_component::init` returns
|
|||||||
|
|
||||||
## Tests to read
|
## Tests to read
|
||||||
|
|
||||||
- [Layers](https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components/src/branch/main/src/primitives/tests.rs): what is raised inside a raise lies over it, and keeps its state.
|
- [Layers](https://github.com/ZacharyZhang-NY/Ely-GPUI-Components/blob/main/src/primitives/tests.rs): what is raised inside a raise lies over it, and keeps its state.
|
||||||
- [Feedback](https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components/src/branch/main/src/feedback/tests/mod.rs): toasts and alerts, and focus handed to the root when a focused toast leaves.
|
- [Feedback](https://github.com/ZacharyZhang-NY/Ely-GPUI-Components/blob/main/src/feedback/tests/mod.rs): toasts and alerts, and focus handed to the root when a focused toast leaves.
|
||||||
- [Tables](https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components/src/branch/main/src/tables/tests/mod.rs): clicks, double presses, Tab across a grid.
|
- [Tables](https://github.com/ZacharyZhang-NY/Ely-GPUI-Components/blob/main/src/tables/tests/mod.rs): clicks, double presses, Tab across a grid.
|
||||||
- [Internationalization](https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components/src/branch/main/src/i18n/tests/mod.rs): catalogs, locales and a mirrored row.
|
- [Internationalization](https://github.com/ZacharyZhang-NY/Ely-GPUI-Components/blob/main/src/i18n/tests/mod.rs): catalogs, locales and a mirrored row.
|
||||||
@@ -13,7 +13,7 @@ RUSTC_BOOTSTRAP=1 cargo check --lib --target wasm32-unknown-unknown
|
|||||||
|
|
||||||
## Start an app in the browser
|
## Start an app in the browser
|
||||||
|
|
||||||
The gallery's [web entry](https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components/src/branch/main/examples/gallery/web.rs) shows the steps:
|
The gallery's [web entry](https://github.com/ZacharyZhang-NY/Ely-GPUI-Components/blob/main/examples/gallery/web.rs) shows the steps:
|
||||||
|
|
||||||
| Step | Why |
|
| Step | Why |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ import VolumeCross from "reicon/icons/VolumeCross";
|
|||||||
|
|
||||||
const set = { ArrowRightUp, Maximize, Minimize, Moon, Refresh, Sound, Sun, VolumeCross };
|
const set = { ArrowRightUp, Maximize, Minimize, Moon, Refresh, Sound, Sun, VolumeCross };
|
||||||
|
|
||||||
// simple-icons' Gitea mark (CC0), padded to sit like Reicon's.
|
// simple-icons' GitHub mark (CC0), padded to sit like Reicon's.
|
||||||
const GITEA =
|
const GITHUB =
|
||||||
'<svg viewBox="-3 -3 30 30" width="24" height="24" fill="currentColor"><path d="M4.209 4.603c-.247 0-.525.02-.84.088-.333.07-1.28.283-2.054 1.027C-.403 7.25.035 9.685.089 10.052c.065.446.263 1.687 1.21 2.768 1.749 2.141 5.513 2.092 5.513 2.092s.462 1.103 1.168 2.119c.955 1.263 1.936 2.248 2.89 2.367 2.406 0 7.212-.004 7.212-.004s.458.004 1.08-.394c.535-.324 1.013-.893 1.013-.893s.492-.527 1.18-1.73c.21-.37.385-.729.538-1.068 0 0 2.107-4.471 2.107-8.823-.042-1.318-.367-1.55-.443-1.627-.156-.156-.366-.153-.366-.153s-4.475.252-6.792.306c-.508.011-1.012.023-1.512.027v4.474l-.634-.301c0-1.39-.004-4.17-.004-4.17-1.107.016-3.405-.084-3.405-.084s-5.399-.27-5.987-.324c-.187-.011-.401-.032-.648-.032zm.354 1.832h.111s.271 2.269.6 3.597C5.549 11.147 6.22 13 6.22 13s-.996-.119-1.641-.348c-.99-.324-1.409-.714-1.409-.714s-.73-.511-1.096-1.52C1.444 8.73 2.021 7.7 2.021 7.7s.32-.859 1.47-1.145c.395-.106.863-.12 1.072-.12zm8.33 2.554c.26.003.509.127.509.127l.868.422-.529 1.075a.686.686 0 0 0-.614.359.685.685 0 0 0 .072.756l-.939 1.924a.69.69 0 0 0-.66.527.687.687 0 0 0 .347.763.686.686 0 0 0 .867-.206.688.688 0 0 0-.069-.882l.916-1.874a.667.667 0 0 0 .237-.02.657.657 0 0 0 .271-.137 8.826 8.826 0 0 1 1.016.512.761.761 0 0 1 .286.282c.073.21-.073.569-.073.569-.087.29-.702 1.55-.702 1.55a.692.692 0 0 0-.676.477.681.681 0 1 0 1.157-.252c.073-.141.141-.282.214-.431.19-.397.515-1.16.515-1.16.035-.066.218-.394.103-.814-.095-.435-.48-.638-.48-.638-.467-.301-1.116-.58-1.116-.58s0-.156-.042-.27a.688.688 0 0 0-.148-.241l.516-1.062 2.89 1.401s.48.218.583.619c.073.282-.019.534-.069.657-.24.587-2.1 4.317-2.1 4.317s-.232.554-.748.588a1.065 1.065 0 0 1-.393-.045l-.202-.08-4.31-2.1s-.417-.218-.49-.596c-.083-.31.104-.691.104-.691l2.073-4.272s.183-.37.466-.497a.855.855 0 0 1 .35-.077z"/></svg>';
|
'<svg viewBox="-3 -3 30 30" width="24" height="24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>';
|
||||||
|
|
||||||
export type IconName = keyof typeof set | "Gitea";
|
export type IconName = keyof typeof set | "GitHub";
|
||||||
|
|
||||||
/** An 18px glyph at 1.5px in a 24px box. */
|
/** An 18px glyph at 1.5px in a 24px box. */
|
||||||
export function Icon({ name }: { name: IconName }) {
|
export function Icon({ name }: { name: IconName }) {
|
||||||
const svg = name === "Gitea" ? GITEA : set[name].toSvg({ size: 24 });
|
const svg = name === "GitHub" ? GITHUB : set[name].toSvg({ size: 24 });
|
||||||
return <span className="icon" aria-hidden="true" dangerouslySetInnerHTML={{ __html: svg }} />;
|
return <span className="icon" aria-hidden="true" dangerouslySetInnerHTML={{ __html: svg }} />;
|
||||||
}
|
}
|
||||||
@@ -4,15 +4,6 @@ set -eu
|
|||||||
cargo fmt --check
|
cargo fmt --check
|
||||||
cargo clippy --all-targets --features test-support -- -D warnings
|
cargo clippy --all-targets --features test-support -- -D warnings
|
||||||
cargo test --lib --features test-support --quiet
|
cargo test --lib --features test-support --quiet
|
||||||
# Ely beside the newest GPUI Kit: a release that moves Kit's gpui pin fails to resolve here.
|
|
||||||
kit="--manifest-path compat/kit/Cargo.toml"
|
|
||||||
newest=$(cargo search gpui-kit --limit 1 --color never | sed -n 's/^gpui-kit = "\([^"]*\)".*/\1/p')
|
|
||||||
[ -n "$newest" ] || { echo "crates.io named no gpui-kit release" >&2; exit 1; }
|
|
||||||
cargo generate-lockfile $kit
|
|
||||||
cargo update $kit -p gpui-kit --precise "$newest"
|
|
||||||
cargo fmt $kit --check
|
|
||||||
cargo clippy $kit --all-targets --target-dir target/kit -- -D warnings
|
|
||||||
cargo test $kit --target-dir target/kit --quiet
|
|
||||||
rustup target list --installed | grep -qx wasm32-unknown-unknown || {
|
rustup target list --installed | grep -qx wasm32-unknown-unknown || {
|
||||||
echo "the web lint needs wasm32: rustup target add wasm32-unknown-unknown" >&2
|
echo "the web lint needs wasm32: rustup target add wasm32-unknown-unknown" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
+18
-12
@@ -24,20 +24,17 @@ pub struct TwoFactorInput {
|
|||||||
id: ElementId,
|
id: ElementId,
|
||||||
failed: usize,
|
failed: usize,
|
||||||
busy: bool,
|
busy: bool,
|
||||||
on_code: OnText,
|
on_code: Option<OnText>,
|
||||||
on_recovery: Option<Run>,
|
on_recovery: Option<Run>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TwoFactorInput {
|
impl TwoFactorInput {
|
||||||
pub fn new(
|
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||||
id: impl Into<ElementId>,
|
|
||||||
on_code: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
failed: 0,
|
failed: 0,
|
||||||
busy: false,
|
busy: false,
|
||||||
on_code: Rc::new(on_code),
|
on_code: None,
|
||||||
on_recovery: None,
|
on_recovery: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,6 +51,14 @@ impl TwoFactorInput {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_code(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_code = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Shows "Use a recovery code".
|
/// Shows "Use a recovery code".
|
||||||
pub fn on_recovery(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
pub fn on_recovery(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
||||||
self.on_recovery = Some(Rc::new(handler));
|
self.on_recovery = Some(Rc::new(handler));
|
||||||
@@ -64,7 +69,9 @@ impl TwoFactorInput {
|
|||||||
impl RenderOnce for TwoFactorInput {
|
impl RenderOnce for TwoFactorInput {
|
||||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let id = self.id;
|
let id = self.id;
|
||||||
let on_code = self.on_code;
|
let on_code = self
|
||||||
|
.on_code
|
||||||
|
.unwrap_or_else(|| panic!("two-factor input {id:?} has no on_code"));
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let note = match (self.busy, self.failed) {
|
let note = match (self.busy, self.failed) {
|
||||||
(true, _) => Some(
|
(true, _) => Some(
|
||||||
@@ -106,11 +113,10 @@ impl RenderOnce for TwoFactorInput {
|
|||||||
)
|
)
|
||||||
.children(note)
|
.children(note)
|
||||||
.children(self.on_recovery.map(|run| {
|
.children(self.on_recovery.map(|run| {
|
||||||
div().child(Link::new(
|
div().child(
|
||||||
(id.clone(), "recovery"),
|
Link::new((id.clone(), "recovery"), "Use a recovery code")
|
||||||
"Use a recovery code",
|
.on_click(move |_, window, cx| run(window, cx)),
|
||||||
move |_, window, cx| run(window, cx),
|
)
|
||||||
))
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+39
-16
@@ -33,26 +33,20 @@ pub struct ApiKeyManager {
|
|||||||
id: ElementId,
|
id: ElementId,
|
||||||
keys: Vec<ApiKey>,
|
keys: Vec<ApiKey>,
|
||||||
secret: Option<SharedString>,
|
secret: Option<SharedString>,
|
||||||
on_create: OnKey,
|
on_create: Option<OnKey>,
|
||||||
on_revoke: OnKey,
|
on_revoke: Option<OnKey>,
|
||||||
on_dismiss: Run,
|
on_dismiss: Option<Run>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApiKeyManager {
|
impl ApiKeyManager {
|
||||||
pub fn new(
|
pub fn new(id: impl Into<ElementId>, keys: impl IntoIterator<Item = ApiKey>) -> Self {
|
||||||
id: impl Into<ElementId>,
|
|
||||||
keys: impl IntoIterator<Item = ApiKey>,
|
|
||||||
on_create: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
|
||||||
on_revoke: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
|
||||||
on_dismiss: impl Fn(&mut Window, &mut App) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
keys: keys.into_iter().collect(),
|
keys: keys.into_iter().collect(),
|
||||||
secret: None,
|
secret: None,
|
||||||
on_create: Rc::new(on_create),
|
on_create: None,
|
||||||
on_revoke: Rc::new(on_revoke),
|
on_revoke: None,
|
||||||
on_dismiss: Rc::new(on_dismiss),
|
on_dismiss: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,13 +55,40 @@ impl ApiKeyManager {
|
|||||||
self.secret = Some(secret.into());
|
self.secret = Some(secret.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs with the new key's name.
|
||||||
|
pub fn on_create(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_create = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_revoke(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_revoke = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs on Done under a secret; the owner clears it.
|
||||||
|
pub fn on_dismiss(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
||||||
|
self.on_dismiss = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for ApiKeyManager {
|
impl RenderOnce for ApiKeyManager {
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let id = self.id;
|
let id = self.id;
|
||||||
let on_create = self.on_create;
|
let on_create = self
|
||||||
let on_revoke = self.on_revoke;
|
.on_create
|
||||||
|
.unwrap_or_else(|| panic!("api key manager {id:?} has no on_create"));
|
||||||
|
let on_revoke = self
|
||||||
|
.on_revoke
|
||||||
|
.unwrap_or_else(|| panic!("api key manager {id:?} has no on_revoke"));
|
||||||
let name = field(&id, "name", "A name, such as Deploys", false, window, cx);
|
let name = field(&id, "name", "A name, such as Deploys", false, window, cx);
|
||||||
let named: SharedString = name.read(cx).text().trim().to_string().into();
|
let named: SharedString = name.read(cx).text().trim().to_string().into();
|
||||||
let ready = !named.is_empty();
|
let ready = !named.is_empty();
|
||||||
@@ -81,7 +102,9 @@ impl RenderOnce for ApiKeyManager {
|
|||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let mono = theme.mono_family.clone();
|
let mono = theme.mono_family.clone();
|
||||||
let secret = self.secret.map(|secret| {
|
let secret = self.secret.map(|secret| {
|
||||||
let dismiss = self.on_dismiss.clone();
|
let dismiss = self.on_dismiss.clone().unwrap_or_else(|| {
|
||||||
|
panic!("api key manager {id:?} shows a secret with no on_dismiss")
|
||||||
|
});
|
||||||
Callout::new(Severity::Warning)
|
Callout::new(Severity::Warning)
|
||||||
.title("Copy this key now. It will not show again.")
|
.title("Copy this key now. It will not show again.")
|
||||||
.child(
|
.child(
|
||||||
|
|||||||
+14
-13
@@ -48,7 +48,7 @@ pub(super) fn aside(
|
|||||||
.text_size(theme.text_size(TextSize::Sm))
|
.text_size(theme.text_size(TextSize::Sm))
|
||||||
.text_color(theme.colors.fg_muted)
|
.text_color(theme.colors.fg_muted)
|
||||||
.child(words)
|
.child(words)
|
||||||
.child(Link::new(id, link, move |_, window, cx| run(window, cx)))
|
.child(Link::new(id, link).on_click(move |_, window, cx| run(window, cx)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a sign-in hands its owner.
|
/// What a sign-in hands its owner.
|
||||||
@@ -67,21 +67,18 @@ pub struct LoginForm {
|
|||||||
id: ElementId,
|
id: ElementId,
|
||||||
error: Option<SharedString>,
|
error: Option<SharedString>,
|
||||||
busy: bool,
|
busy: bool,
|
||||||
on_submit: OnLogin,
|
on_submit: Option<OnLogin>,
|
||||||
on_forgot: Option<Run>,
|
on_forgot: Option<Run>,
|
||||||
on_sign_up: Option<Run>,
|
on_sign_up: Option<Run>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LoginForm {
|
impl LoginForm {
|
||||||
pub fn new(
|
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||||
id: impl Into<ElementId>,
|
|
||||||
on_submit: impl Fn(&Login, &mut Window, &mut App) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
error: None,
|
error: None,
|
||||||
busy: false,
|
busy: false,
|
||||||
on_submit: Rc::new(on_submit),
|
on_submit: None,
|
||||||
on_forgot: None,
|
on_forgot: None,
|
||||||
on_sign_up: None,
|
on_sign_up: None,
|
||||||
}
|
}
|
||||||
@@ -99,6 +96,11 @@ impl LoginForm {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_submit(mut self, handler: impl Fn(&Login, &mut Window, &mut App) + 'static) -> Self {
|
||||||
|
self.on_submit = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Shows "Forgot password?".
|
/// Shows "Forgot password?".
|
||||||
pub fn on_forgot(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
pub fn on_forgot(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
||||||
self.on_forgot = Some(Rc::new(handler));
|
self.on_forgot = Some(Rc::new(handler));
|
||||||
@@ -125,7 +127,9 @@ impl RenderOnce for LoginForm {
|
|||||||
};
|
};
|
||||||
let remembered = login.remember;
|
let remembered = login.remember;
|
||||||
let ready = is_email(&login.email) && !login.password.is_empty() && !self.busy;
|
let ready = is_email(&login.email) && !login.password.is_empty() && !self.busy;
|
||||||
let on_submit = self.on_submit;
|
let on_submit = self
|
||||||
|
.on_submit
|
||||||
|
.unwrap_or_else(|| panic!("login form {id:?} has no on_submit"));
|
||||||
let submit: Run = Rc::new(move |window, cx| {
|
let submit: Run = Rc::new(move |window, cx| {
|
||||||
log::info!("login form: sign in");
|
log::info!("login form: sign in");
|
||||||
on_submit(&login, window, cx);
|
on_submit(&login, window, cx);
|
||||||
@@ -166,11 +170,8 @@ impl RenderOnce for LoginForm {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.children(self.on_forgot.map(|run| {
|
.children(self.on_forgot.map(|run| {
|
||||||
Link::new(
|
Link::new((id.clone(), "forgot"), "Forgot password?")
|
||||||
(id.clone(), "forgot"),
|
.on_click(move |_, window, cx| run(window, cx))
|
||||||
"Forgot password?",
|
|
||||||
move |_, window, cx| run(window, cx),
|
|
||||||
)
|
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.children(
|
.children(
|
||||||
|
|||||||
+27
-16
@@ -33,7 +33,7 @@ struct Step {
|
|||||||
words: Words,
|
words: Words,
|
||||||
sent: bool,
|
sent: bool,
|
||||||
busy: bool,
|
busy: bool,
|
||||||
on_send: OnText,
|
on_send: Option<OnText>,
|
||||||
back: Way,
|
back: Way,
|
||||||
other: Way,
|
other: Way,
|
||||||
}
|
}
|
||||||
@@ -52,6 +52,7 @@ fn email_step(step: Step, window: &mut Window, cx: &mut App) -> AnyElement {
|
|||||||
let email = field(&id, "email", "name@example.com", false, window, cx);
|
let email = field(&id, "email", "name@example.com", false, window, cx);
|
||||||
let address: SharedString = email.read(cx).text().trim().to_string().into();
|
let address: SharedString = email.read(cx).text().trim().to_string().into();
|
||||||
let ready = is_email(&address) && !busy;
|
let ready = is_email(&address) && !busy;
|
||||||
|
let on_send = on_send.unwrap_or_else(|| panic!("email step {id:?} has no on_send"));
|
||||||
let send: Run = {
|
let send: Run = {
|
||||||
let address = address.clone();
|
let address = address.clone();
|
||||||
Rc::new(move |window, cx| {
|
Rc::new(move |window, cx| {
|
||||||
@@ -71,9 +72,7 @@ fn email_step(step: Step, window: &mut Window, cx: &mut App) -> AnyElement {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
let back = back.map(|(label, run)| {
|
let back = back.map(|(label, run)| {
|
||||||
Link::new((id.clone(), "back"), label, move |_, window, cx| {
|
Link::new((id.clone(), "back"), label).on_click(move |_, window, cx| run(window, cx))
|
||||||
run(window, cx)
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
if sent {
|
if sent {
|
||||||
let other = other.map(|(label, run)| {
|
let other = other.map(|(label, run)| {
|
||||||
@@ -150,20 +149,17 @@ pub struct MagicLinkForm {
|
|||||||
id: ElementId,
|
id: ElementId,
|
||||||
sent: bool,
|
sent: bool,
|
||||||
busy: bool,
|
busy: bool,
|
||||||
on_send: OnText,
|
on_send: Option<OnText>,
|
||||||
on_other: Option<Run>,
|
on_other: Option<Run>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MagicLinkForm {
|
impl MagicLinkForm {
|
||||||
pub fn new(
|
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||||
id: impl Into<ElementId>,
|
|
||||||
on_send: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
sent: false,
|
sent: false,
|
||||||
busy: false,
|
busy: false,
|
||||||
on_send: Rc::new(on_send),
|
on_send: None,
|
||||||
on_other: None,
|
on_other: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,6 +176,15 @@ impl MagicLinkForm {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs with the address, for the first link and each one after.
|
||||||
|
pub fn on_send(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_send = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Shows "Use another email" once sent; the owner clears `sent`.
|
/// Shows "Use another email" once sent; the owner clears `sent`.
|
||||||
pub fn on_other(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
pub fn on_other(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
||||||
self.on_other = Some(Rc::new(handler));
|
self.on_other = Some(Rc::new(handler));
|
||||||
@@ -217,20 +222,17 @@ pub struct ForgotPassword {
|
|||||||
id: ElementId,
|
id: ElementId,
|
||||||
sent: bool,
|
sent: bool,
|
||||||
busy: bool,
|
busy: bool,
|
||||||
on_send: OnText,
|
on_send: Option<OnText>,
|
||||||
on_back: Option<Run>,
|
on_back: Option<Run>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ForgotPassword {
|
impl ForgotPassword {
|
||||||
pub fn new(
|
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||||
id: impl Into<ElementId>,
|
|
||||||
on_send: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
sent: false,
|
sent: false,
|
||||||
busy: false,
|
busy: false,
|
||||||
on_send: Rc::new(on_send),
|
on_send: None,
|
||||||
on_back: None,
|
on_back: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -247,6 +249,15 @@ impl ForgotPassword {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs with the address, for the first link and each one after.
|
||||||
|
pub fn on_send(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_send = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Shows "Back to sign in".
|
/// Shows "Back to sign in".
|
||||||
pub fn on_back(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
pub fn on_back(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
||||||
self.on_back = Some(Rc::new(handler));
|
self.on_back = Some(Rc::new(handler));
|
||||||
|
|||||||
+17
-9
@@ -14,7 +14,7 @@ pub struct OAuthButtons {
|
|||||||
id: ElementId,
|
id: ElementId,
|
||||||
providers: Vec<(SharedString, SharedString)>,
|
providers: Vec<(SharedString, SharedString)>,
|
||||||
busy: Option<SharedString>,
|
busy: Option<SharedString>,
|
||||||
on_pick: OnKey,
|
on_pick: Option<OnKey>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OAuthButtons {
|
impl OAuthButtons {
|
||||||
@@ -22,7 +22,6 @@ impl OAuthButtons {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
id: impl Into<ElementId>,
|
id: impl Into<ElementId>,
|
||||||
providers: impl IntoIterator<Item = (impl Into<SharedString>, impl Into<SharedString>)>,
|
providers: impl IntoIterator<Item = (impl Into<SharedString>, impl Into<SharedString>)>,
|
||||||
on_pick: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let id = id.into();
|
let id = id.into();
|
||||||
let providers: Vec<_> = providers
|
let providers: Vec<_> = providers
|
||||||
@@ -34,27 +33,36 @@ impl OAuthButtons {
|
|||||||
id,
|
id,
|
||||||
providers,
|
providers,
|
||||||
busy: None,
|
busy: None,
|
||||||
on_pick: Rc::new(on_pick),
|
on_pick: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The provider on its way.
|
/// The provider on its way.
|
||||||
pub fn busy(mut self, key: impl Into<SharedString>) -> Self {
|
pub fn busy(mut self, key: impl Into<SharedString>) -> Self {
|
||||||
let key = key.into();
|
let key = key.into();
|
||||||
if !self.providers.iter().any(|(each, _)| *each == key) {
|
assert!(
|
||||||
log::error!("oauth buttons: no provider {key}; none busy");
|
self.providers.iter().any(|(each, _)| *each == key),
|
||||||
self.busy = None;
|
"oauth buttons: no provider {key}"
|
||||||
return self;
|
);
|
||||||
}
|
|
||||||
self.busy = Some(key);
|
self.busy = Some(key);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_pick(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_pick = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for OAuthButtons {
|
impl RenderOnce for OAuthButtons {
|
||||||
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
||||||
let id = self.id;
|
let id = self.id;
|
||||||
let on_pick = self.on_pick;
|
let on_pick = self
|
||||||
|
.on_pick
|
||||||
|
.unwrap_or_else(|| panic!("oauth buttons {id:?} have no on_pick"));
|
||||||
let busy = self.busy;
|
let busy = self.busy;
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
|
|||||||
+19
-18
@@ -97,12 +97,11 @@ pub fn expiry(text: &str, now: (i32, i8)) -> Result<(i8, i32), ExpiryError> {
|
|||||||
.trim()
|
.trim()
|
||||||
.parse::<i8>()
|
.parse::<i8>()
|
||||||
.map_err(|_| ExpiryError::Unread)?;
|
.map_err(|_| ExpiryError::Unread)?;
|
||||||
let year = year
|
let year = 2000
|
||||||
.trim()
|
+ year
|
||||||
.parse::<i32>()
|
.trim()
|
||||||
.ok()
|
.parse::<i32>()
|
||||||
.and_then(|year| year.checked_add(2000))
|
.map_err(|_| ExpiryError::Unread)?;
|
||||||
.ok_or(ExpiryError::Unread)?;
|
|
||||||
if !(1..=12).contains(&month) {
|
if !(1..=12).contains(&month) {
|
||||||
return Err(ExpiryError::NoMonth(month));
|
return Err(ExpiryError::NoMonth(month));
|
||||||
}
|
}
|
||||||
@@ -147,19 +146,16 @@ pub struct PaymentMethodForm {
|
|||||||
id: ElementId,
|
id: ElementId,
|
||||||
error: Option<SharedString>,
|
error: Option<SharedString>,
|
||||||
busy: bool,
|
busy: bool,
|
||||||
on_submit: OnCard,
|
on_submit: Option<OnCard>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PaymentMethodForm {
|
impl PaymentMethodForm {
|
||||||
pub fn new(
|
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||||
id: impl Into<ElementId>,
|
|
||||||
on_submit: impl Fn(&CardDetails, &mut Window, &mut App) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
error: None,
|
error: None,
|
||||||
busy: false,
|
busy: false,
|
||||||
on_submit: Rc::new(on_submit),
|
on_submit: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,12 +170,22 @@ impl PaymentMethodForm {
|
|||||||
self.busy = busy;
|
self.busy = busy;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_submit(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(&CardDetails, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_submit = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for PaymentMethodForm {
|
impl RenderOnce for PaymentMethodForm {
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let id = self.id;
|
let id = self.id;
|
||||||
let on_submit = self.on_submit;
|
let on_submit = self
|
||||||
|
.on_submit
|
||||||
|
.unwrap_or_else(|| panic!("payment method form {id:?} has no on_submit"));
|
||||||
let number = field(&id, "number", "", false, window, cx);
|
let number = field(&id, "number", "", false, window, cx);
|
||||||
let until = field(&id, "expiry", "", false, window, cx);
|
let until = field(&id, "expiry", "", false, window, cx);
|
||||||
let code = field(&id, "cvc", "", false, window, cx);
|
let code = field(&id, "cvc", "", false, window, cx);
|
||||||
@@ -309,11 +315,6 @@ mod tests {
|
|||||||
"this month still counts"
|
"this month still counts"
|
||||||
);
|
);
|
||||||
assert_eq!(expiry("12/31", now), Ok((12, 2031)));
|
assert_eq!(expiry("12/31", now), Ok((12, 2031)));
|
||||||
assert_eq!(
|
|
||||||
expiry("12/2147483647", now),
|
|
||||||
Err(ExpiryError::Unread),
|
|
||||||
"a year past any clock"
|
|
||||||
);
|
|
||||||
assert_eq!(expiry("08/26", now), Err(ExpiryError::Passed));
|
assert_eq!(expiry("08/26", now), Err(ExpiryError::Passed));
|
||||||
assert_eq!(expiry("13/30", now), Err(ExpiryError::NoMonth(13)));
|
assert_eq!(expiry("13/30", now), Err(ExpiryError::NoMonth(13)));
|
||||||
assert_eq!(expiry("1230", now), Err(ExpiryError::Unread));
|
assert_eq!(expiry("1230", now), Err(ExpiryError::Unread));
|
||||||
|
|||||||
+11
-8
@@ -212,27 +212,30 @@ fn savable(edited: &Profile, seed: &Profile) -> bool {
|
|||||||
pub struct ProfileEditor {
|
pub struct ProfileEditor {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
profile: Profile,
|
profile: Profile,
|
||||||
on_save: OnProfile,
|
on_save: Option<OnProfile>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProfileEditor {
|
impl ProfileEditor {
|
||||||
pub fn new(
|
pub fn new(id: impl Into<ElementId>, profile: Profile) -> Self {
|
||||||
id: impl Into<ElementId>,
|
|
||||||
profile: Profile,
|
|
||||||
on_save: impl Fn(&Profile, &mut Window, &mut App) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
profile,
|
profile,
|
||||||
on_save: Rc::new(on_save),
|
on_save: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_save(mut self, handler: impl Fn(&Profile, &mut Window, &mut App) + 'static) -> Self {
|
||||||
|
self.on_save = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for ProfileEditor {
|
impl RenderOnce for ProfileEditor {
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let (id, profile) = (self.id, self.profile);
|
let (id, profile) = (self.id, self.profile);
|
||||||
let on_save = self.on_save;
|
let on_save = self
|
||||||
|
.on_save
|
||||||
|
.unwrap_or_else(|| panic!("profile editor {id:?} has no on_save"));
|
||||||
let fields = window.use_keyed_state((id.clone(), "fields"), cx, |window, cx| {
|
let fields = window.use_keyed_state((id.clone(), "fields"), cx, |window, cx| {
|
||||||
Fields::filled(&profile, window, cx)
|
Fields::filled(&profile, window, cx)
|
||||||
});
|
});
|
||||||
|
|||||||
+14
-8
@@ -33,16 +33,12 @@ pub struct Session {
|
|||||||
pub struct SessionList {
|
pub struct SessionList {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
sessions: Vec<Session>,
|
sessions: Vec<Session>,
|
||||||
on_sign_out: OnKey,
|
on_sign_out: Option<OnKey>,
|
||||||
on_sign_out_others: Option<Run>,
|
on_sign_out_others: Option<Run>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SessionList {
|
impl SessionList {
|
||||||
pub fn new(
|
pub fn new(id: impl Into<ElementId>, sessions: impl IntoIterator<Item = Session>) -> Self {
|
||||||
id: impl Into<ElementId>,
|
|
||||||
sessions: impl IntoIterator<Item = Session>,
|
|
||||||
on_sign_out: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
let mut sessions: Vec<_> = sessions.into_iter().collect();
|
let mut sessions: Vec<_> = sessions.into_iter().collect();
|
||||||
assert!(
|
assert!(
|
||||||
sessions.iter().filter(|session| session.current).count() <= 1,
|
sessions.iter().filter(|session| session.current).count() <= 1,
|
||||||
@@ -52,11 +48,19 @@ impl SessionList {
|
|||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
sessions,
|
sessions,
|
||||||
on_sign_out: Rc::new(on_sign_out),
|
on_sign_out: None,
|
||||||
on_sign_out_others: None,
|
on_sign_out_others: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_sign_out(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_sign_out = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Shows "Sign out of all other devices" while there are others.
|
/// Shows "Sign out of all other devices" while there are others.
|
||||||
pub fn on_sign_out_others(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
pub fn on_sign_out_others(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
||||||
self.on_sign_out_others = Some(Rc::new(handler));
|
self.on_sign_out_others = Some(Rc::new(handler));
|
||||||
@@ -67,7 +71,9 @@ impl SessionList {
|
|||||||
impl RenderOnce for SessionList {
|
impl RenderOnce for SessionList {
|
||||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let id = self.id;
|
let id = self.id;
|
||||||
let on_sign_out = self.on_sign_out;
|
let on_sign_out = self
|
||||||
|
.on_sign_out
|
||||||
|
.unwrap_or_else(|| panic!("session list {id:?} has no on_sign_out"));
|
||||||
let others = self.sessions.iter().any(|session| !session.current);
|
let others = self.sessions.iter().any(|session| !session.current);
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let rows = self.sessions.into_iter().enumerate().map(|(ix, session)| {
|
let rows = self.sessions.into_iter().enumerate().map(|(ix, session)| {
|
||||||
|
|||||||
+11
-7
@@ -40,21 +40,18 @@ pub struct SignupForm {
|
|||||||
terms: Option<SharedString>,
|
terms: Option<SharedString>,
|
||||||
error: Option<SharedString>,
|
error: Option<SharedString>,
|
||||||
busy: bool,
|
busy: bool,
|
||||||
on_submit: OnSignup,
|
on_submit: Option<OnSignup>,
|
||||||
on_sign_in: Option<Run>,
|
on_sign_in: Option<Run>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SignupForm {
|
impl SignupForm {
|
||||||
pub fn new(
|
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||||
id: impl Into<ElementId>,
|
|
||||||
on_submit: impl Fn(&Signup, &mut Window, &mut App) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
terms: None,
|
terms: None,
|
||||||
error: None,
|
error: None,
|
||||||
busy: false,
|
busy: false,
|
||||||
on_submit: Rc::new(on_submit),
|
on_submit: None,
|
||||||
on_sign_in: None,
|
on_sign_in: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,6 +74,11 @@ impl SignupForm {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_submit(mut self, handler: impl Fn(&Signup, &mut Window, &mut App) + 'static) -> Self {
|
||||||
|
self.on_submit = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Shows "Have an account? Sign in".
|
/// Shows "Have an account? Sign in".
|
||||||
pub fn on_sign_in(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
pub fn on_sign_in(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
||||||
self.on_sign_in = Some(Rc::new(handler));
|
self.on_sign_in = Some(Rc::new(handler));
|
||||||
@@ -103,7 +105,9 @@ impl RenderOnce for SignupForm {
|
|||||||
&& rules.iter().all(|(_, kept)| *kept)
|
&& rules.iter().all(|(_, kept)| *kept)
|
||||||
&& (self.terms.is_none() || agreed)
|
&& (self.terms.is_none() || agreed)
|
||||||
&& !self.busy;
|
&& !self.busy;
|
||||||
let on_submit = self.on_submit;
|
let on_submit = self
|
||||||
|
.on_submit
|
||||||
|
.unwrap_or_else(|| panic!("sign-up form {id:?} has no on_submit"));
|
||||||
let submit: Run = Rc::new(move |window, cx| {
|
let submit: Run = Rc::new(move |window, cx| {
|
||||||
log::info!("sign-up form: create account");
|
log::info!("sign-up form: create account");
|
||||||
on_submit(&signup, window, cx);
|
on_submit(&signup, window, cx);
|
||||||
|
|||||||
+63
-40
@@ -109,7 +109,7 @@ pub struct AccountSwitcher {
|
|||||||
id: ElementId,
|
id: ElementId,
|
||||||
accounts: Vec<Account>,
|
accounts: Vec<Account>,
|
||||||
current: SharedString,
|
current: SharedString,
|
||||||
on_select: OnKey,
|
on_select: Option<OnKey>,
|
||||||
on_add: Option<Run>,
|
on_add: Option<Run>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,19 +118,30 @@ impl AccountSwitcher {
|
|||||||
id: impl Into<ElementId>,
|
id: impl Into<ElementId>,
|
||||||
accounts: impl IntoIterator<Item = Account>,
|
accounts: impl IntoIterator<Item = Account>,
|
||||||
current: impl Into<SharedString>,
|
current: impl Into<SharedString>,
|
||||||
on_select: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let (accounts, current): (Vec<_>, SharedString) =
|
let (accounts, current): (Vec<_>, SharedString) =
|
||||||
(accounts.into_iter().collect(), current.into());
|
(accounts.into_iter().collect(), current.into());
|
||||||
|
assert!(
|
||||||
|
accounts.iter().any(|account| account.key == current),
|
||||||
|
"account switcher: no account {current}"
|
||||||
|
);
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
accounts,
|
accounts,
|
||||||
current,
|
current,
|
||||||
on_select: Rc::new(on_select),
|
on_select: None,
|
||||||
on_add: None,
|
on_add: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_select(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_select = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Shows "Add account".
|
/// Shows "Add account".
|
||||||
pub fn on_add(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
pub fn on_add(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
||||||
self.on_add = Some(Rc::new(handler));
|
self.on_add = Some(Rc::new(handler));
|
||||||
@@ -141,7 +152,9 @@ impl AccountSwitcher {
|
|||||||
impl RenderOnce for AccountSwitcher {
|
impl RenderOnce for AccountSwitcher {
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let id = self.id;
|
let id = self.id;
|
||||||
let on_select = self.on_select;
|
let on_select = self
|
||||||
|
.on_select
|
||||||
|
.unwrap_or_else(|| panic!("account switcher {id:?} has no on_select"));
|
||||||
let items = self.accounts.iter().map(|account| {
|
let items = self.accounts.iter().map(|account| {
|
||||||
let (key, pick) = (account.key.clone(), on_select.clone());
|
let (key, pick) = (account.key.clone(), on_select.clone());
|
||||||
MenuItem::radio(account.email.clone(), account.key == self.current).on_click(
|
MenuItem::radio(account.email.clone(), account.key == self.current).on_click(
|
||||||
@@ -162,22 +175,18 @@ impl RenderOnce for AccountSwitcher {
|
|||||||
let now = self
|
let now = self
|
||||||
.accounts
|
.accounts
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|account| account.key == self.current);
|
.find(|account| account.key == self.current)
|
||||||
if now.is_none() {
|
.expect("the current account");
|
||||||
log::error!(
|
let mut avatar = Avatar::new((id.clone(), "avatar"), now.name.clone()).size(AvatarSize::Sm);
|
||||||
"account switcher {id:?}: no account {}; none current",
|
if let Some(image) = now.image {
|
||||||
self.current
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let name = now
|
|
||||||
.as_ref()
|
|
||||||
.map_or_else(|| SharedString::from("No account"), |now| now.name.clone());
|
|
||||||
let mut avatar = Avatar::new((id.clone(), "avatar"), name.clone()).size(AvatarSize::Sm);
|
|
||||||
if let Some(image) = now.as_ref().and_then(|now| now.image.clone()) {
|
|
||||||
avatar = avatar.image(image);
|
avatar = avatar.image(image);
|
||||||
}
|
}
|
||||||
let email = now.map(|now| now.email).unwrap_or_default();
|
let trigger = row(
|
||||||
let trigger = row((id.clone(), "trigger"), avatar, named(name, email, cx), cx);
|
(id.clone(), "trigger"),
|
||||||
|
avatar,
|
||||||
|
named(now.name, now.email, cx),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
opening(id, &menu, trigger, window, cx)
|
opening(id, &menu, trigger, window, cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,7 +197,7 @@ pub struct WorkspaceSwitcher {
|
|||||||
id: ElementId,
|
id: ElementId,
|
||||||
workspaces: Vec<Workspace>,
|
workspaces: Vec<Workspace>,
|
||||||
current: SharedString,
|
current: SharedString,
|
||||||
on_select: OnKey,
|
on_select: Option<OnKey>,
|
||||||
on_create: Option<Run>,
|
on_create: Option<Run>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,19 +206,30 @@ impl WorkspaceSwitcher {
|
|||||||
id: impl Into<ElementId>,
|
id: impl Into<ElementId>,
|
||||||
workspaces: impl IntoIterator<Item = Workspace>,
|
workspaces: impl IntoIterator<Item = Workspace>,
|
||||||
current: impl Into<SharedString>,
|
current: impl Into<SharedString>,
|
||||||
on_select: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let (workspaces, current): (Vec<_>, SharedString) =
|
let (workspaces, current): (Vec<_>, SharedString) =
|
||||||
(workspaces.into_iter().collect(), current.into());
|
(workspaces.into_iter().collect(), current.into());
|
||||||
|
assert!(
|
||||||
|
workspaces.iter().any(|each| each.key == current),
|
||||||
|
"workspace switcher: no workspace {current}"
|
||||||
|
);
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
workspaces,
|
workspaces,
|
||||||
current,
|
current,
|
||||||
on_select: Rc::new(on_select),
|
on_select: None,
|
||||||
on_create: None,
|
on_create: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_select(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_select = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Shows "Create workspace".
|
/// Shows "Create workspace".
|
||||||
pub fn on_create(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
pub fn on_create(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
||||||
self.on_create = Some(Rc::new(handler));
|
self.on_create = Some(Rc::new(handler));
|
||||||
@@ -220,7 +240,9 @@ impl WorkspaceSwitcher {
|
|||||||
impl RenderOnce for WorkspaceSwitcher {
|
impl RenderOnce for WorkspaceSwitcher {
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let id = self.id;
|
let id = self.id;
|
||||||
let on_select = self.on_select;
|
let on_select = self
|
||||||
|
.on_select
|
||||||
|
.unwrap_or_else(|| panic!("workspace switcher {id:?} has no on_select"));
|
||||||
let items: Vec<_> = self
|
let items: Vec<_> = self
|
||||||
.workspaces
|
.workspaces
|
||||||
.iter()
|
.iter()
|
||||||
@@ -245,22 +267,17 @@ impl RenderOnce for WorkspaceSwitcher {
|
|||||||
let now = self
|
let now = self
|
||||||
.workspaces
|
.workspaces
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|each| each.key == self.current);
|
.find(|each| each.key == self.current)
|
||||||
if now.is_none() {
|
.expect("the current workspace");
|
||||||
log::error!(
|
let mark = Avatar::new((id.clone(), "mark"), now.name.clone())
|
||||||
"workspace switcher {id:?}: no workspace {}; none current",
|
|
||||||
self.current
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let name = now.as_ref().map_or_else(
|
|
||||||
|| SharedString::from("No workspace"),
|
|
||||||
|now| now.name.clone(),
|
|
||||||
);
|
|
||||||
let mark = Avatar::new((id.clone(), "mark"), name.clone())
|
|
||||||
.square()
|
.square()
|
||||||
.size(AvatarSize::Sm);
|
.size(AvatarSize::Sm);
|
||||||
let detail = now.map(|now| now.detail).unwrap_or_default();
|
let trigger = row(
|
||||||
let trigger = row((id.clone(), "trigger"), mark, named(name, detail, cx), cx);
|
(id.clone(), "trigger"),
|
||||||
|
mark,
|
||||||
|
named(now.name, now.detail, cx),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
opening(id, &menu, trigger, window, cx)
|
opening(id, &menu, trigger, window, cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,7 +291,7 @@ pub struct UserMenu {
|
|||||||
image: Option<ImageSource>,
|
image: Option<ImageSource>,
|
||||||
presence: Option<Presence>,
|
presence: Option<Presence>,
|
||||||
items: Vec<MenuItem>,
|
items: Vec<MenuItem>,
|
||||||
on_sign_out: Run,
|
on_sign_out: Option<Run>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserMenu {
|
impl UserMenu {
|
||||||
@@ -282,7 +299,6 @@ impl UserMenu {
|
|||||||
id: impl Into<ElementId>,
|
id: impl Into<ElementId>,
|
||||||
name: impl Into<SharedString>,
|
name: impl Into<SharedString>,
|
||||||
email: impl Into<SharedString>,
|
email: impl Into<SharedString>,
|
||||||
on_sign_out: impl Fn(&mut Window, &mut App) + 'static,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
@@ -291,7 +307,7 @@ impl UserMenu {
|
|||||||
image: None,
|
image: None,
|
||||||
presence: None,
|
presence: None,
|
||||||
items: Vec::new(),
|
items: Vec::new(),
|
||||||
on_sign_out: Rc::new(on_sign_out),
|
on_sign_out: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,12 +326,19 @@ impl UserMenu {
|
|||||||
self.items.push(item);
|
self.items.push(item);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_sign_out(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
||||||
|
self.on_sign_out = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for UserMenu {
|
impl RenderOnce for UserMenu {
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let id = self.id;
|
let id = self.id;
|
||||||
let sign_out = self.on_sign_out;
|
let sign_out = self
|
||||||
|
.on_sign_out
|
||||||
|
.unwrap_or_else(|| panic!("user menu {id:?} has no on_sign_out"));
|
||||||
let menu =
|
let menu =
|
||||||
Menu::new()
|
Menu::new()
|
||||||
.group(format!("Signed in as {}", self.email), self.items)
|
.group(format!("Signed in as {}", self.email), self.items)
|
||||||
|
|||||||
+111
-47
@@ -25,13 +25,11 @@ pub struct Role {
|
|||||||
pub description: SharedString,
|
pub description: SharedString,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The role of `key`; none, logged for `owner`, when the roles lost it.
|
fn role_named<'a>(roles: &'a [Role], key: &SharedString, owner: &str) -> &'a Role {
|
||||||
fn role_named<'a>(roles: &'a [Role], key: &SharedString, owner: &str) -> Option<&'a Role> {
|
roles
|
||||||
let role = roles.iter().find(|role| role.key == *key);
|
.iter()
|
||||||
if role.is_none() {
|
.find(|role| role.key == *key)
|
||||||
log::error!("{owner}: no role {key}; none shown");
|
.unwrap_or_else(|| panic!("{owner}: no role {key}"))
|
||||||
}
|
|
||||||
role
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A role to pick, each with what it may do beneath its name.
|
/// A role to pick, each with what it may do beneath its name.
|
||||||
@@ -41,7 +39,7 @@ pub struct RoleSelector {
|
|||||||
roles: Vec<Role>,
|
roles: Vec<Role>,
|
||||||
selected: SharedString,
|
selected: SharedString,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
on_change: OnKey,
|
on_change: Option<OnKey>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RoleSelector {
|
impl RoleSelector {
|
||||||
@@ -49,7 +47,6 @@ impl RoleSelector {
|
|||||||
id: impl Into<ElementId>,
|
id: impl Into<ElementId>,
|
||||||
roles: impl IntoIterator<Item = Role>,
|
roles: impl IntoIterator<Item = Role>,
|
||||||
selected: impl Into<SharedString>,
|
selected: impl Into<SharedString>,
|
||||||
on_change: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let (roles, selected): (Vec<_>, SharedString) =
|
let (roles, selected): (Vec<_>, SharedString) =
|
||||||
(roles.into_iter().collect(), selected.into());
|
(roles.into_iter().collect(), selected.into());
|
||||||
@@ -59,7 +56,7 @@ impl RoleSelector {
|
|||||||
roles,
|
roles,
|
||||||
selected,
|
selected,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
on_change: Rc::new(on_change),
|
on_change: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,12 +64,22 @@ impl RoleSelector {
|
|||||||
self.disabled = disabled;
|
self.disabled = disabled;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_change(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_change = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for RoleSelector {
|
impl RenderOnce for RoleSelector {
|
||||||
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
||||||
let id = self.id;
|
let id = self.id;
|
||||||
let on_change = self.on_change;
|
let on_change = self
|
||||||
|
.on_change
|
||||||
|
.unwrap_or_else(|| panic!("role selector {id:?} has no on_change"));
|
||||||
let choices = self
|
let choices = self
|
||||||
.roles
|
.roles
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -106,8 +113,8 @@ pub struct TeamMemberTable {
|
|||||||
id: ElementId,
|
id: ElementId,
|
||||||
members: Vec<Member>,
|
members: Vec<Member>,
|
||||||
roles: Vec<Role>,
|
roles: Vec<Role>,
|
||||||
on_role: OnPair,
|
on_role: Option<OnPair>,
|
||||||
on_remove: OnKey,
|
on_remove: Option<OnKey>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TeamMemberTable {
|
impl TeamMemberTable {
|
||||||
@@ -115,26 +122,48 @@ impl TeamMemberTable {
|
|||||||
id: impl Into<ElementId>,
|
id: impl Into<ElementId>,
|
||||||
members: impl IntoIterator<Item = Member>,
|
members: impl IntoIterator<Item = Member>,
|
||||||
roles: impl IntoIterator<Item = Role>,
|
roles: impl IntoIterator<Item = Role>,
|
||||||
on_role: impl Fn(&SharedString, &SharedString, &mut Window, &mut App) + 'static,
|
|
||||||
on_remove: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let (members, roles): (Vec<Member>, Vec<Role>) =
|
let (members, roles): (Vec<Member>, Vec<Role>) =
|
||||||
(members.into_iter().collect(), roles.into_iter().collect());
|
(members.into_iter().collect(), roles.into_iter().collect());
|
||||||
|
for member in members.iter().filter(|member| !member.owner) {
|
||||||
|
role_named(&roles, &member.role, "team member table");
|
||||||
|
}
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
members,
|
members,
|
||||||
roles,
|
roles,
|
||||||
on_role: Rc::new(on_role),
|
on_role: None,
|
||||||
on_remove: Rc::new(on_remove),
|
on_remove: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs with a member's key and their new role's.
|
||||||
|
pub fn on_role(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(&SharedString, &SharedString, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_role = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_remove(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_remove = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for TeamMemberTable {
|
impl RenderOnce for TeamMemberTable {
|
||||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let id = self.id;
|
let id = self.id;
|
||||||
let on_role = self.on_role;
|
let on_role = self
|
||||||
let on_remove = self.on_remove;
|
.on_role
|
||||||
|
.unwrap_or_else(|| panic!("team member table {id:?} has no on_role"));
|
||||||
|
let on_remove = self
|
||||||
|
.on_remove
|
||||||
|
.unwrap_or_else(|| panic!("team member table {id:?} has no on_remove"));
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let rows = self.members.into_iter().enumerate().map(|(ix, member)| {
|
let rows = self.members.into_iter().enumerate().map(|(ix, member)| {
|
||||||
let mut avatar = Avatar::new(
|
let mut avatar = Avatar::new(
|
||||||
@@ -160,8 +189,8 @@ impl RenderOnce for TeamMemberTable {
|
|||||||
(id.clone(), format!("role-{}", member.key)),
|
(id.clone(), format!("role-{}", member.key)),
|
||||||
self.roles.clone(),
|
self.roles.clone(),
|
||||||
member.role.clone(),
|
member.role.clone(),
|
||||||
move |role, window, cx| set(&changed, role, window, cx),
|
)
|
||||||
);
|
.on_change(move |role, window, cx| set(&changed, role, window, cx));
|
||||||
let button = ConfirmButton::new(
|
let button = ConfirmButton::new(
|
||||||
(id.clone(), format!("remove-{}", member.key)),
|
(id.clone(), format!("remove-{}", member.key)),
|
||||||
"Remove",
|
"Remove",
|
||||||
@@ -243,9 +272,9 @@ pub struct InvitationList {
|
|||||||
id: ElementId,
|
id: ElementId,
|
||||||
invitations: Vec<Invitation>,
|
invitations: Vec<Invitation>,
|
||||||
roles: Vec<Role>,
|
roles: Vec<Role>,
|
||||||
on_invite: OnPair,
|
on_invite: Option<OnPair>,
|
||||||
on_resend: OnKey,
|
on_resend: Option<OnKey>,
|
||||||
on_revoke: OnKey,
|
on_revoke: Option<OnKey>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InvitationList {
|
impl InvitationList {
|
||||||
@@ -253,32 +282,63 @@ impl InvitationList {
|
|||||||
id: impl Into<ElementId>,
|
id: impl Into<ElementId>,
|
||||||
invitations: impl IntoIterator<Item = Invitation>,
|
invitations: impl IntoIterator<Item = Invitation>,
|
||||||
roles: impl IntoIterator<Item = Role>,
|
roles: impl IntoIterator<Item = Role>,
|
||||||
on_invite: impl Fn(&SharedString, &SharedString, &mut Window, &mut App) + 'static,
|
|
||||||
on_resend: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
|
||||||
on_revoke: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let (invitations, roles): (Vec<Invitation>, Vec<Role>) = (
|
let (invitations, roles): (Vec<Invitation>, Vec<Role>) = (
|
||||||
invitations.into_iter().collect(),
|
invitations.into_iter().collect(),
|
||||||
roles.into_iter().collect(),
|
roles.into_iter().collect(),
|
||||||
);
|
);
|
||||||
assert!(!roles.is_empty(), "invitation list: no role to invite as");
|
assert!(!roles.is_empty(), "invitation list: no role to invite as");
|
||||||
|
for invitation in &invitations {
|
||||||
|
role_named(&roles, &invitation.role, "invitation list");
|
||||||
|
}
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
invitations,
|
invitations,
|
||||||
roles,
|
roles,
|
||||||
on_invite: Rc::new(on_invite),
|
on_invite: None,
|
||||||
on_resend: Rc::new(on_resend),
|
on_resend: None,
|
||||||
on_revoke: Rc::new(on_revoke),
|
on_revoke: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs with the address and the role's key.
|
||||||
|
pub fn on_invite(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(&SharedString, &SharedString, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_invite = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_resend(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_resend = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_revoke(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_revoke = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for InvitationList {
|
impl RenderOnce for InvitationList {
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let id = self.id;
|
let id = self.id;
|
||||||
let on_invite = self.on_invite;
|
let on_invite = self
|
||||||
let on_resend = self.on_resend;
|
.on_invite
|
||||||
let on_revoke = self.on_revoke;
|
.unwrap_or_else(|| panic!("invitation list {id:?} has no on_invite"));
|
||||||
|
let on_resend = self
|
||||||
|
.on_resend
|
||||||
|
.unwrap_or_else(|| panic!("invitation list {id:?} has no on_resend"));
|
||||||
|
let on_revoke = self
|
||||||
|
.on_revoke
|
||||||
|
.unwrap_or_else(|| panic!("invitation list {id:?} has no on_revoke"));
|
||||||
let email = field(&id, "email", "name@example.com", false, window, cx);
|
let email = field(&id, "email", "name@example.com", false, window, cx);
|
||||||
let first = self.roles[0].key.clone();
|
let first = self.roles[0].key.clone();
|
||||||
let role = window.use_keyed_state((id.clone(), "role"), cx, move |_, _| first);
|
let role = window.use_keyed_state((id.clone(), "role"), cx, move |_, _| first);
|
||||||
@@ -286,8 +346,7 @@ impl RenderOnce for InvitationList {
|
|||||||
email.read(cx).text().trim().to_string().into(),
|
email.read(cx).text().trim().to_string().into(),
|
||||||
role.read(cx).clone(),
|
role.read(cx).clone(),
|
||||||
);
|
);
|
||||||
let offered = self.roles.iter().any(|role| role.key == chosen);
|
let ready = is_email(&address);
|
||||||
let ready = is_email(&address) && offered;
|
|
||||||
let clear = email.clone();
|
let clear = email.clone();
|
||||||
let invite = Rc::new(move |window: &mut Window, cx: &mut App| {
|
let invite = Rc::new(move |window: &mut Window, cx: &mut App| {
|
||||||
log::info!("invitation list: invite as {chosen}");
|
log::info!("invitation list: invite as {chosen}");
|
||||||
@@ -299,7 +358,8 @@ impl RenderOnce for InvitationList {
|
|||||||
let rows =
|
let rows =
|
||||||
self.invitations.iter().map(|invitation| {
|
self.invitations.iter().map(|invitation| {
|
||||||
let named = role_named(&self.roles, &invitation.role, "invitation list")
|
let named = role_named(&self.roles, &invitation.role, "invitation list")
|
||||||
.map_or_else(|| SharedString::from("No role"), |role| role.name.clone());
|
.name
|
||||||
|
.clone();
|
||||||
let [again, gone] = [(); 2].map(|_| invitation.key.clone());
|
let [again, gone] = [(); 2].map(|_| invitation.key.clone());
|
||||||
let (resend, revoke) = (on_resend.clone(), on_revoke.clone());
|
let (resend, revoke) = (on_resend.clone(), on_revoke.clone());
|
||||||
div()
|
div()
|
||||||
@@ -388,17 +448,21 @@ impl RenderOnce for InvitationList {
|
|||||||
.min_w(theme.label_width())
|
.min_w(theme.label_width())
|
||||||
.child(EmailInput::new(&email)),
|
.child(EmailInput::new(&email)),
|
||||||
)
|
)
|
||||||
.child(div().w(theme.label_width()).child(RoleSelector::new(
|
.child(
|
||||||
(id.clone(), "invite-role"),
|
div().w(theme.label_width()).child(
|
||||||
self.roles.clone(),
|
RoleSelector::new(
|
||||||
role.read(cx).clone(),
|
(id.clone(), "invite-role"),
|
||||||
move |key, _, cx| {
|
self.roles.clone(),
|
||||||
picked.update(cx, |role, cx| {
|
role.read(cx).clone(),
|
||||||
*role = key.clone();
|
)
|
||||||
cx.notify();
|
.on_change(move |key, _, cx| {
|
||||||
})
|
picked.update(cx, |role, cx| {
|
||||||
},
|
*role = key.clone();
|
||||||
)))
|
cx.notify();
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new((id.clone(), "invite"), "Invite")
|
Button::new((id.clone(), "invite"), "Invite")
|
||||||
.variant(ButtonVariant::Primary)
|
.variant(ButtonVariant::Primary)
|
||||||
|
|||||||
+26
-37
@@ -21,8 +21,8 @@ fn accounts(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
|||||||
account("home", "Ada", "ada@home.com"),
|
account("home", "Ada", "ada@home.com"),
|
||||||
],
|
],
|
||||||
"ada",
|
"ada",
|
||||||
move |key, _, cx| say(&owner, key.to_string(), cx),
|
|
||||||
)
|
)
|
||||||
|
.on_select(move |key, _, cx| say(&owner, key.to_string(), cx))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,13 +48,10 @@ fn sessions(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
|||||||
current,
|
current,
|
||||||
};
|
};
|
||||||
let everyone = owner.clone();
|
let everyone = owner.clone();
|
||||||
SessionList::new(
|
SessionList::new("sessions", [session("phone", false), session("mac", true)])
|
||||||
"sessions",
|
.on_sign_out(move |key, _, cx| say(&owner, format!("out {key}"), cx))
|
||||||
[session("phone", false), session("mac", true)],
|
.on_sign_out_others(move |_, cx| say(&everyone, "others".into(), cx))
|
||||||
move |key, _, cx| say(&owner, format!("out {key}"), cx),
|
.into_any_element()
|
||||||
)
|
|
||||||
.on_sign_out_others(move |_, cx| say(&everyone, "others".into(), cx))
|
|
||||||
.into_any_element()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops: the phone's Sign out, then Sign out of all other devices; this device has none.
|
/// Stops: the phone's Sign out, then Sign out of all other devices; this device has none.
|
||||||
@@ -84,13 +81,10 @@ fn keys(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
|
|||||||
used: "Never used".into(),
|
used: "Never used".into(),
|
||||||
};
|
};
|
||||||
let [made, revoked, done] = [(); 3].map(|_| owner.clone());
|
let [made, revoked, done] = [(); 3].map(|_| owner.clone());
|
||||||
let manager = ApiKeyManager::new(
|
let manager = ApiKeyManager::new("keys", [key])
|
||||||
"keys",
|
.on_create(move |name, _, cx| say(&made, format!("make {name}"), cx))
|
||||||
[key],
|
.on_revoke(move |key, _, cx| say(&revoked, format!("revoke {key}"), cx))
|
||||||
move |name, _, cx| say(&made, format!("make {name}"), cx),
|
.on_dismiss(move |_, cx| say(&done, "done".into(), cx));
|
||||||
move |key, _, cx| say(&revoked, format!("revoke {key}"), cx),
|
|
||||||
move |_, cx| say(&done, "done".into(), cx),
|
|
||||||
);
|
|
||||||
match bench.sent {
|
match bench.sent {
|
||||||
true => manager.secret("sk_live_0123456789").into_any_element(),
|
true => manager.secret("sk_live_0123456789").into_any_element(),
|
||||||
false => manager.into_any_element(),
|
false => manager.into_any_element(),
|
||||||
@@ -144,10 +138,11 @@ fn profile() -> Profile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn editor(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
fn editor(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
||||||
ProfileEditor::new("profile", profile(), move |profile, _, cx| {
|
ProfileEditor::new("profile", profile())
|
||||||
say(&owner, format!("{} {}", profile.name, profile.email), cx)
|
.on_save(move |profile, _, cx| {
|
||||||
})
|
say(&owner, format!("{} {}", profile.name, profile.email), cx)
|
||||||
.into_any_element()
|
})
|
||||||
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops: the picture, name, username, email, about, location; Discard and Save once changed.
|
/// Stops: the picture, name, username, email, about, location; Discard and Save once changed.
|
||||||
@@ -172,11 +167,10 @@ fn a_profile_saves_only_what_changed_and_discard_puts_it_back(cx: &mut TestAppCo
|
|||||||
|
|
||||||
fn menu(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
fn menu(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
||||||
let [profile, out] = [(); 2].map(|_| owner.clone());
|
let [profile, out] = [(); 2].map(|_| owner.clone());
|
||||||
UserMenu::new("user", "Ada", "ada@example.com", move |_, cx| {
|
UserMenu::new("user", "Ada", "ada@example.com")
|
||||||
say(&out, "out".into(), cx)
|
.item(MenuItem::new("Profile").on_click(move |_, cx| say(&profile, "profile".into(), cx)))
|
||||||
})
|
.on_sign_out(move |_, cx| say(&out, "out".into(), cx))
|
||||||
.item(MenuItem::new("Profile").on_click(move |_, cx| say(&profile, "profile".into(), cx)))
|
.into_any_element()
|
||||||
.into_any_element()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops: the avatar; the menu's rows are Profile, then Sign out.
|
/// Stops: the avatar; the menu's rows are Profile, then Sign out.
|
||||||
@@ -197,14 +191,10 @@ fn workspaces(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
|||||||
detail: "Pro".into(),
|
detail: "Pro".into(),
|
||||||
};
|
};
|
||||||
let made = owner.clone();
|
let made = owner.clone();
|
||||||
WorkspaceSwitcher::new(
|
WorkspaceSwitcher::new("spaces", [space("north"), space("south")], "south")
|
||||||
"spaces",
|
.on_select(move |key, _, cx| say(&owner, key.to_string(), cx))
|
||||||
[space("north"), space("south")],
|
.on_create(move |_, cx| say(&made, "create".into(), cx))
|
||||||
"south",
|
.into_any_element()
|
||||||
move |key, _, cx| say(&owner, key.to_string(), cx),
|
|
||||||
)
|
|
||||||
.on_create(move |_, cx| say(&made, "create".into(), cx))
|
|
||||||
.into_any_element()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops: the switcher; its rows are north, south, then Create workspace.
|
/// Stops: the switcher; its rows are north, south, then Create workspace.
|
||||||
@@ -232,11 +222,10 @@ fn alone(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
|||||||
current: true,
|
current: true,
|
||||||
};
|
};
|
||||||
let everyone = owner.clone();
|
let everyone = owner.clone();
|
||||||
SessionList::new("sessions", [here], move |key, _, cx| {
|
SessionList::new("sessions", [here])
|
||||||
say(&owner, format!("out {key}"), cx)
|
.on_sign_out(move |key, _, cx| say(&owner, format!("out {key}"), cx))
|
||||||
})
|
.on_sign_out_others(move |_, cx| say(&everyone, "others".into(), cx))
|
||||||
.on_sign_out_others(move |_, cx| say(&everyone, "others".into(), cx))
|
.into_any_element()
|
||||||
.into_any_element()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// With this device alone there is nothing to sign out, so the list has no stop.
|
/// With this device alone there is nothing to sign out, so the list has no stop.
|
||||||
|
|||||||
+38
-152
@@ -51,22 +51,23 @@ fn a_plan_cancels_on_the_second_press_and_comes_back(cx: &mut TestAppContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn card(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
fn card(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
||||||
PaymentMethodForm::new("card", move |card, _, cx| {
|
PaymentMethodForm::new("card")
|
||||||
say(
|
.on_submit(move |card, _, cx| {
|
||||||
&owner,
|
say(
|
||||||
format!(
|
&owner,
|
||||||
"{} {} {}/{} {} {}",
|
format!(
|
||||||
card.brand.name(),
|
"{} {} {}/{} {} {}",
|
||||||
card.number,
|
card.brand.name(),
|
||||||
card.month,
|
card.number,
|
||||||
card.year,
|
card.month,
|
||||||
card.cvc,
|
card.year,
|
||||||
card.name
|
card.cvc,
|
||||||
),
|
card.name
|
||||||
cx,
|
),
|
||||||
)
|
cx,
|
||||||
})
|
)
|
||||||
.into_any_element()
|
})
|
||||||
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops: number, expiry, code, name, then Save once the card reads.
|
/// Stops: number, expiry, code, name, then Save once the card reads.
|
||||||
@@ -115,15 +116,11 @@ fn invites(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
|||||||
expired: false,
|
expired: false,
|
||||||
};
|
};
|
||||||
let [invited, again, gone] = [(); 3].map(|_| owner.clone());
|
let [invited, again, gone] = [(); 3].map(|_| owner.clone());
|
||||||
InvitationList::new(
|
InvitationList::new("invites", [sent], roles())
|
||||||
"invites",
|
.on_invite(move |email, role, _, cx| say(&invited, format!("invite {email} {role}"), cx))
|
||||||
[sent],
|
.on_resend(move |key, _, cx| say(&again, format!("resend {key}"), cx))
|
||||||
roles(),
|
.on_revoke(move |key, _, cx| say(&gone, format!("revoke {key}"), cx))
|
||||||
move |email, role, _, cx| say(&invited, format!("invite {email} {role}"), cx),
|
.into_any_element()
|
||||||
move |key, _, cx| say(&again, format!("resend {key}"), cx),
|
|
||||||
move |key, _, cx| say(&gone, format!("revoke {key}"), cx),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops: the address, the role, then Resend and Revoke while Invite rests.
|
/// Stops: the address, the role, then Resend and Revoke while Invite rests.
|
||||||
@@ -152,14 +149,10 @@ fn team(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
|||||||
owner,
|
owner,
|
||||||
};
|
};
|
||||||
let removed = owner.clone();
|
let removed = owner.clone();
|
||||||
TeamMemberTable::new(
|
TeamMemberTable::new("team", [member("ada", true), member("bo", false)], roles())
|
||||||
"team",
|
.on_role(move |key, role, _, cx| say(&owner, format!("{key} {role}"), cx))
|
||||||
[member("ada", true), member("bo", false)],
|
.on_remove(move |key, _, cx| say(&removed, format!("remove {key}"), cx))
|
||||||
roles(),
|
.into_any_element()
|
||||||
move |key, role, _, cx| say(&owner, format!("{key} {role}"), cx),
|
|
||||||
move |key, _, cx| say(&removed, format!("remove {key}"), cx),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops: bo's role, then bo's Remove; the owner has neither.
|
/// Stops: bo's role, then bo's Remove; the owner has neither.
|
||||||
@@ -174,12 +167,11 @@ fn a_member_leaves_on_the_second_press_and_the_owner_stays(cx: &mut TestAppConte
|
|||||||
|
|
||||||
fn wall(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
fn wall(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
||||||
let later = owner.clone();
|
let later = owner.clone();
|
||||||
UpgradePrompt::new("wall", "Unlock history", move |_, cx| {
|
UpgradePrompt::new("wall", "Unlock history")
|
||||||
say(&owner, "upgrade".into(), cx)
|
.benefit("Every message, kept")
|
||||||
})
|
.on_upgrade(move |_, cx| say(&owner, "upgrade".into(), cx))
|
||||||
.benefit("Every message, kept")
|
.on_dismiss(move |_, cx| say(&later, "later".into(), cx))
|
||||||
.on_dismiss(move |_, cx| say(&later, "later".into(), cx))
|
.into_any_element()
|
||||||
.into_any_element()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops: Upgrade, then Not now.
|
/// Stops: Upgrade, then Not now.
|
||||||
@@ -194,11 +186,10 @@ fn a_paywall_upgrades_or_waits(cx: &mut TestAppContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn busy_card(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
fn busy_card(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
||||||
PaymentMethodForm::new("card", move |card, _, cx| {
|
PaymentMethodForm::new("card")
|
||||||
say(&owner, card.number.to_string(), cx)
|
.busy(true)
|
||||||
})
|
.on_submit(move |card, _, cx| say(&owner, card.number.to_string(), cx))
|
||||||
.busy(true)
|
.into_any_element()
|
||||||
.into_any_element()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops: number, expiry, code, name; Save rests while the owner checks the card.
|
/// Stops: number, expiry, code, name; Save rests while the owner checks the card.
|
||||||
@@ -363,10 +354,9 @@ fn a_quota_past_its_limit_draws_full(cx: &mut TestAppContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn line(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
fn line(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
||||||
UpgradePrompt::new("line", "Unlock history", move |_, cx| {
|
UpgradePrompt::new("line", "Unlock history")
|
||||||
say(&owner, "upgrade".into(), cx)
|
.on_upgrade(move |_, cx| say(&owner, "upgrade".into(), cx))
|
||||||
})
|
.into_any_element()
|
||||||
.into_any_element()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// With benefits the prompt is a paywall that lists them.
|
/// With benefits the prompt is a paywall that lists them.
|
||||||
@@ -382,107 +372,3 @@ fn a_prompt_without_benefits_stands_as_a_line(cx: &mut TestAppContext) {
|
|||||||
let (_, cx) = bench(line, cx);
|
let (_, cx) = bench(line, cx);
|
||||||
assert!(cx.debug_bounds("paywall-benefits").is_none());
|
assert!(cx.debug_bounds("paywall-benefits").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Accounts, workspaces, a provider and roles that left their lists.
|
|
||||||
fn departed(_: &Bench, _: Entity<Bench>) -> AnyElement {
|
|
||||||
let account = crate::account::Account {
|
|
||||||
key: "ada".into(),
|
|
||||||
name: "Ada".into(),
|
|
||||||
email: "ada@work.com".into(),
|
|
||||||
image: None,
|
|
||||||
};
|
|
||||||
let space = crate::account::Workspace {
|
|
||||||
key: "north".into(),
|
|
||||||
name: "North".into(),
|
|
||||||
detail: "Pro".into(),
|
|
||||||
};
|
|
||||||
let invitation = Invitation {
|
|
||||||
key: "bo".into(),
|
|
||||||
email: "bo@example.com".into(),
|
|
||||||
role: "gone".into(),
|
|
||||||
sent: "Sent 2 days ago".into(),
|
|
||||||
expired: false,
|
|
||||||
};
|
|
||||||
let member = Member {
|
|
||||||
key: "bo".into(),
|
|
||||||
name: "Bo".into(),
|
|
||||||
email: "bo@example.com".into(),
|
|
||||||
role: "gone".into(),
|
|
||||||
seen: "Active today".into(),
|
|
||||||
image: None,
|
|
||||||
owner: false,
|
|
||||||
};
|
|
||||||
div()
|
|
||||||
.w(px(360.0))
|
|
||||||
.child(crate::account::AccountSwitcher::new(
|
|
||||||
"accounts",
|
|
||||||
[account],
|
|
||||||
"gone",
|
|
||||||
|_, _, _| {},
|
|
||||||
))
|
|
||||||
.child(crate::account::WorkspaceSwitcher::new(
|
|
||||||
"spaces",
|
|
||||||
[space],
|
|
||||||
"gone",
|
|
||||||
|_, _, _| {},
|
|
||||||
))
|
|
||||||
.child(
|
|
||||||
crate::account::OAuthButtons::new("oauth", [("github", "GitHub")], |_, _, _| {})
|
|
||||||
.busy("gone"),
|
|
||||||
)
|
|
||||||
.child(InvitationList::new(
|
|
||||||
"invites",
|
|
||||||
[invitation],
|
|
||||||
roles(),
|
|
||||||
|_, _, _, _| {},
|
|
||||||
|_, _, _| {},
|
|
||||||
|_, _, _| {},
|
|
||||||
))
|
|
||||||
.child(TeamMemberTable::new(
|
|
||||||
"team",
|
|
||||||
[member],
|
|
||||||
roles(),
|
|
||||||
|_, _, _, _| {},
|
|
||||||
|_, _, _| {},
|
|
||||||
))
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
fn accounts_workspaces_and_roles_that_left_draw(cx: &mut TestAppContext) {
|
|
||||||
let (_, cx) = bench(departed, cx);
|
|
||||||
settle(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Invitations whose first role leaves once the bench says sent.
|
|
||||||
fn departing_role(state: &Bench, owner: Entity<Bench>) -> AnyElement {
|
|
||||||
let offered: Vec<Role> = roles()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|role| !state.sent || role.key != "member")
|
|
||||||
.collect();
|
|
||||||
InvitationList::new(
|
|
||||||
"invites",
|
|
||||||
[],
|
|
||||||
offered,
|
|
||||||
move |email, role, _, cx| say(&owner, format!("invite {email} {role}"), cx),
|
|
||||||
|_, _, _| {},
|
|
||||||
|_, _, _| {},
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
fn an_invitation_as_a_role_that_left_sends_nothing(cx: &mut TestAppContext) {
|
|
||||||
let (host, cx) = bench(departing_role, cx);
|
|
||||||
tab(1, cx);
|
|
||||||
write("cy@example.com", cx);
|
|
||||||
host.update(cx, |state, cx| {
|
|
||||||
state.sent = true;
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
settle(cx);
|
|
||||||
tap("enter", cx);
|
|
||||||
tab(3, cx);
|
|
||||||
tap("space", cx);
|
|
||||||
assert_eq!(said(&host, cx), Vec::<String>::new());
|
|
||||||
}
|
|
||||||
@@ -6,15 +6,16 @@ use crate::account::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
fn login(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
fn login(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
||||||
LoginForm::new("login", move |login, _, cx| {
|
LoginForm::new("login")
|
||||||
say(
|
.on_submit(move |login, _, cx| {
|
||||||
&owner,
|
say(
|
||||||
format!("{} {} {}", login.email, login.password, login.remember),
|
&owner,
|
||||||
cx,
|
format!("{} {} {}", login.email, login.password, login.remember),
|
||||||
)
|
cx,
|
||||||
})
|
)
|
||||||
.on_forgot(|_, _| {})
|
})
|
||||||
.into_any_element()
|
.on_forgot(|_, _| {})
|
||||||
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops: email, password, its eye, the box, the forgotten password, Sign in.
|
/// Stops: email, password, its eye, the box, the forgotten password, Sign in.
|
||||||
@@ -40,15 +41,16 @@ fn sign_in_waits_for_an_address_and_a_password_and_enter_sends(cx: &mut TestAppC
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn signup(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
fn signup(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
||||||
SignupForm::new("signup", move |signup, _, cx| {
|
SignupForm::new("signup")
|
||||||
say(
|
.terms("I agree to the terms")
|
||||||
&owner,
|
.on_submit(move |signup, _, cx| {
|
||||||
format!("{} {} {}", signup.name, signup.email, signup.password),
|
say(
|
||||||
cx,
|
&owner,
|
||||||
)
|
format!("{} {} {}", signup.name, signup.email, signup.password),
|
||||||
})
|
cx,
|
||||||
.terms("I agree to the terms")
|
)
|
||||||
.into_any_element()
|
})
|
||||||
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops: name, email, password, its eye, the terms, then Create account once it holds.
|
/// Stops: name, email, password, its eye, the terms, then Create account once it holds.
|
||||||
@@ -78,11 +80,8 @@ fn sign_up_waits_for_every_rule_and_the_terms(cx: &mut TestAppContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn providers(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
|
fn providers(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
|
||||||
let buttons = OAuthButtons::new(
|
let buttons = OAuthButtons::new("oauth", [("github", "GitHub"), ("google", "Google")])
|
||||||
"oauth",
|
.on_pick(move |key, _, cx| say(&owner, key.to_string(), cx));
|
||||||
[("github", "GitHub"), ("google", "Google")],
|
|
||||||
move |key, _, cx| say(&owner, key.to_string(), cx),
|
|
||||||
);
|
|
||||||
match bench.sent {
|
match bench.sent {
|
||||||
true => buttons.busy("github").into_any_element(),
|
true => buttons.busy("github").into_any_element(),
|
||||||
false => buttons.into_any_element(),
|
false => buttons.into_any_element(),
|
||||||
@@ -114,18 +113,19 @@ fn a_provider_hands_its_key_and_rests_while_one_is_on_its_way(cx: &mut TestAppCo
|
|||||||
|
|
||||||
fn magic(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
|
fn magic(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
|
||||||
let other = owner.clone();
|
let other = owner.clone();
|
||||||
MagicLinkForm::new("magic", move |address, _, cx| {
|
MagicLinkForm::new("magic")
|
||||||
say(&owner, format!("send {address}"), cx);
|
.sent(bench.sent)
|
||||||
owner.update(cx, |bench, _| bench.sent = true);
|
.on_send(move |address, _, cx| {
|
||||||
})
|
say(&owner, format!("send {address}"), cx);
|
||||||
.sent(bench.sent)
|
owner.update(cx, |bench, _| bench.sent = true);
|
||||||
.on_other(move |_, cx| {
|
|
||||||
other.update(cx, |bench, cx| {
|
|
||||||
bench.sent = false;
|
|
||||||
cx.notify();
|
|
||||||
})
|
})
|
||||||
})
|
.on_other(move |_, cx| {
|
||||||
.into_any_element()
|
other.update(cx, |bench, cx| {
|
||||||
|
bench.sent = false;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops: email, then the link button; once sent, Send again and Use another email.
|
/// Stops: email, then the link button; once sent, Send again and Use another email.
|
||||||
@@ -151,12 +151,11 @@ fn a_link_goes_to_the_address_and_again_on_request(cx: &mut TestAppContext) {
|
|||||||
|
|
||||||
fn forgot(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
|
fn forgot(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
|
||||||
let back = owner.clone();
|
let back = owner.clone();
|
||||||
ForgotPassword::new("forgot", move |address, _, cx| {
|
ForgotPassword::new("forgot")
|
||||||
say(&owner, format!("reset {address}"), cx)
|
.sent(bench.sent)
|
||||||
})
|
.on_send(move |address, _, cx| say(&owner, format!("reset {address}"), cx))
|
||||||
.sent(bench.sent)
|
.on_back(move |_, cx| say(&back, "back".into(), cx))
|
||||||
.on_back(move |_, cx| say(&back, "back".into(), cx))
|
.into_any_element()
|
||||||
.into_any_element()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops: email, then Back to sign in while the send rests.
|
/// Stops: email, then Back to sign in while the send rests.
|
||||||
@@ -173,11 +172,10 @@ fn a_reset_waits_for_an_address_and_offers_the_way_back(cx: &mut TestAppContext)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn factor(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
|
fn factor(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
|
||||||
TwoFactorInput::new("factor", move |code, _, cx| {
|
TwoFactorInput::new("factor")
|
||||||
say(&owner, code.to_string(), cx)
|
.failed(bench.failed)
|
||||||
})
|
.on_code(move |code, _, cx| say(&owner, code.to_string(), cx))
|
||||||
.failed(bench.failed)
|
.into_any_element()
|
||||||
.into_any_element()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops: the code.
|
/// Stops: the code.
|
||||||
@@ -209,11 +207,10 @@ fn sign_in_waits_for_a_password(cx: &mut TestAppContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn busy_login(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
fn busy_login(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
||||||
LoginForm::new("login", move |login, _, cx| {
|
LoginForm::new("login")
|
||||||
say(&owner, login.email.to_string(), cx)
|
.busy(true)
|
||||||
})
|
.on_submit(move |login, _, cx| say(&owner, login.email.to_string(), cx))
|
||||||
.busy(true)
|
.into_any_element()
|
||||||
.into_any_element()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops: email, password, its eye, the box; Sign in rests while busy, so a fifth Tab wraps to the email.
|
/// Stops: email, password, its eye, the box; Sign in rests while busy, so a fifth Tab wraps to the email.
|
||||||
@@ -251,13 +248,12 @@ fn sign_up_waits_for_a_name(cx: &mut TestAppContext) {
|
|||||||
|
|
||||||
fn busy_sent(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
fn busy_sent(_: &Bench, owner: Entity<Bench>) -> AnyElement {
|
||||||
let other = owner.clone();
|
let other = owner.clone();
|
||||||
MagicLinkForm::new("magic", move |address, _, cx| {
|
MagicLinkForm::new("magic")
|
||||||
say(&owner, format!("send {address}"), cx)
|
.sent(true)
|
||||||
})
|
.busy(true)
|
||||||
.sent(true)
|
.on_send(move |address, _, cx| say(&owner, format!("send {address}"), cx))
|
||||||
.busy(true)
|
.on_other(move |_, cx| say(&other, "other".into(), cx))
|
||||||
.on_other(move |_, cx| say(&other, "other".into(), cx))
|
.into_any_element()
|
||||||
.into_any_element()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// While a link is on its way Send again rests, so the first stop is Use another email.
|
/// While a link is on its way Send again rests, so the first stop is Use another email.
|
||||||
|
|||||||
+11
-8
@@ -20,22 +20,18 @@ pub struct UpgradePrompt {
|
|||||||
title: SharedString,
|
title: SharedString,
|
||||||
body: Option<SharedString>,
|
body: Option<SharedString>,
|
||||||
benefits: Vec<SharedString>,
|
benefits: Vec<SharedString>,
|
||||||
on_upgrade: Run,
|
on_upgrade: Option<Run>,
|
||||||
on_dismiss: Option<Run>,
|
on_dismiss: Option<Run>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpgradePrompt {
|
impl UpgradePrompt {
|
||||||
pub fn new(
|
pub fn new(id: impl Into<ElementId>, title: impl Into<SharedString>) -> Self {
|
||||||
id: impl Into<ElementId>,
|
|
||||||
title: impl Into<SharedString>,
|
|
||||||
on_upgrade: impl Fn(&mut Window, &mut App) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
title: title.into(),
|
title: title.into(),
|
||||||
body: None,
|
body: None,
|
||||||
benefits: Vec::new(),
|
benefits: Vec::new(),
|
||||||
on_upgrade: Rc::new(on_upgrade),
|
on_upgrade: None,
|
||||||
on_dismiss: None,
|
on_dismiss: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,6 +47,11 @@ impl UpgradePrompt {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_upgrade(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
||||||
|
self.on_upgrade = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Shows "Not now".
|
/// Shows "Not now".
|
||||||
pub fn on_dismiss(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
pub fn on_dismiss(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
||||||
self.on_dismiss = Some(Rc::new(handler));
|
self.on_dismiss = Some(Rc::new(handler));
|
||||||
@@ -61,7 +62,9 @@ impl UpgradePrompt {
|
|||||||
impl RenderOnce for UpgradePrompt {
|
impl RenderOnce for UpgradePrompt {
|
||||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let id = self.id;
|
let id = self.id;
|
||||||
let upgrade = self.on_upgrade;
|
let upgrade = self
|
||||||
|
.on_upgrade
|
||||||
|
.unwrap_or_else(|| panic!("upgrade prompt {id:?} has no on_upgrade"));
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let wall = !self.benefits.is_empty();
|
let wall = !self.benefits.is_empty();
|
||||||
let actions = div()
|
let actions = div()
|
||||||
|
|||||||
@@ -99,10 +99,7 @@ impl SandboxStatus {
|
|||||||
|
|
||||||
/// Memory in use and its limit, in bytes.
|
/// Memory in use and its limit, in bytes.
|
||||||
pub fn memory(mut self, used: u64, limit: u64) -> Self {
|
pub fn memory(mut self, used: u64, limit: u64) -> Self {
|
||||||
assert!(limit > 0, "memory {used} of no limit");
|
assert!(limit > 0 && used <= limit, "memory {used} of {limit}");
|
||||||
if used > limit {
|
|
||||||
log::error!("environment: memory {used} past its limit {limit}; the meter fills");
|
|
||||||
}
|
|
||||||
self.memory = Some((used, limit));
|
self.memory = Some((used, limit));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -176,7 +173,7 @@ impl RenderOnce for SandboxStatus {
|
|||||||
Meter::new(
|
Meter::new(
|
||||||
(self.id.clone(), "memory"),
|
(self.id.clone(), "memory"),
|
||||||
"Memory",
|
"Memory",
|
||||||
used.min(limit) as f32 / limit as f32,
|
used as f32 / limit as f32,
|
||||||
)
|
)
|
||||||
.detail(format!(
|
.detail(format!(
|
||||||
"{} of {}",
|
"{} of {}",
|
||||||
|
|||||||
+10
-12
@@ -166,7 +166,7 @@ pub struct Checkpoint {
|
|||||||
pub struct CheckpointList {
|
pub struct CheckpointList {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
checkpoints: Vec<Checkpoint>,
|
checkpoints: Vec<Checkpoint>,
|
||||||
current: Option<usize>,
|
current: usize,
|
||||||
on_rewind: Option<Pick>,
|
on_rewind: Option<Pick>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,17 +178,15 @@ impl CheckpointList {
|
|||||||
current: usize,
|
current: usize,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let checkpoints: Vec<Checkpoint> = checkpoints.into_iter().collect();
|
let checkpoints: Vec<Checkpoint> = checkpoints.into_iter().collect();
|
||||||
let held = current < checkpoints.len();
|
assert!(
|
||||||
if !held {
|
current < checkpoints.len(),
|
||||||
log::error!(
|
"checkpoint {current} of {}",
|
||||||
"checkpoints: checkpoint {current} of {}; none current",
|
checkpoints.len()
|
||||||
checkpoints.len()
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
checkpoints,
|
checkpoints,
|
||||||
current: held.then_some(current),
|
current,
|
||||||
on_rewind: None,
|
on_rewind: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,7 +210,7 @@ impl RenderOnce for CheckpointList {
|
|||||||
let rewind = self
|
let rewind = self
|
||||||
.on_rewind
|
.on_rewind
|
||||||
.clone()
|
.clone()
|
||||||
.filter(|_| current != Some(ix))
|
.filter(|_| ix != current)
|
||||||
.map(|rewind| {
|
.map(|rewind| {
|
||||||
Button::new((self.id.clone(), format!("rewind-{ix}")), "Rewind to here")
|
Button::new((self.id.clone(), format!("rewind-{ix}")), "Rewind to here")
|
||||||
.variant(ButtonVariant::Ghost)
|
.variant(ButtonVariant::Ghost)
|
||||||
@@ -235,12 +233,12 @@ impl RenderOnce for CheckpointList {
|
|||||||
.items_center()
|
.items_center()
|
||||||
.gap_x_2()
|
.gap_x_2()
|
||||||
.child(plural(point.files as u64, "file", "files"))
|
.child(plural(point.files as u64, "file", "files"))
|
||||||
.when(current == Some(ix), |meta| {
|
.when(ix == current, |meta| {
|
||||||
meta.child(div().text_color(colors.accent).child("Here now"))
|
meta.child(div().text_color(colors.accent).child("Here now"))
|
||||||
})
|
})
|
||||||
.children(rewind),
|
.children(rewind),
|
||||||
);
|
);
|
||||||
timeline.item(if current == Some(ix) {
|
timeline.item(if ix == current {
|
||||||
item.tone(Tone::Accent)
|
item.tone(Tone::Accent)
|
||||||
} else {
|
} else {
|
||||||
item
|
item
|
||||||
|
|||||||
+6
-12
@@ -71,7 +71,7 @@ pub struct BrowserPreview {
|
|||||||
id: ElementId,
|
id: ElementId,
|
||||||
url: SharedString,
|
url: SharedString,
|
||||||
frames: Vec<SharedString>,
|
frames: Vec<SharedString>,
|
||||||
shown: Option<usize>,
|
shown: usize,
|
||||||
ratio: f32,
|
ratio: f32,
|
||||||
loading: bool,
|
loading: bool,
|
||||||
on_show: Option<Pick>,
|
on_show: Option<Pick>,
|
||||||
@@ -86,18 +86,12 @@ impl BrowserPreview {
|
|||||||
shown: usize,
|
shown: usize,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let frames: Vec<SharedString> = frames.into_iter().map(Into::into).collect();
|
let frames: Vec<SharedString> = frames.into_iter().map(Into::into).collect();
|
||||||
let held = shown < frames.len();
|
assert!(shown < frames.len(), "frame {shown} of {}", frames.len());
|
||||||
if !held {
|
|
||||||
log::error!(
|
|
||||||
"browser preview: frame {shown} of {}; none shown",
|
|
||||||
frames.len()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
url: url.into(),
|
url: url.into(),
|
||||||
frames,
|
frames,
|
||||||
shown: held.then_some(shown),
|
shown,
|
||||||
ratio: FRAME,
|
ratio: FRAME,
|
||||||
loading: false,
|
loading: false,
|
||||||
on_show: None,
|
on_show: None,
|
||||||
@@ -176,7 +170,7 @@ impl RenderOnce for BrowserPreview {
|
|||||||
.h(thumb)
|
.h(thumb)
|
||||||
.rounded(theme.radius(Radius::Sm))
|
.rounded(theme.radius(Radius::Sm))
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(if shown == Some(ix) {
|
.border_color(if ix == shown {
|
||||||
colors.accent
|
colors.accent
|
||||||
} else {
|
} else {
|
||||||
colors.border
|
colors.border
|
||||||
@@ -211,7 +205,7 @@ impl RenderOnce for BrowserPreview {
|
|||||||
.when(strip.is_none(), |frame| {
|
.when(strip.is_none(), |frame| {
|
||||||
frame.rounded_b(theme.radius(Radius::Lg))
|
frame.rounded_b(theme.radius(Radius::Lg))
|
||||||
})
|
})
|
||||||
.children(shown.map(|shown| {
|
.child({
|
||||||
let image =
|
let image =
|
||||||
Image::new((self.id.clone(), "frame"), source(&self.frames[shown]))
|
Image::new((self.id.clone(), "frame"), source(&self.frames[shown]))
|
||||||
.size_full();
|
.size_full();
|
||||||
@@ -220,7 +214,7 @@ impl RenderOnce for BrowserPreview {
|
|||||||
} else {
|
} else {
|
||||||
image
|
image
|
||||||
}
|
}
|
||||||
})),
|
}),
|
||||||
)
|
)
|
||||||
.children(strip)
|
.children(strip)
|
||||||
}
|
}
|
||||||
|
|||||||
Loaded 100 of 551 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user