diff --git a/.gitattributes b/.gitattributes index f6cc464..f9bac11 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,6 +8,11 @@ *.py text eol=lf *.sh text eol=lf *.md text eol=lf +# The GPUI fork patches are a build input now, not just an audit artifact: +# `.shots/gpui_patches.py --materialize` applies them before every cargo +# command on all three CI hosts. `* text=auto` above would hand the Windows +# runner CRLF copies, so pin them the way every other source file is pinned. +*.patch text eol=lf # Oxfmt checks LF endings, including on Windows checkouts. web/**/*.ts text eol=lf web/**/*.tsx text eol=lf diff --git a/.github/actions/rust-env/action.yml b/.github/actions/rust-env/action.yml index 9409e1e..ed21684 100644 --- a/.github/actions/rust-env/action.yml +++ b/.github/actions/rust-env/action.yml @@ -19,6 +19,33 @@ inputs: runs: using: composite steps: + # The patched GPUI sources are not in the repository; five + # `[patch.crates-io]` entries point into the gitignored `.vendor/` tree that + # `.shots/gpui_patches.py --materialize` builds from the pinned published + # packages plus `docs/upstream/patches/*.patch`. Cargo fails at manifest + # load when a patch path is missing ("failed to load source for + # dependency"), so this has to happen before the first cargo command in the + # job -- which is why it lives in the shared action rather than in each + # job, where one omission would break a whole leg. + # + # It also has to happen before `Swatinem/rust-cache` below: that action runs + # `cargo metadata` of its own to key the cache, and that call is a cargo + # command like any other. The cost is that a cold job downloads the five + # `.crate` files before the registry cache is restored; they are small next + # to the dependency graph the cache is actually for, and a warm `.vendor/` + # check is a hash comparison that finishes in under a tenth of a second. + # + # `setup-python` rather than the runner's default interpreter: `python3` is + # not on PATH on the Windows runners, and pinning the version keeps the + # three OS legs on one interpreter. + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Materialize the patched GPUI sources + shell: bash + run: python .shots/gpui_patches.py --materialize + - name: Disable Windows Defender and configure exclusions # Windows-only. GitHub Actions Windows runners run Defender by default. # Scanning every .rlib, .rmeta, .exe, and cache tar extraction causes diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0b4a00..6ba871d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,6 +48,19 @@ jobs: working-directory: web run: pnpm install --frozen-lockfile + # `cargo fmt --all` is a cargo command, so it needs the patched GPUI + # sources materialized like every other one -- and this job needs it for a + # second reason besides `[patch.crates-io]`: + # `crates/herogpui-components/tests/rounded_clip_shaders.rs` includes the + # renderer's own `shaders.rs` by `#[path]`, so rustfmt fails on a missing + # module before cargo ever resolves a dependency. + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Materialize the patched GPUI sources + run: python .shots/gpui_patches.py --materialize + - name: Format check run: cargo fmt --all -- --check @@ -146,15 +159,23 @@ jobs: with: tool: cargo-machete + # The patches are the only record of the fork, so this is the gate that + # keeps them honest: each one has to apply to the pristine published + # package with no fuzz and no offset, and reproduce the `.vendor/` tree + # the rest of this job just built against, byte for byte. A hand-edit + # under `.vendor/` that never made it back into a patch fails here. - name: Verify GPUI renderer patches - run: python .shots/gpui_patches.py --check + run: | + python .shots/gpui_patches.py --self-test + python .shots/gpui_patches.py --check # Unused-dependency gate. cargo-machete reads every manifest in the # tree and greps the crate sources for each dependency's name -- no # build, not even a toolchain -- so it answers in seconds and sits - # before the clippy step below, which pays for a full compile. The - # vendored `crates/gpui_web` fork is clean under it too (verified by - # running machete on that directory directly). + # before the clippy step below, which pays for a full compile. It walks + # the working directory rather than the dependency graph, and the + # materialized GPUI sources under `.vendor/` are hidden from it by the + # leading dot, so it sees only HeroGPUI's own manifests. # # There is no ignore list today, deliberately: the tree is clean with # none (verified against cargo-machete 0.9.2, what install-action @@ -164,7 +185,7 @@ jobs: # that crate's manifest, or the same table under # `[workspace.metadata.cargo-machete]` in the root. Every entry must # carry its reason in a comment beside it, the way - # `[package.metadata.cargo-shear]` does in crates/gpui_web; an entry + # `[package.metadata.cargo-shear]` does upstream; an entry # without a reason is a hole in the gate. - name: Unused dependencies (cargo-machete) run: cargo machete @@ -338,10 +359,12 @@ jobs: # channel # --> .../wasm_thread-0.3.3/src/lib.rs:1:1 # - # `multithreaded` is now off in the vendored fork's own manifest - # (`crates/gpui_web/Cargo.toml`, which `[patch.crates-io]` substitutes -- - # the only place it can be switched, because `gpui_platform`'s wasm32 - # edge enables default features and cargo unions feature sets). With it + # `multithreaded` is now off in the forked manifest itself -- a + # `default = []` hunk in `docs/upstream/patches/gpui-pre-web-0.3.3.patch`, + # materialized into `.vendor/gpui-pre-web-0.3.3/Cargo.toml`, which + # `[patch.crates-io]` substitutes. That is the only place it can be + # switched, because `gpui_platform`'s wasm32 edge enables default features + # and cargo unions feature sets. With it # off, `wasm_thread` is not in the graph and stable builds this target, # profile included. Nothing was using it: `crates/herogpui-web` starts the # app with `single_threaded_web()`, and the multi-threaded platform needs @@ -350,6 +373,20 @@ jobs: - name: Add the wasm32 target to the pinned toolchain run: rustup target add wasm32-unknown-unknown + # This job builds its own environment instead of using + # `./.github/actions/rust-env`, so it repeats that action's + # materialization step. `gpui-pre-web` is the package that matters most + # here: its `default = []` hunk is what keeps `wasm_thread` -- and with it + # a nightly toolchain -- out of the wasm32 graph, and it now lives only in + # `docs/upstream/patches/gpui-pre-web-0.3.3.patch`. Before + # `Swatinem/rust-cache`, which runs a `cargo metadata` of its own. + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Materialize the patched GPUI sources + run: python .shots/gpui_patches.py --materialize + - uses: Swatinem/rust-cache@v2 with: key: wasm32 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a98c989..b908f20 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,6 +50,17 @@ jobs: libxcb-xfixes0-dev \ libfontconfig-dev \ libfreetype-dev + # The patched GPUI sources are not in the repository -- the five + # `[patch.crates-io]` paths under `.vendor/` are built from + # `docs/upstream/patches/*.patch` by this command, and cargo aborts at + # manifest load without them. Before `Swatinem/rust-cache`, which runs a + # `cargo metadata` of its own. + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Materialize the patched GPUI sources + shell: bash + run: python .shots/gpui_patches.py --materialize - uses: Swatinem/rust-cache@v2 - run: cargo test --workspace --locked @@ -93,6 +104,17 @@ jobs: libxcb-xfixes0-dev \ libfontconfig-dev \ libfreetype-dev + # The patched GPUI sources are not in the repository -- the five + # `[patch.crates-io]` paths under `.vendor/` are built from + # `docs/upstream/patches/*.patch` by this command, and cargo aborts at + # manifest load without them. Before `Swatinem/rust-cache`, which runs a + # `cargo metadata` of its own. + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Materialize the patched GPUI sources + shell: bash + run: python .shots/gpui_patches.py --materialize - uses: Swatinem/rust-cache@v2 - run: cargo build --locked --release -p herogpui-gallery - name: Stage release binary diff --git a/.gitignore b/.gitignore index 696db12..e18c2f6 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,9 @@ web/.verify/ .vercel .env* -# Vendored gpui-pre forks are standalone workspaces and may be built in -# place; their target output is not source. -crates/gpui_pre*/target/ +# The patched GPUI sources. The repository records the fork as patches under +# docs/upstream/patches/; `.shots/gpui_patches.py --materialize` rebuilds this +# tree from the pinned published packages plus those patches, and every cargo +# command (and rust-analyzer) needs it to exist first. ~153,000 lines of +# dependency source that is not ours and is never committed. +/.vendor/ diff --git a/.shots/fence_audit.py b/.shots/fence_audit.py index c1e624a..69eb019 100644 --- a/.shots/fence_audit.py +++ b/.shots/fence_audit.py @@ -53,17 +53,6 @@ SCAN_ROOT = 'crates' -# These are exact-version gpui-pre source forks selected through the workspace -# `[patch.crates-io]` block. Their rustdoc belongs to the external dependency; -# the application fence contract is audited in HeroGPUI-owned crates below. -EXTERNAL_GPUI_DIRS = { - 'gpui_pre', - 'gpui_pre_apple', - 'gpui_pre_wgpu', - 'gpui_pre_windows', - 'gpui_web', -} - # (path relative to the repository root, why running it is not an option). # Every entry is asserted to still match a `no_run` fence in that file, so an # example that stops needing the exemption fails this audit until its entry @@ -128,12 +117,11 @@ def rust_files(root): """Every `.rs` file under `root`, sorted, skipping build output.""" found = [] for base, dirs, names in os.walk(root): - dirs[:] = [ - d for d in dirs - if d != 'target' - and not d.startswith('.') - and d not in EXTERNAL_GPUI_DIRS - ] + # The patched gpui-pre forks used to sit under `crates/` and had to be + # named here to keep an external dependency's rustdoc out of this + # contract. They are materialized into `.vendor/` now, which the + # dot-prefix rule already excludes -- and `SCAN_ROOT` does not reach it. + dirs[:] = [d for d in dirs if d != 'target' and not d.startswith('.')] for name in names: if name.endswith('.rs'): found.append(os.path.join(base, name).replace(os.sep, '/')) diff --git a/.shots/gpui_patches.py b/.shots/gpui_patches.py index 8583591..a70c87f 100644 --- a/.shots/gpui_patches.py +++ b/.shots/gpui_patches.py @@ -1,8 +1,29 @@ #!/usr/bin/env python3 -"""Record and verify renderer forks against their exact published packages.""" +"""Materialize, record and verify the renderer forks of the published GPUI packages. + +The repository carries the *patches*, never the patched sources. Five published +`gpui-pre*` packages are forked (`docs/upstream/gpui-rounded-content-mask.md` +and `docs/upstream/gpui-web-scroll-and-ime.md` say why), and each fork is stored +as a single unified patch under `docs/upstream/patches/`. The build materializes +the patched tree into the gitignored `.vendor/` root, which the workspace's +`[patch.crates-io]` table points at. + + python3 .shots/gpui_patches.py --materialize # bootstrap: required before *any* cargo command + python3 .shots/gpui_patches.py --check # patch still reproduces `.vendor/` exactly + python3 .shots/gpui_patches.py --write # re-record a patch after editing `.vendor/` + python3 .shots/gpui_patches.py --self-test # the recorder's own regressions + +A missing `.vendor/` tree is not a soft failure: cargo aborts at manifest load +with "failed to load source for dependency" because a `[patch.crates-io]` path +does not exist. That is why `--materialize` runs first in every CI job that +touches cargo, and why a fresh clone must run it before `cargo`, `rustup` +component tooling, or rust-analyzer. +""" import argparse import difflib +import hashlib +import json import os from pathlib import Path import shutil @@ -11,11 +32,26 @@ import tomllib ROOT = Path(__file__).resolve().parents[1] -PACKAGES = ("gpui-pre", "gpui-pre-apple", "gpui-pre-wgpu", "gpui-pre-windows") +PATCH_DIR = ROOT / "docs/upstream/patches" +PACKAGES = ( + "gpui-pre", + "gpui-pre-apple", + "gpui-pre-web", + "gpui-pre-wgpu", + "gpui-pre-windows", +) +# Written into each materialized tree so a warm run can decide in milliseconds +# whether the tree is already the patch applied to the pinned published source. +STAMP = ".herogpui-materialized.json" # Cargo uses the workspace lockfile. These registry/cache files are not fork -# code: `Cargo.toml.orig` is the packager's pre-normalization backup and is -# gitignored, so it exists only in an unpacked registry copy, never in the fork. -IGNORED = {".cargo-checksum.json", ".cargo-ok", "Cargo.lock", "Cargo.toml.orig"} +# code: `Cargo.toml.orig` is the packager's pre-normalization backup and +# `.cargo-ok`/`.cargo-checksum.json` are cargo's own unpack bookkeeping, so +# none of them belongs in a materialized tree or in a recorded patch. +IGNORED = {".cargo-checksum.json", ".cargo-ok", "Cargo.lock", "Cargo.toml.orig", STAMP} + + +def digest(payload): + return hashlib.sha256(payload).hexdigest() def files(root): @@ -28,6 +64,15 @@ def files(root): } +def tree_digest(root): + return digest( + json.dumps( + {name: digest(content) for name, content in files(root).items()}, + sort_keys=True, + ).encode() + ) + + def patch_text(base, fork, package): original, changed = files(base), files(fork) output = [] @@ -46,6 +91,146 @@ def patch_text(base, fork, package): return "".join(output) +def parse_patch(body, package): + """Split a `patch_text` unified diff into `(path, deleted, created, hunks)` per file. + + Only the dialect `patch_text` emits is accepted -- no index lines, no + renames, no binary hunks, one `---`/`+++` pair per file. Anything else is a + hand-edited patch, which is exactly what this refuses to guess at. + """ + lines = body.splitlines(keepends=True) + files_, index = [], 0 + while index < len(lines): + line = lines[index] + if not line.startswith("--- ") or index + 1 >= len(lines): + raise ValueError(f"unexpected line {index + 1} in the patch for {package}: {line!r}") + source, target = line[4:].rstrip("\n"), lines[index + 1][4:].rstrip("\n") + if not lines[index + 1].startswith("+++ "): + raise ValueError(f"patch for {package} has a `---` line with no `+++` line") + index += 2 + deleted, created = target == "/dev/null", source == "/dev/null" + named = source if deleted else target + prefix = "a/" if deleted else "b/" + if not named.startswith(f"{prefix}{package}/") or deleted and created: + raise ValueError(f"patch for {package} names a foreign file: {named!r}") + path, hunks = named[len(prefix) + len(package) + 1:], [] + while index < len(lines) and lines[index].startswith("@@ "): + header = lines[index].split(" ") + old_start, old_count = hunk_range(header[1]) + _, new_count = hunk_range(header[2]) + index, body_lines = index + 1, [] + # The two counts say exactly how many body lines this hunk has, so + # the scan never has to guess where it ends -- a `-` line that opens + # the next file's `--- a/...` header cannot be mistaken for a removal. + seen_old = seen_new = 0 + while seen_old < old_count or seen_new < new_count: + if index >= len(lines): + raise ValueError(f"patch for {package} ends inside a hunk for {path}") + entry = lines[index] + index += 1 + if entry.startswith("\\"): + body_lines[-1] = strip_final_newline(body_lines[-1]) + continue + if entry == "\n": + entry = " \n" # an empty context line, if an editor ate the space + tag, text = entry[0], entry[1:] + seen_old += tag in " -" + seen_new += tag in " +" + if tag not in " -+": + raise ValueError(f"patch for {package} has an unreadable hunk line: {entry!r}") + body_lines.append((tag, text)) + # A `\ No newline at end of file` marker belongs to the line above + # it, whose recorded text carries a newline the file does not have. + if index < len(lines) and lines[index].startswith("\\"): + body_lines[-1] = strip_final_newline(body_lines[-1]) + index += 1 + hunks.append((old_start, old_count, body_lines)) + if not hunks: + raise ValueError(f"patch for {package} has no hunk for {path}") + files_.append((path, deleted, created, hunks)) + return files_ + + +def hunk_range(field): + """`-12,3` or `+12` into `(start, count)`; a bare number means one line.""" + start, _, count = field[1:].partition(",") + return int(start), int(count) if count else 1 + + +def strip_final_newline(entry): + tag, text = entry + return tag, text[:-1] if text.endswith("\n") else text + + +def apply_hunks(original, hunks, path): + """Replay `hunks` against `original`, requiring an exact positional match. + + There is no search and no fallback, so there is nothing for a fuzz factor or + a line offset to hide in: every context and removed line has to be the line + the patch says it is, or the whole application fails. + """ + result, cursor = [], 0 + for start, count, body in hunks: + # difflib writes a zero-length range as the line *before* the insertion + # point, so the 0-based index is `start` there and `start - 1` otherwise. + index = start if count == 0 else start - 1 + if index < cursor or index > len(original): + raise ValueError(f"hunk at line {start} of {path} is out of order or past the file end") + result.extend(original[cursor:index]) + cursor = index + for tag, text in body: + if tag == "+": + result.append(text) + continue + if cursor >= len(original) or original[cursor] != text: + found = original[cursor] if cursor < len(original) else "" + raise ValueError( + f"{path}: line {cursor + 1} is {found!r}, but the patch expects {text!r}" + ) + if tag == " ": + result.append(text) + cursor += 1 + result.extend(original[cursor:]) + return result + + +def apply_patch(tree, patch, package): + """Apply `patch` inside `tree`, rejecting anything short of an exact application. + + Deliberately not a `patch(1)` subprocess. This runs on all three CI hosts + now that materialization precedes every cargo command, and the GNU/BSD + implementations disagree about deletions, whitespace and exit codes -- BSD + leaves a zero-length file where GNU unlinks it, and "applied with fuzz" is + reported on stdout rather than in the exit status, so strictness depended on + sniffing English prose. An in-process applier is the same behaviour on every + host, and `--check` proves it round-trips against the recorded diff. + """ + body = patch.read_text() + if not body: + return + for path, deleted, created, hunks in parse_patch(body, package): + target = tree / path + # A header says whether the published package has this file, so a + # rebase that adds or removes one upstream fails here rather than + # quietly producing a tree the patch no longer describes. + if target.is_file() == created: + state = "already exists" if created else "is absent" + raise ValueError(f"patch does not apply exactly: {patch}\n{path} {state} in the published source") + if deleted: + target.unlink() + continue + # Bytes, not `read_text`/`write_text`: those translate newlines, and a + # published source file with CRLF endings has to survive the round trip + # byte for byte or the recorded patch stops matching it. + original = [] if created else target.read_bytes().decode("utf-8").splitlines(keepends=True) + try: + updated = apply_hunks(original, hunks, path) + except ValueError as error: + raise ValueError(f"patch does not apply exactly: {patch}\n{error}") from error + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes("".join(updated).encode("utf-8")) + + def check_patch(base, fork, patch, package): expected = patch_text(base, fork, package) if not patch.is_file(): @@ -54,26 +239,37 @@ def check_patch(base, fork, patch, package): raise ValueError(f"stale patch: {patch}; regenerate with --write") with tempfile.TemporaryDirectory(prefix="herogpui-patch-") as temporary: replay = Path(temporary) / "source" - shutil.copytree(base, replay) - if expected: - result = subprocess.run( - ["patch", "--batch", "--forward", "-p2", "-i", str(patch.resolve())], - cwd=replay, capture_output=True, text=True, - ) - if result.returncode or "fuzz" in result.stdout or "offset" in result.stdout: - raise ValueError(f"patch does not apply exactly: {patch}\n{result.stdout}{result.stderr}") - # BSD patch leaves a zero-length file for a /dev/null deletion. Remove - # only explicitly deleted files; -E would also erase intentional empties. - original, changed = files(base), files(fork) - for name in original.keys() - changed.keys(): - deleted = replay / name - if deleted.is_file() and deleted.stat().st_size == 0: - deleted.unlink() - if files(replay) != changed: - raise ValueError(f"patch replay differs from local fork: {package}") + copy_pristine(base, replay) + apply_patch(replay, patch, package) + if files(replay) != files(fork): + raise ValueError(f"patch replay differs from the materialized tree: {package}") + + +def copy_pristine(base, destination): + """Copy the published source into a writable tree, minus cargo's bookkeeping. + + Registry unpacks are read-only, so the bytes are rewritten rather than + `copytree`d; `patch` has to be able to edit what lands here. Every file in + the published packages is mode 644 (verified against all five), so no + executable bit is carried across. + """ + destination.mkdir(parents=True) + for name, content in files(base).items(): + path = destination / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(content) + path.chmod(0o644) def registry_package(name, version): + """The pristine published source for the exact pin, from cargo's own cache. + + Nothing is downloaded by this script, so there is no unverified download to + trust: the tree returned here is one cargo unpacked itself after checking + the `.crate` tarball against the registry index checksum. On a cold cache + `cargo info` is what fetches it, outside this workspace so the patched + `[patch.crates-io]` entries cannot redirect the fetch to the fork. + """ cargo_home = Path(os.environ.get("CARGO_HOME", Path.home() / ".cargo")) candidates = sorted((cargo_home / "registry/src").glob(f"*/{name}-{version}")) candidates = [path for path in candidates if (path / "Cargo.toml").is_file()] @@ -97,6 +293,92 @@ def registry_package(name, version): return source +def write_stamp(tree, name, version, patch_sha256, upstream_sha256): + (tree / STAMP).write_text(json.dumps({ + "note": "Generated by .shots/gpui_patches.py --materialize. Not source; do not edit.", + "package": name, + "version": version, + "patch_sha256": patch_sha256, + "upstream_sha256": upstream_sha256, + "tree_sha256": tree_digest(tree), + }, indent=2) + "\n") + + +def materialize(name, version, destination, patch, package, resolve_base=registry_package): + """Make `destination` the pinned published source with `patch` applied. + + Idempotent and self-healing: a tree whose stamp still matches both the patch + and its own contents is left alone, and anything else is rebuilt from + scratch under a `.partial` name and swapped in, so an interrupted run never + leaves a half-patched tree behind for cargo to compile. + """ + if not patch.is_file(): + raise ValueError(f"missing patch: {patch}") + fingerprint = digest(patch.read_bytes()) + stamp = destination / STAMP + if stamp.is_file(): + try: + record = json.loads(stamp.read_text()) + except (json.JSONDecodeError, UnicodeDecodeError): + record = {} + if (record.get("package") == name + and record.get("version") == version + and record.get("patch_sha256") == fingerprint + and record.get("tree_sha256") == tree_digest(destination)): + return "already materialized" + base = resolve_base(name, version) + destination.parent.mkdir(parents=True, exist_ok=True) + staging = destination.parent / f".{destination.name}.partial" + shutil.rmtree(staging, ignore_errors=True) + try: + copy_pristine(base, staging) + apply_patch(staging, patch, package) + write_stamp(staging, name, version, fingerprint, tree_digest(base)) + shutil.rmtree(destination, ignore_errors=True) + staging.replace(destination) + finally: + shutil.rmtree(staging, ignore_errors=True) + return "materialized" + + +def workspace_pin(): + manifest = tomllib.loads((ROOT / "Cargo.toml").read_text()) + version = manifest["workspace"]["dependencies"]["gpui"]["version"] + if not version.startswith("="): + raise ValueError("the workspace GPUI dependency must have an exact version pin") + return manifest, version[1:] + + +def patched_path(manifest, name, version): + """Where `[patch.crates-io]` says this package lives, asserted to the convention. + + The path, the patch filename and the pin are one fact written three times, + so this refuses any `[patch.crates-io]` entry that does not spell the + materialization root the way `--materialize` writes it. + """ + configured = manifest["patch"]["crates-io"][name]["path"] + expected = f".vendor/{name}-{version}" + if configured != expected: + raise ValueError( + f"[patch.crates-io].{name} points at {configured!r}, not {expected!r}" + ) + return ROOT / configured + + +def require_materialized(destination, name, version): + if not (destination / "Cargo.toml").is_file(): + raise ValueError( + f"{destination.relative_to(ROOT).as_posix()} is not materialized; " + "run `python3 .shots/gpui_patches.py --materialize` first" + ) + package = tomllib.loads((destination / "Cargo.toml").read_text())["package"] + if package["name"] != name or package["version"] != version: + raise ValueError( + f"{destination.relative_to(ROOT).as_posix()}: materialized tree is " + f"{package['name']} {package['version']}, not the pinned {name} {version}" + ) + + def self_test(): with tempfile.TemporaryDirectory(prefix="herogpui-patch-test-") as temporary: root = Path(temporary) @@ -110,6 +392,30 @@ def self_test(): (fork / "added.rs").write_text("no trailing newline") patch.write_text(patch_text(base, fork, "example-0.3.3")) check_patch(base, fork, patch, "example-0.3.3") + + # Materialization reproduces the fork from the patch alone, and says so + # cheaply on a second run instead of rebuilding the tree. + vendor = root / "vendor" / "example-0.3.3" + resolve = lambda *_: base # noqa: E731 - one-line stub for the registry lookup + assert materialize("example", "0.3.3", vendor, patch, "example-0.3.3", resolve) == "materialized" + assert files(vendor) == files(fork), "materialized tree differs from the fork" + assert json.loads((vendor / STAMP).read_text())["package"] == "example" + assert materialize("example", "0.3.3", vendor, patch, "example-0.3.3", resolve) == "already materialized" + + # A hand-edited or half-written tree is rebuilt, not trusted. + (vendor / "changed.rs").write_text("tampered\n") + assert materialize("example", "0.3.3", vendor, patch, "example-0.3.3", resolve) == "materialized" + assert files(vendor) == files(fork), "tampered tree was not rebuilt" + + # ... and `--check` reports that tampering rather than repairing it. + (vendor / "changed.rs").write_text("tampered\n") + try: + check_patch(base, vendor, patch, "example-0.3.3") + except ValueError as error: + assert "stale patch" in str(error) + else: + raise AssertionError("a tampered materialized tree was accepted") + patch.write_text("") try: check_patch(base, fork, patch, "example-0.3.3") @@ -124,37 +430,45 @@ def self_test(): assert "missing patch" in str(error) else: raise AssertionError("a missing patch was accepted") - print("GPUI patch self-test: replay passed; stale and missing patches rejected") + try: + materialize("example", "0.3.3", vendor, patch, "example-0.3.3", resolve) + except ValueError as error: + assert "missing patch" in str(error) + else: + raise AssertionError("materialization accepted a missing patch") + print("GPUI patch self-test: materialize, replay, tamper, stale and missing patches all handled") def main(): parser = argparse.ArgumentParser(description=__doc__) mode = parser.add_mutually_exclusive_group() - mode.add_argument("--write", action="store_true", help="regenerate versioned patches") - mode.add_argument("--check", action="store_true", help="verify freshness and exact replay (default)") + mode.add_argument("--materialize", action="store_true", + help="build .vendor/ from the published sources plus the recorded patches") + mode.add_argument("--write", action="store_true", help="regenerate versioned patches from .vendor/") + mode.add_argument("--check", action="store_true", + help="verify each patch reproduces its materialized tree exactly (default)") mode.add_argument("--self-test", action="store_true") args = parser.parse_args() if args.self_test: self_test() return - manifest = tomllib.loads((ROOT / "Cargo.toml").read_text()) - version = manifest["workspace"]["dependencies"]["gpui"]["version"] - if not version.startswith("="): - raise ValueError("the workspace GPUI dependency must have an exact version pin") - version = version[1:] + manifest, version = workspace_pin() for name in PACKAGES: - fork = ROOT / manifest["patch"]["crates-io"][name]["path"] - package = tomllib.loads((fork / "Cargo.toml").read_text())["package"] - if package["name"] != name or package["version"] != version: - raise ValueError(f"{fork}: fork name/version differs from the workspace pin") + destination = patched_path(manifest, name, version) + package = f"{name}-{version}" + patch = PATCH_DIR / f"{package}.patch" + if args.materialize: + state = materialize(name, version, destination, patch, package) + print(f"{package}: {state} in {destination.relative_to(ROOT).as_posix()}") + continue + require_materialized(destination, name, version) base = registry_package(name, version) - label = f"{name}-{version}" - patch = ROOT / "docs/upstream/patches" / f"{label}.patch" if args.write: patch.parent.mkdir(parents=True, exist_ok=True) - patch.write_text(patch_text(base, fork, label)) - check_patch(base, fork, patch, label) - print(f"{label}: patch matches fork and replays exactly") + patch.write_text(patch_text(base, destination, package)) + write_stamp(destination, name, version, digest(patch.read_bytes()), tree_digest(base)) + check_patch(base, destination, patch, package) + print(f"{package}: patch applies exactly to the published source and reproduces .vendor/") if __name__ == "__main__": diff --git a/.shots/lint.ps1 b/.shots/lint.ps1 index d93cd5f..cc7faaa 100644 --- a/.shots/lint.ps1 +++ b/.shots/lint.ps1 @@ -14,11 +14,10 @@ $root = Split-Path -Parent $PSScriptRoot Push-Location $root try { # 1. Every member crate must opt in, or the policy is not what it looks like. - # Exclude vendored crates that form their own workspace roots (the GPUI - # forks); they keep upstream's lint configuration, not this workspace's. - $vendored = @('gpui_web', 'gpui_pre', 'gpui_pre_apple', 'gpui_pre_wgpu', 'gpui_pre_windows') + # Only HeroGPUI's own crates live under crates/ now: the patched GPUI + # forks are materialized into .vendor/, keep upstream's lint + # configuration, and are never walked here. $members = Get-ChildItem -Path (Join-Path $root 'crates') -Directory | - Where-Object { $_.Name -notin $vendored } | ForEach-Object { Join-Path $_.FullName 'Cargo.toml' } $members += (Join-Path $root 'gallery/Cargo.toml') diff --git a/.shots/package_audit.py b/.shots/package_audit.py index 3261930..c9d99f0 100644 --- a/.shots/package_audit.py +++ b/.shots/package_audit.py @@ -53,8 +53,8 @@ def main(): if dependency.get("package") != packages[name]: errors.append(f"{name}: must rename the {packages[name]} package") # The pin is exact on purpose. `gpui-pre-platform` requires the rest of - # the family at an exact `=` version, and the vendored `gpui-pre-web` - # fork's own version has to satisfy that same requirement or + # the family at an exact `=` version, and the materialized + # `gpui-pre-web` fork's version has to satisfy that same requirement or # `[patch.crates-io]` stops applying with no error at all. A caret let a # bare `cargo update` walk off the pin once already. if not dependency.get("version", "").startswith("="): @@ -66,9 +66,10 @@ def main(): version = requirement.lstrip("=") locked = manifest(ROOT / "Cargo.lock")["package"] # The published family remains the dependency contract of every package, - # while the workspace's `[patch.crates-io]` block substitutes local, - # version-identical renderer forks during development. The lockfile is - # therefore intentionally path-resolved for the five patched members. + # while the workspace's `[patch.crates-io]` block substitutes the + # materialized, version-identical renderer forks from `.vendor/`. The + # lockfile is therefore intentionally path-resolved for the five patched + # members. patched_gpui = { "gpui-pre", "gpui-pre-apple", @@ -92,24 +93,44 @@ def main(): if any((entry.get("source") or "").startswith("git+") for entry in locked): errors.append("lockfile still contains a git source; the crates cannot be published") - # The vendored `gpui-pre-web` fork (`crates/gpui_web`) reaches the wasm32 - # graph only through `[patch.crates-io]`, and cargo drops that override - # silently when the fork's version no longer satisfies what - # `gpui-pre-platform` asks for -- the fork's two `events.rs` hunks would - # just disappear from the browser build. Assert the version match and that - # the lock resolves the crate to the local path (no registry `source`). - fork = manifest(ROOT / "crates/gpui_web/Cargo.toml")["package"] - if version and fork.get("version") != version: - errors.append( - f"crates/gpui_web: vendored fork is {fork.get('version')!r}, " - f"not the pinned {version}; [patch.crates-io] will not apply" - ) + # No fork source is checked in: the deviation is recorded as a patch under + # docs/upstream/patches/ and `.shots/gpui_patches.py --materialize` applies + # it to the pinned published source under the gitignored `.vendor/`. So the + # version agreement this audit guards is spelled in two committed places -- + # the `[patch.crates-io]` path and the patch filename -- and both have to + # carry the pin. Cargo drops a `[patch.crates-io]` override silently when + # the substitute's version no longer satisfies what `gpui-pre-platform` + # asks for; the `gpui-pre-web` fork's two `events.rs` hunks and its + # `default = []` feature deviation would just disappear from the browser + # build, and nothing else would report it. `--check` verifies the patch + # content; this verifies the wiring, with no Rust build and no `.vendor/` + # tree, so it still runs in the parity job. + overrides = workspace.get("patch", {}).get("crates-io", {}) + for name in sorted(patched_gpui): + entry = overrides.get(name) + if not isinstance(entry, dict) or not entry.get("path"): + errors.append(f"{name}: no [patch.crates-io] path override") + continue + if entry.get("git"): + errors.append(f"{name}: [patch.crates-io] must not use a git source") + expected_path = f".vendor/{name}-{version}" + if version and entry["path"] != expected_path: + errors.append( + f"{name}: [patch.crates-io] path is {entry['path']!r}, not {expected_path!r}; " + "the override would apply the wrong version or none at all" + ) + patch_file = ROOT / "docs/upstream/patches" / f"{name}-{version}.patch" + if version and not patch_file.is_file(): + errors.append( + f"{name}: docs/upstream/patches/{name}-{version}.patch is missing; " + "nothing can materialize the fork" + ) web_entries = [entry for entry in locked if entry["name"] == "gpui-pre-web"] if len(web_entries) != 1: errors.append("gpui-pre-web: lockfile does not resolve to exactly one version") elif web_entries[0].get("source"): errors.append( - "gpui-pre-web: lockfile carries a source, so the vendored fork is not patched in" + "gpui-pre-web: lockfile carries a source, so the materialized fork is not patched in" ) elif version and web_entries[0].get("version") != version: errors.append(f"gpui-pre-web: lockfile version is not the pinned {version}") diff --git a/.shots/test_interaction_inventory.py b/.shots/test_interaction_inventory.py index 60c431b..09cf714 100644 --- a/.shots/test_interaction_inventory.py +++ b/.shots/test_interaction_inventory.py @@ -175,7 +175,7 @@ def test_snapshot_ignores_build_output_directories(self): source.parent.mkdir(parents=True, exist_ok=True) source.write_text('source') previous = SOURCE_SNAPSHOT(self.root) - artifact = self.root / 'crates/gpui_pre/target/debug/build/out/generated.rs' + artifact = self.root / 'crates/herogpui-components/target/debug/build/out/generated.rs' artifact.parent.mkdir(parents=True, exist_ok=True) artifact.write_text('generated') self.assertEqual(SOURCE_SNAPSHOT(self.root), previous) diff --git a/AGENTS.md b/AGENTS.md index 87d48b7..49eb8c7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,18 +3,45 @@ HeroGPUI is a native Rust/GPUI port of HeroUI v3.2.5. The repository targets Rust 1.98 and the published `gpui-pre` crates pinned in `Cargo.toml` and `Cargo.lock` at exactly `=0.3.3` — zed-industries' own prerelease publish -of the GPUI sources, 0.3.3 being a snapshot of `zed@5b055fa`. Use those -unpacked sources for API evidence — -`~/.cargo/registry/src/index.crates.io-*/gpui-pre-0.3.3/` — not a Zed git -checkout, not the older third-party `gpui-unofficial` republish this replaced, -and not the unrelated crates.io `gpui` 0.2.2 crate. `gpui-pre`'s version -numbers are its own; they do not track Zed release tags. The pin is exact -because `gpui-pre-platform` requires the family at an exact `=` version and the -vendored `gpui-pre-web` fork's version must satisfy that same requirement or -`[patch.crates-io]` silently stops applying; 0.3.1 and 0.3.2 additionally must -not be resolved, because `gpui-pre-macros` 0.3.1 breaks every +of the GPUI sources, 0.3.3 being a snapshot of `zed@5b055fa`. `gpui-pre`'s +version numbers are its own; they do not track Zed release tags. The pin is +exact because `gpui-pre-platform` requires the family at an exact `=` version +and the patched `gpui-pre-web` fork's version must satisfy that same +requirement or `[patch.crates-io]` silently stops applying; 0.3.1 and 0.3.2 +additionally must not be resolved, because `gpui-pre-macros` 0.3.1 breaks every `debug_assertions`-off build (see `RELEASING.md`). +## Bootstrap the patched GPUI sources before any cargo command + +Five of those packages are forked. The repository stores **only the patches**, +under `docs/upstream/patches/`; the patched sources are not checked in. Before +the first `cargo` command in a fresh clone — and before pointing +rust-analyzer at it, because rust-analyzer runs `cargo metadata` — run: + +```sh +python3 .shots/gpui_patches.py --materialize +``` + +That copies the pinned published packages out of cargo's registry cache into +the gitignored `.vendor/` tree and applies each patch, exactly, with no fuzz and +no offset. It is idempotent: a warm run is a hash comparison that finishes in +well under a second, so it is safe to put in front of anything. Skipping it is +not a soft failure — `[patch.crates-io]` names five paths that do not exist yet, +and cargo aborts at manifest load with `failed to load source for dependency`. +`crates/herogpui-components/tests/rounded_clip_shaders.rs` also includes the +renderer's own `shaders.rs` from `.vendor/` by `#[path]`, so even `cargo fmt +--all` fails without it. Every CI job that touches cargo runs the step first, +through `.github/actions/rust-env` or its own copy. + +Use the pristine unpacked registry sources for API evidence — +`~/.cargo/registry/src/index.crates.io-*/gpui-pre-0.3.3/` — and `.vendor/` when +what you need is the patched behaviour. Neither is a Zed git checkout, the older +third-party `gpui-unofficial` republish this replaced, or the unrelated +crates.io `gpui` 0.2.2 crate. Never commit anything under `.vendor/`; to change +the fork, edit the materialized tree and re-record it with +`python3 .shots/gpui_patches.py --write`, which rewrites the patch and is +verified by `--check`. + ## Before editing 1. Run `git status --short` and inspect the relevant diff. Preserve unrelated @@ -100,10 +127,12 @@ all. Two facts about how, because both are easy to get wrong: - The switch is **not** `default-features = false` on a dependency edge of ours. `gpui_platform` depends on `gpui_web` with default features on, so a second edge from this workspace is unioned with that one and changes nothing. - The only lever is `default` in the vendored fork's own manifest, - `crates/gpui_web/Cargo.toml`, which `[patch.crates-io]` substitutes. It is - set to `default = []` there, documented at the `[features]` table as the - fork's third deviation from upstream. + The only lever is `default` in the forked manifest itself, which + `[patch.crates-io]` substitutes. It is set to `default = []` by a hunk in + `docs/upstream/patches/gpui-pre-web-0.3.3.patch` — the fork's third deviation + from upstream — and reaches the build as + `.vendor/gpui-pre-web-0.3.3/Cargo.toml`. Lose that hunk and the wasm job + needs a nightly pin again. - Nothing was using it. `crates/herogpui-web` starts the app with `gpui_platform::single_threaded_web()`, and the multi-threaded platform runs its background executors on web workers over shared wasm memory, which a @@ -128,4 +157,5 @@ sources, so after rebuilding it regenerate its manifests in the same change with `pnpm run wasm:manifest` from `web/`. They pin the artifact and every gallery example body by hash, and `pnpm run extract:check` fails when the two have parted company. `docs/upstream/gpui-web-scroll-and-ime.md` covers the one -remaining fork, the +9/-5 lines in `crates/gpui_web`. +remaining source fork of that crate, +9/-5 lines in `src/events.rs`, recorded +in `docs/upstream/patches/gpui-pre-web-0.3.3.patch`. diff --git a/Cargo.toml b/Cargo.toml index 398d306..e0d9844 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,16 +9,13 @@ members = [ "gallery", ] -# The vendored `gpui_web` fork is a workspace root of its own (see its -# manifest's header), reached only through `[patch.crates-io]` at the bottom of -# this file. Naming it here says that omission from `members` is deliberate. -exclude = [ - "crates/gpui_pre", - "crates/gpui_pre_apple", - "crates/gpui_pre_wgpu", - "crates/gpui_pre_windows", - "crates/gpui_web", -] +# `.vendor/` is where `.shots/gpui_patches.py --materialize` writes the patched +# GPUI sources; it is gitignored and holds no HeroGPUI code. Each materialized +# manifest declares its own `[workspace]` table, so excluding the root here is +# belt and braces -- but it is also the line that says the omission from +# `members` is deliberate, and it keeps a stray `cargo metadata` from walking +# ~153,000 lines of dependency source it has no business in. +exclude = [".vendor"] [workspace.package] version = "0.1.0" @@ -43,14 +40,16 @@ categories = ["gui"] # `gpui_platform`'s wasm32 dependency, which enables its default features; a # second, `default-features = false` edge from this workspace would be unioned # with that one and change nothing. The `multithreaded` feature is therefore -# switched in the vendored fork's own manifest (`crates/gpui_web/Cargo.toml`), -# which `[patch.crates-io]` at the bottom of this file substitutes for it. +# switched in the forked manifest itself -- a `default = []` hunk in +# `docs/upstream/patches/gpui-pre-web-0.3.3.patch`, materialized into +# `.vendor/gpui-pre-web-0.3.3/Cargo.toml`, which `[patch.crates-io]` at the +# bottom of this file substitutes for the published crate. # # Both pins are exact (`=0.3.3`) rather than caret. `gpui-pre-platform` depends # on the rest of the family at an exact `=` version of its own, so a caret here # buys no flexibility while letting a bare `cargo update` walk the workspace to -# a version the vendored `gpui-pre-web` fork no longer matches -- at which point -# `[patch.crates-io]` silently stops applying. 0.3.1 and 0.3.2 must not be +# a version the recorded `gpui-pre-web` patch no longer matches -- at which +# point `[patch.crates-io]` silently stops applying. 0.3.1 and 0.3.2 must not be # resolved in any case: `gpui-pre-macros` leaves the inner # `__gpui_pre_derive_inspector_reflection` helper ungated in 0.3.1 while its # body calls into a `#[cfg(any(feature = "inspector", debug_assertions))]` @@ -164,16 +163,30 @@ strip = "symbols" # wasm checkout used, when GPUI came from a Zed git rev) matches no source in # that graph and applies *nothing*, with no error. Prove it is live with # `cargo tree -i gpui-pre-web --target wasm32-unknown-unknown`, which must -# print the `crates/gpui_web` path source. +# print the `.vendor/gpui-pre-web-0.3.3` path source. +# +# These five paths do not exist in a fresh clone. The repository records the +# fork as patches under `docs/upstream/patches/` and nothing else; +# `.shots/gpui_patches.py --materialize` copies the pinned published sources +# out of cargo's registry cache into `.vendor/` and applies them. Cargo cannot +# do that for us and it fails hard, not softly, when the path is absent -- +# "failed to load source for dependency" at manifest load, before any build -- +# so the materialization step runs first in every CI job that touches cargo, +# and a fresh clone (including one rust-analyzer is about to index) must run it +# before the first cargo command. These are `path` entries and not `git` ones +# on purpose: cargo refuses to publish a crate whose workspace carries a git +# dependency, which is the whole reason GPUI comes from the registry at all +# (see RELEASING.md). [patch.crates-io] # The published gpui-pre 0.3.3 clips `overflow_hidden()` to a rectangle even -# when the element has rounded corners. Keep the exact upstream snapshot in -# the workspace so the shared content mask can carry those corners through the -# renderer; every component then gets the same fix automatically. The fork -# delta and the rebase procedure are recorded in -# docs/upstream/gpui-rounded-content-mask.md. -gpui-pre = { path = "crates/gpui_pre" } -gpui-pre-apple = { path = "crates/gpui_pre_apple" } -gpui-pre-wgpu = { path = "crates/gpui_pre_wgpu" } -gpui-pre-windows = { path = "crates/gpui_pre_windows" } -gpui-pre-web = { path = "crates/gpui_web" } +# when the element has rounded corners. Materializing the exact upstream +# snapshot plus our patch lets the shared content mask carry those corners +# through the renderer; every component then gets the same fix automatically. +# The fork delta and the rebase procedure are recorded in +# docs/upstream/gpui-rounded-content-mask.md, and the `gpui-pre-web` fork in +# docs/upstream/gpui-web-scroll-and-ime.md. +gpui-pre = { path = ".vendor/gpui-pre-0.3.3" } +gpui-pre-apple = { path = ".vendor/gpui-pre-apple-0.3.3" } +gpui-pre-wgpu = { path = ".vendor/gpui-pre-wgpu-0.3.3" } +gpui-pre-windows = { path = ".vendor/gpui-pre-windows-0.3.3" } +gpui-pre-web = { path = ".vendor/gpui-pre-web-0.3.3" } diff --git a/README.md b/README.md index 83ec3f8..b4b3932 100644 --- a/README.md +++ b/README.md @@ -133,10 +133,19 @@ HeroUI spelling wins, and GPUI's keep the `gpui::` path (`gpui::Size`). A desktop gallery ships with the library and documents every component: ```bash +python3 .shots/gpui_patches.py --materialize # once per clone, before any cargo command cargo run -p herogpui-gallery # open the component gallery cargo install --path gallery --locked # install the gallery CLI from this checkout ``` +HeroGPUI patches five published `gpui-pre` packages and checks in only the +patches, under `docs/upstream/patches/`. The first line above applies them to +the pinned registry sources under the gitignored `.vendor/`, which +`[patch.crates-io]` points at. It is idempotent and near-instant when warm, but +it is not optional: without it cargo stops at `failed to load source for +dependency` before it builds anything. Consumers of the published crates are +unaffected -- this is a workspace-local development substitution. + `HEROGPUI_PAGE` and `HEROGPUI_THEME` select the page and appearance; `HEROGPUI_WINDOW_SIZE` sets the window size. diff --git a/RELEASING.md b/RELEASING.md index d87ef45..9cc5941 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -8,18 +8,38 @@ git revision: cargo refuses to publish any crate that carries a git dependency, so every `cargo publish` below would have failed outright. GPUI now comes from the published `gpui-pre` crates named in `[workspace.dependencies]`, and that registry dependency is what makes the steps below executable. Reintroducing -a git dependency anywhere in the workspace re-breaks publishing. +a git dependency anywhere in the workspace re-breaks publishing. The five +`[patch.crates-io]` overrides are deliberately `path` entries into `.vendor/` +for that reason, and must stay `path` entries: a `git` override would put a git +dependency back in the graph and make every crate below unpublishable again. + +## Materialize the patched GPUI sources first + +No step in this document works in a fresh clone until the patched GPUI sources +exist. The repository carries only the patches, under `docs/upstream/patches/`, +and `[patch.crates-io]` points at five `.vendor/` paths that are gitignored: + +```sh +python3 .shots/gpui_patches.py --materialize +``` + +Without it, cargo stops at manifest load with `failed to load source for +dependency` — `cargo package`, `cargo publish` and `cargo publish --dry-run` +included. It is idempotent, so run it whenever in doubt. `.vendor/` is a build +input, never a release artifact: the published crates depend on the registry +`gpui-pre` family, and the patch overrides are a workspace-local development +substitution that `cargo publish` does not carry into a `.crate`. ## Why the GPUI pin is exact `[workspace.dependencies]` pins `gpui-pre` and `gpui-pre-platform` at -`=0.3.3`, and the vendored `gpui-pre-web` fork in `crates/gpui_web` carries the -same version. Do not relax either to a caret. `gpui-pre-platform` requires the -rest of the family at an exact `=` version of its own, so the caret bought no -flexibility while letting a bare `cargo update` walk the workspace onto a -version the fork no longer matched — at which point `[patch.crates-io]` stops -applying with no error and the fork's `events.rs` hunks silently leave the -browser build. +`=0.3.3`, and the recorded `gpui-pre-web` patch carries the same version in its +filename and in the `[patch.crates-io]` path it materializes to. Do not relax +either to a caret. `gpui-pre-platform` requires the rest of the family at an +exact `=` version of its own, so the caret bought no flexibility while letting a +bare `cargo update` walk the workspace onto a version the fork no longer matched +— at which point `[patch.crates-io]` stops applying with no error and the fork's +`events.rs` hunks silently leave the browser build. 0.3.1 and 0.3.2 must not be resolved. `gpui-pre-macros` 0.3.1 leaves the inner `__gpui_pre_derive_inspector_reflection` helper ungated while its body calls @@ -56,7 +76,8 @@ credentials and none of these steps are required. 1. Update `[workspace.package].version` and all four version requirements under `[workspace.dependencies]` to the same SemVer value. -2. Run the complete local gate from `AGENTS.md`, plus: +2. Run the complete local gate from `AGENTS.md` (which starts with the + materialization above), plus: ```powershell cargo package -p herogpui-core --allow-dirty --no-verify --list diff --git a/crates/gpui_pre/Cargo.lock b/crates/gpui_pre/Cargo.lock deleted file mode 100644 index b505fe4..0000000 --- a/crates/gpui_pre/Cargo.lock +++ /dev/null @@ -1,5112 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "accesskit" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" -dependencies = [ - "enumn", - "uuid", -] - -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "aligned" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" -dependencies = [ - "as-slice", -] - -[[package]] -name = "aligned-vec" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" -dependencies = [ - "equator", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anes" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" - -[[package]] -name = "anstyle" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" - -[[package]] -name = "anyhow" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" - -[[package]] -name = "ar_archive_writer" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" -dependencies = [ - "object", -] - -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" - -[[package]] -name = "arg_enum_proc_macro" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "as-slice" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" -dependencies = [ - "stable_deref_trait", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-compression" -version = "0.4.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0" -dependencies = [ - "compression-codecs", - "compression-core", - "futures-core", - "futures-io", - "pin-project-lite", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "atomic" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "av-scenechange" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" -dependencies = [ - "aligned", - "anyhow", - "arg_enum_proc_macro", - "arrayvec", - "log", - "num-rational", - "num-traits", - "pastey", - "rayon", - "thiserror 2.0.17", - "v_frame", - "y4m", -] - -[[package]] -name = "av1-grain" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" -dependencies = [ - "anyhow", - "arrayvec", - "log", - "nom", - "num-rational", - "v_frame", -] - -[[package]] -name = "avif-serialize" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link 0.2.1", -] - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.13.1", - "cexpr", - "clang-sys", - "itertools 0.11.0", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex 1.3.0", - "syn 2.0.117", -] - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bit_field" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" - -[[package]] -name = "bitstream-io" -version = "4.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" -dependencies = [ - "core2", -] - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2", -] - -[[package]] -name = "borsh" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" -dependencies = [ - "cfg_aliases", -] - -[[package]] -name = "built" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "bytemuck" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - -[[package]] -name = "bzip2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" -dependencies = [ - "libbz2-rs-sys", -] - -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - -[[package]] -name = "cc" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex 2.0.1", -] - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "cgl" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" -dependencies = [ - "libc", -] - -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link 0.2.1", -] - -[[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" -dependencies = [ - "ciborium-io", - "half", -] - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "clap" -version = "4.5.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" -dependencies = [ - "clap_builder", -] - -[[package]] -name = "clap_builder" -version = "4.5.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" -dependencies = [ - "anstyle", - "clap_lex", -] - -[[package]] -name = "clap_lex" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" - -[[package]] -name = "cocoa" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" -dependencies = [ - "bitflags 1.3.2", - "block", - "cocoa-foundation", - "core-foundation 0.9.4", - "core-graphics 0.23.2", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" -dependencies = [ - "bitflags 1.3.2", - "block", - "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "libc", - "objc", -] - -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - -[[package]] -name = "compression-codecs" -version = "0.4.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" -dependencies = [ - "bzip2", - "compression-core", - "flate2", - "memchr", -] - -[[package]] -name = "compression-core" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "convert_case" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" -dependencies = [ - "bitflags 2.13.1", - "core-foundation 0.10.0", - "core-graphics-types 0.2.0", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-helmer-fork" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5" -dependencies = [ - "bitflags 2.13.1", - "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.13.1", - "core-foundation 0.10.0", - "libc", -] - -[[package]] -name = "core-graphics2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4416167a69126e617f8d0a214af0e3c1dbdeffcb100ddf72dcd1a1ac9893c146" -dependencies = [ - "bitflags 2.13.1", - "block", - "cfg-if", - "core-foundation 0.10.0", - "libc", -] - -[[package]] -name = "core-text" -version = "21.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130" -dependencies = [ - "core-foundation 0.10.0", - "core-graphics 0.24.0", - "foreign-types", - "libc", -] - -[[package]] -name = "core-video" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139679cc63eb9504bdbe37e37874b0247136177655f0008588781e90863afa62" -dependencies = [ - "block", - "core-foundation 0.10.0", - "core-graphics2", - "io-surface", - "libc", - "metal", -] - -[[package]] -name = "core2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" -dependencies = [ - "memchr", -] - -[[package]] -name = "core_maths" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" -dependencies = [ - "libm", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "criterion" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" -dependencies = [ - "anes", - "cast", - "ciborium", - "clap", - "criterion-plot", - "is-terminal", - "itertools 0.10.5", - "num-traits", - "once_cell", - "oorandom", - "plotters", - "rayon", - "regex", - "serde", - "serde_derive", - "serde_json", - "tinytemplate", - "walkdir", -] - -[[package]] -name = "criterion-plot" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" -dependencies = [ - "cast", - "itertools 0.10.5", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "ctor" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d83cb7e7a873830708d6b02a78cd36a592c6fa14bf267b68725103b85c0d77f" -dependencies = [ - "link-section", - "linktime-proc-macro", -] - -[[package]] -name = "data-url" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case 0.10.0", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.117", - "unicode-xid", -] - -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.48.0", -] - -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.13.1", - "objc2", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "dlib" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" -dependencies = [ - "libloading", -] - -[[package]] -name = "dwrote" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b35532432acc8b19ceed096e35dfa088d3ea037fe4f3c085f1f97f33b4d02" -dependencies = [ - "lazy_static", - "libc", - "winapi", - "wio", -] - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "embed-resource" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" -dependencies = [ - "cc", - "memchr", - "rustc_version", - "toml", - "vswhom", - "winreg", -] - -[[package]] -name = "enumn" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "equator" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" -dependencies = [ - "equator-macro", -] - -[[package]] -name = "equator-macro" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "erased-serde" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "etagere" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" -dependencies = [ - "euclid", - "svg_fmt", -] - -[[package]] -name = "euclid" -version = "0.22.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" -dependencies = [ - "num-traits", -] - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "exr" -version = "1.74.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" -dependencies = [ - "bit_field", - "half", - "lebe", - "miniz_oxide", - "rayon-core", - "smallvec", - "zune-inflate", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "fax" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" -dependencies = [ - "fax_derive", -] - -[[package]] -name = "fax_derive" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" - -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - -[[package]] -name = "flate2" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" - -[[package]] -name = "float-ord" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" - -[[package]] -name = "float_next_after" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" - -[[package]] -name = "flume" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" -dependencies = [ - "fastrand", - "futures-core", - "futures-sink", - "spin 0.9.8", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "fontconfig-parser" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" -dependencies = [ - "roxmltree 0.20.0", -] - -[[package]] -name = "fontdb" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" -dependencies = [ - "fontconfig-parser", - "log", - "memmap2", - "slotmap", - "tinyvec", - "ttf-parser", -] - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "freetype-sys" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-concurrency" -version = "7.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6" -dependencies = [ - "fixedbitset", - "futures-core", - "futures-lite", - "pin-project", - "smallvec", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", - "wasip3", -] - -[[package]] -name = "gif" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" -dependencies = [ - "color_quant", - "weezl", -] - -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "gpui-pre" -version = "0.3.3" -dependencies = [ - "accesskit", - "anyhow", - "async-channel", - "async-task", - "backtrace", - "bindgen", - "bitflags 2.13.1", - "chrono", - "core-video", - "criterion", - "ctor", - "derive_more", - "embed-resource", - "etagere", - "futures", - "futures-concurrency", - "getrandom 0.3.4", - "gpui-pre-collections", - "gpui-pre-http-client", - "gpui-pre-macros", - "gpui-pre-refineable", - "gpui-pre-scheduler", - "gpui-pre-shared-string", - "gpui-pre-sum-tree", - "gpui-pre-util", - "gpui-pre-util-macros", - "gpui-pre-ztracing", - "hdrhistogram", - "heapless", - "image", - "inventory", - "itertools 0.14.0", - "log", - "lyon", - "num_cpus", - "objc2", - "objc2-metal", - "parking", - "parking_lot", - "pin-project", - "pollster 0.4.0", - "postage", - "profiling", - "proptest", - "rand 0.9.4", - "raw-window-handle", - "regex", - "resvg", - "schemars", - "seahash", - "serde", - "serde_json", - "slotmap", - "smallvec", - "spin 0.10.0", - "stacksafe", - "strum", - "taffy", - "thiserror 2.0.17", - "tracing", - "ttf-parser", - "url", - "usvg", - "uuid", - "waker-fn", - "web-time", - "windows 0.62.2", - "zed-font-kit", - "zed-scap", -] - -[[package]] -name = "gpui-pre-collections" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89c8efa2e51e368c8538a7be1ea9a12127ca03abe6cc01d9d3474e9ac4f53016" -dependencies = [ - "gpui-pre-util", - "indexmap", - "rustc-hash", -] - -[[package]] -name = "gpui-pre-derive-refineable" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a098d319acc9f84bf159944f96c5ea43a4f4cd7ed759f984acbd15719495aa0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "gpui-pre-http-client" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3495a45a28cb800c8626d2053406114bcaca26d390a4d4b183365b5bb8fdaf02" -dependencies = [ - "anyhow", - "async-compression", - "bytes", - "derive_more", - "futures", - "http", - "http-body", - "log", - "parking_lot", - "serde", - "serde_json", - "serde_urlencoded", - "url", -] - -[[package]] -name = "gpui-pre-macros" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be2db7b5097b4d523b2bce933604bcc5acdaf679bb9a150e8299e6c07efc29c" -dependencies = [ - "heck", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "gpui-pre-perf" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1db0c046b93c2a29120f8ee4c04bc80a4d7d6117d1164ef349faface8943491" -dependencies = [ - "gpui-pre-collections", - "serde", - "serde_json", -] - -[[package]] -name = "gpui-pre-refineable" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864e2e54a3029481dae5b6aae3ba2905f2fb1dfe66ced913b68c9ff527f8e327" -dependencies = [ - "gpui-pre-derive-refineable", -] - -[[package]] -name = "gpui-pre-scheduler" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b58a78c4e0c032900704ea49922641c534cf8bbf1bf22eda6ba00a76e88c6c3" -dependencies = [ - "async-task", - "backtrace", - "chrono", - "flume", - "futures", - "parking_lot", - "rand 0.9.4", - "web-time", -] - -[[package]] -name = "gpui-pre-shared-string" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82fc99fe88e44173758a500522d3f4059a6541da4b471af720e3f60cf34a2bc3" -dependencies = [ - "schemars", - "serde", - "smol_str", -] - -[[package]] -name = "gpui-pre-sum-tree" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "002baed852f20cef1188d3d5e0f749025fc718e4d6cea026b9077a9a6c10d042" -dependencies = [ - "gpui-pre-ztracing", - "heapless", - "log", - "rayon", - "tracing", -] - -[[package]] -name = "gpui-pre-util" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fe779c4cb00929aafcb2b307cd1240588aebb5d1eb328c59b80f77c05a41fad" -dependencies = [ - "anyhow", - "log", - "which", -] - -[[package]] -name = "gpui-pre-util-macros" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f0ccc4bccb6a31786095d15fc9f40d6a4c6a295522e3460331dd7e929ff2f79" -dependencies = [ - "gpui-pre-perf", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "gpui-pre-zlog" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1416ea5f018e3a8a1c8be266332c443583f28f8f87379a84ab1e77622173ac0" -dependencies = [ - "anyhow", - "chrono", - "gpui-pre-collections", - "log", -] - -[[package]] -name = "gpui-pre-ztracing" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cc64a25a3cc4e4f1d8acb00074d3925339dae657c020083735738ba8af96bf7" -dependencies = [ - "gpui-pre-zlog", - "gpui-pre-ztracing-macro", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "gpui-pre-ztracing-macro" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f3ea75672348f37d94472e579f5979ee19b744d0fa5aaadc2fe129ccafa80b" - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - -[[package]] -name = "hash32" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" -dependencies = [ - "byteorder", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "hdrhistogram" -version = "7.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" -dependencies = [ - "byteorder", - "num-traits", -] - -[[package]] -name = "heapless" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" -dependencies = [ - "hash32", - "stable_deref_trait", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "http" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" - -[[package]] -name = "icu_properties" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" - -[[package]] -name = "icu_provider" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "image" -version = "0.25.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" -dependencies = [ - "bytemuck", - "byteorder-lite", - "color_quant", - "exr", - "gif", - "image-webp", - "moxcms", - "num-traits", - "png 0.18.0", - "qoi", - "ravif", - "rayon", - "tiff", - "zune-core", - "zune-jpeg", -] - -[[package]] -name = "image-webp" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" -dependencies = [ - "byteorder-lite", - "quick-error 2.0.1", -] - -[[package]] -name = "imagesize" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" - -[[package]] -name = "imgref" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[package]] -name = "interpolate_name" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "inventory" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" -dependencies = [ - "rustversion", -] - -[[package]] -name = "io-surface" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "554b8c5d64ec09a3a520fe58e4d48a73e00ff32899cdcbe32a4877afd4968b8e" -dependencies = [ - "cgl", - "core-foundation 0.10.0", - "core-foundation-sys", - "leaky-cow", -] - -[[package]] -name = "is-terminal" -version = "0.4.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" -dependencies = [ - "cfg-if", - "futures-util", - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "kurbo" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" -dependencies = [ - "arrayvec", - "euclid", - "polycool", - "smallvec", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leak" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd100e01f1154f2908dfa7d02219aeab25d0b9c7fa955164192e3245255a0c73" - -[[package]] -name = "leaky-cow" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a8225d44241fd324a8af2806ba635fc7c8a7e9a7de4d5cf3ef54e71f5926fc" -dependencies = [ - "leak", -] - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "lebe" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" - -[[package]] -name = "libbz2-rs-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "libfuzzer-sys" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" -dependencies = [ - "arbitrary", - "cc", -] - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link 0.2.1", -] - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "libredox" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" -dependencies = [ - "bitflags 2.13.1", - "libc", -] - -[[package]] -name = "link-section" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee1a0d6e252afe82e7bc2db42fba60e02ddf3b1accaf8cb21d96e34ba61f3d4" - -[[package]] -name = "linktime-proc-macro" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "348d0075b1fc163b26d72a7f75fc5141daf2fd1bdf128d873cbaf6785d495bdf" - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litemap" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -dependencies = [ - "serde_core", - "value-bag", -] - -[[package]] -name = "loop9" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" -dependencies = [ - "imgref", -] - -[[package]] -name = "lyon" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbcb7d54d54c8937364c9d41902d066656817dce1e03a44e5533afebd1ef4352" -dependencies = [ - "lyon_algorithms", - "lyon_tessellation", -] - -[[package]] -name = "lyon_algorithms" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c0829e28c4f336396f250d850c3987e16ce6db057ffe047ce0dd54aab6b647" -dependencies = [ - "lyon_path", - "num-traits", -] - -[[package]] -name = "lyon_geom" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e16770d760c7848b0c1c2d209101e408207a65168109509f8483837a36cf2e7" -dependencies = [ - "arrayvec", - "euclid", - "num-traits", -] - -[[package]] -name = "lyon_path" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aeca86bcfd632a15984ba029b539ffb811e0a70bf55e814ef8b0f54f506fdeb" -dependencies = [ - "lyon_geom", - "num-traits", -] - -[[package]] -name = "lyon_tessellation" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f586142e1280335b1bc89539f7c97dd80f08fc43e9ab1b74ef0a42b04aa353" -dependencies = [ - "float_next_after", - "lyon_path", - "num-traits", -] - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - -[[package]] -name = "maybe-rayon" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" -dependencies = [ - "cfg-if", - "rayon", -] - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "memmap2" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" -dependencies = [ - "libc", -] - -[[package]] -name = "metal" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15" -dependencies = [ - "bitflags 2.13.1", - "block", - "core-graphics-types 0.2.0", - "foreign-types", - "log", - "objc", - "paste", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "moxcms" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" -dependencies = [ - "num-traits", - "pxfm", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "noop_proc_macro" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" - -[[package]] -name = "ntapi" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" -dependencies = [ - "winapi", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num-bigint" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", - "objc_exception", -] - -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - -[[package]] -name = "objc2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" -dependencies = [ - "objc2-encode", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.13.1", - "dispatch2", - "objc2", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.13.1", - "objc2", -] - -[[package]] -name = "objc2-metal" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" -dependencies = [ - "bitflags 2.13.1", - "block2", - "dispatch2", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc_exception" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" -dependencies = [ - "cc", -] - -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link 0.2.1", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pastey" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" - -[[package]] -name = "pathfinder_geometry" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" -dependencies = [ - "log", - "pathfinder_simd", -] - -[[package]] -name = "pathfinder_simd" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4500030c302e4af1d423f36f3b958d1aecb6c04184356ed5a833bf6b60435777" -dependencies = [ - "rustc_version", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pico-args" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" - -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "plotters" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" - -[[package]] -name = "plotters-svg" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" -dependencies = [ - "plotters-backend", -] - -[[package]] -name = "png" -version = "0.17.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "png" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" -dependencies = [ - "bitflags 2.13.1", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "pollster" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" - -[[package]] -name = "pollster" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" - -[[package]] -name = "polycool" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "postage" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" -dependencies = [ - "atomic", - "crossbeam-queue", - "futures", - "log", - "parking_lot", - "pin-project", - "pollster 0.2.5", - "static_assertions", - "thiserror 1.0.69", -] - -[[package]] -name = "potential_utf" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" -dependencies = [ - "zerovec", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - -[[package]] -name = "proc-macro-crate" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "profiling" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" -dependencies = [ - "profiling-procmacros", -] - -[[package]] -name = "profiling-procmacros" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" -dependencies = [ - "quote", - "syn 2.0.117", -] - -[[package]] -name = "proptest" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" -dependencies = [ - "bit-set", - "bit-vec", - "bitflags 2.13.1", - "num-traits", - "proptest-macro", - "rand 0.9.4", - "rand_chacha 0.9.0", - "rand_xorshift", - "regex-syntax", - "rusty-fork", - "tempfile", - "unarray", -] - -[[package]] -name = "proptest-macro" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efaa288b896cb2b345da7b7f2110ab19e51565b83495b56fcec98a62f8b1f33e" -dependencies = [ - "convert_case 0.11.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "psm" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" -dependencies = [ - "ar_archive_writer", - "cc", -] - -[[package]] -name = "pxfm" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" -dependencies = [ - "num-traits", -] - -[[package]] -name = "qoi" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - -[[package]] -name = "quick-xml" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rand_xorshift" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" -dependencies = [ - "rand_core 0.9.3", -] - -[[package]] -name = "rav1e" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" -dependencies = [ - "aligned-vec", - "arbitrary", - "arg_enum_proc_macro", - "arrayvec", - "av-scenechange", - "av1-grain", - "bitstream-io", - "built", - "cfg-if", - "interpolate_name", - "itertools 0.14.0", - "libc", - "libfuzzer-sys", - "log", - "maybe-rayon", - "new_debug_unreachable", - "noop_proc_macro", - "num-derive", - "num-traits", - "paste", - "profiling", - "rand 0.9.4", - "rand_chacha 0.9.0", - "simd_helpers", - "thiserror 2.0.17", - "v_frame", - "wasm-bindgen", -] - -[[package]] -name = "ravif" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" -dependencies = [ - "avif-serialize", - "imgref", - "loop9", - "quick-error 2.0.1", - "rav1e", - "rayon", - "rgb", -] - -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - -[[package]] -name = "rayon" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.13.1", -] - -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.16", - "libredox", - "thiserror 1.0.69", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "resvg" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b563218631706d614e23059436526d005b50ab5f2d506b55a17eb65c5eb83419" -dependencies = [ - "gif", - "image-webp", - "log", - "pico-args", - "rgb", - "svgtypes", - "tiny-skia", - "usvg", - "zune-jpeg", -] - -[[package]] -name = "rgb" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "roxmltree" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" - -[[package]] -name = "roxmltree" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" -dependencies = [ - "memchr", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.13.1", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "rusty-fork" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" -dependencies = [ - "fnv", - "quick-error 1.2.3", - "tempfile", - "wait-timeout", -] - -[[package]] -name = "rustybuzz" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" -dependencies = [ - "bitflags 2.13.1", - "bytemuck", - "core_maths", - "log", - "smallvec", - "ttf-parser", - "unicode-bidi-mirroring", - "unicode-ccc", - "unicode-properties", - "unicode-script", -] - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schemars" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" -dependencies = [ - "dyn-clone", - "indexmap", - "ref-cast", - "schemars_derive", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.117", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "screencapturekit" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5eeeb57ac94960cfe5ff4c402be6585ae4c8d29a2cf41b276048c2e849d64e" -dependencies = [ - "screencapturekit-sys", -] - -[[package]] -name = "screencapturekit-sys" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22411b57f7d49e7fe08025198813ee6fd65e1ee5eff4ebc7880c12c82bde4c60" -dependencies = [ - "block", - "dispatch", - "objc", - "objc-foundation", - "objc_id", - "once_cell", -] - -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_fmt" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_json" -version = "1.0.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "indexmap", - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "simd_helpers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" -dependencies = [ - "quote", -] - -[[package]] -name = "simplecss" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" -dependencies = [ - "log", -] - -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "slotmap" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" -dependencies = [ - "version_check", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "smol_str" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" -dependencies = [ - "borsh", - "serde_core", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spin" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" -dependencies = [ - "lock_api", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "stacker" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys 0.61.2", -] - -[[package]] -name = "stacksafe" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95f9c34983ac74195c710c473db6fdf1085f64a47dbaa0090d1bea03be70da66" -dependencies = [ - "stacker", - "stacksafe-macro", -] - -[[package]] -name = "stacksafe-macro" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6feeae42a2d6b0dcb8aeb2f08d9e48cdac600239cf8a20fc59f9e252e86bdfe1" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strict-num" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" -dependencies = [ - "float-cmp", -] - -[[package]] -name = "strum" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "sval" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d94c4464e595f0284970fd9c7e9013804d035d4a61ab74b113242c874c05814d" - -[[package]] -name = "sval_buffer" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0f46e34b20a39e6a2bf02b926983149b3af6609fd1ee8a6e63f6f340f3e2164" -dependencies = [ - "sval", - "sval_ref", -] - -[[package]] -name = "sval_dynamic" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d0970e53c92ab5381d3b2db1828da8af945954d4234225f6dd9c3afbcef3f5" -dependencies = [ - "sval", -] - -[[package]] -name = "sval_fmt" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43e5e6e1613e1e7fc2e1a9fdd709622e54c122ceb067a60d170d75efd491a839" -dependencies = [ - "itoa", - "ryu", - "sval", -] - -[[package]] -name = "sval_json" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aec382f7bfa6e367b23c9611f129b94eb7daaf3d8fae45a8d0a0211eb4d4c8e6" -dependencies = [ - "itoa", - "ryu", - "sval", -] - -[[package]] -name = "sval_nested" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3049d0f99ce6297f8f7d9953b35a0103b7584d8f638de40e64edb7105fa578ae" -dependencies = [ - "sval", - "sval_buffer", - "sval_ref", -] - -[[package]] -name = "sval_ref" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f88913e77506085c0a8bf6912bb6558591a960faf5317df6c1d9b227224ca6e1" -dependencies = [ - "sval", -] - -[[package]] -name = "sval_serde" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f579fd7254f4be6cd7b450034f856b78523404655848789c451bacc6aa8b387d" -dependencies = [ - "serde_core", - "sval", - "sval_nested", -] - -[[package]] -name = "svg_fmt" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" - -[[package]] -name = "svgtypes" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d" -dependencies = [ - "kurbo", - "siphasher", -] - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "sysinfo" -version = "0.31.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" -dependencies = [ - "core-foundation-sys", - "libc", - "memchr", - "ntapi", - "rayon", - "windows 0.57.0", -] - -[[package]] -name = "taffy" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c034e05f6ee85a12daa63863c2245797715075c70649947aa0da54f3f2ab1d0f" -dependencies = [ - "arrayvec", - "serde", - "slotmap", - "smallvec", -] - -[[package]] -name = "tao-core-video-sys" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271450eb289cb4d8d0720c6ce70c72c8c858c93dd61fc625881616752e6b98f6" -dependencies = [ - "cfg-if", - "core-foundation-sys", - "libc", - "objc", -] - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.1", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" -dependencies = [ - "thiserror-impl 2.0.17", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "tiff" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" -dependencies = [ - "fax", - "flate2", - "half", - "quick-error 2.0.1", - "weezl", - "zune-jpeg", -] - -[[package]] -name = "tiny-skia" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" -dependencies = [ - "arrayref", - "arrayvec", - "bytemuck", - "cfg-if", - "log", - "png 0.17.16", - "tiny-skia-path", -] - -[[package]] -name = "tiny-skia-path" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" -dependencies = [ - "arrayref", - "bytemuck", - "strict-num", -] - -[[package]] -name = "tinystr" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "toml" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.23.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_writer" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" - -[[package]] -name = "tracing" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tracing-core" -version = "0.1.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "ttf-parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" -dependencies = [ - "core_maths", -] - -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - -[[package]] -name = "unarray" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" - -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - -[[package]] -name = "unicode-bidi-mirroring" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" - -[[package]] -name = "unicode-ccc" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-properties" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" - -[[package]] -name = "unicode-script" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" - -[[package]] -name = "unicode-segmentation" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" - -[[package]] -name = "unicode-vo" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "usvg" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e419dff010bb12512b0ae9e3d2f318dfbdf0167fde7eb05465134d4e8756076f" -dependencies = [ - "base64", - "data-url", - "flate2", - "fontdb", - "imagesize", - "kurbo", - "log", - "pico-args", - "roxmltree 0.21.1", - "rustybuzz", - "simplecss", - "siphasher", - "strict-num", - "svgtypes", - "tiny-skia-path", - "unicode-bidi", - "unicode-script", - "unicode-vo", - "xmlwriter", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" -dependencies = [ - "getrandom 0.3.4", - "js-sys", - "serde", - "sha1_smol", - "wasm-bindgen", -] - -[[package]] -name = "v_frame" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" -dependencies = [ - "aligned-vec", - "num-traits", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "value-bag" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" -dependencies = [ - "value-bag-serde1", - "value-bag-sval2", -] - -[[package]] -name = "value-bag-serde1" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16530907bfe2999a1773ca5900a65101e092c70f642f25cc23ca0c43573262c5" -dependencies = [ - "erased-serde", - "serde_core", - "serde_fmt", -] - -[[package]] -name = "value-bag-sval2" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d00ae130edd690eaa877e4f40605d534790d1cf1d651e7685bd6a144521b251f" -dependencies = [ - "sval", - "sval_buffer", - "sval_dynamic", - "sval_fmt", - "sval_json", - "sval_ref", - "sval_serde", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vswhom" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" -dependencies = [ - "libc", - "vswhom-sys", -] - -[[package]] -name = "vswhom-sys" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "wait-timeout" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" -dependencies = [ - "libc", -] - -[[package]] -name = "waker-fn" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen 0.46.0", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.117", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.13.1", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "web-sys" -version = "0.3.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "weezl" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" - -[[package]] -name = "which" -version = "8.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3ef584124b911bcc3875c2f1472e80f24361ceb789bd1c62b3e9a3df9ff43c" -dependencies = [ - "libc", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" -dependencies = [ - "windows-core 0.57.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections 0.2.0", - "windows-core 0.61.2", - "windows-future 0.2.1", - "windows-link 0.1.3", - "windows-numerics 0.2.0", -] - -[[package]] -name = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections 0.3.2", - "windows-core 0.62.2", - "windows-future 0.3.2", - "windows-numerics 0.3.1", -] - -[[package]] -name = "windows-capture" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a4df73e95feddb9ec1a7e9c2ca6323b8c97d5eeeff78d28f1eccdf19c882b24" -dependencies = [ - "parking_lot", - "rayon", - "thiserror 2.0.17", - "windows 0.61.3", - "windows-future 0.2.1", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core 0.61.2", -] - -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core 0.62.2", -] - -[[package]] -name = "windows-core" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" -dependencies = [ - "windows-implement 0.57.0", - "windows-interface 0.57.0", - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading 0.1.0", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core 0.62.2", - "windows-link 0.2.1", - "windows-threading 0.2.1", -] - -[[package]] -name = "windows-implement" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-interface" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", -] - -[[package]] -name = "windows-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core 0.62.2", - "windows-link 0.2.1", -] - -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winnow" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" -dependencies = [ - "memchr", -] - -[[package]] -name = "winreg" -version = "0.55.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" -dependencies = [ - "cfg-if", - "windows-sys 0.59.0", -] - -[[package]] -name = "wio" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" -dependencies = [ - "winapi", -] - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.13.1", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "writeable" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" - -[[package]] -name = "x11" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "xcb" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f07c123b796139bfe0603e654eaf08e132e52387ba95b252c78bad3640ba37ea" -dependencies = [ - "bitflags 1.3.2", - "libc", - "quick-xml", - "x11", -] - -[[package]] -name = "xmlwriter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" - -[[package]] -name = "y4m" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" - -[[package]] -name = "yeslogic-fontconfig-sys" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503a066b4c037c440169d995b869046827dbc71263f6e8f3be6d77d4f3229dbd" -dependencies = [ - "dlib", - "once_cell", - "pkg-config", -] - -[[package]] -name = "yoke" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zed-font-kit" -version = "0.14.1-zed" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3898e450f36f852edda72e3f985c34426042c4951790b23b107f93394f9bff5" -dependencies = [ - "bitflags 2.13.1", - "byteorder", - "core-foundation 0.10.0", - "core-graphics 0.24.0", - "core-text", - "dirs", - "dwrote", - "float-ord", - "freetype-sys", - "lazy_static", - "libc", - "log", - "pathfinder_geometry", - "pathfinder_simd", - "walkdir", - "winapi", - "yeslogic-fontconfig-sys", -] - -[[package]] -name = "zed-scap" -version = "0.0.8-zed" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b338d705ae33a43ca00287c11129303a7a0aa57b101b72a1c08c863f698ac8" -dependencies = [ - "anyhow", - "cocoa", - "core-graphics-helmer-fork", - "log", - "objc", - "rand 0.8.6", - "screencapturekit", - "screencapturekit-sys", - "sysinfo", - "tao-core-video-sys", - "windows 0.61.3", - "windows-capture", - "x11", - "xcb", -] - -[[package]] -name = "zerocopy" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f212a141d820099d57ffafb9569be9617a6f27d3dc881fbee8fb56642f917a9" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" - -[[package]] -name = "zune-core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" - -[[package]] -name = "zune-inflate" -version = "0.2.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "zune-jpeg" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" -dependencies = [ - "zune-core", -] diff --git a/crates/gpui_pre/Cargo.toml b/crates/gpui_pre/Cargo.toml deleted file mode 100644 index d5b305c..0000000 --- a/crates/gpui_pre/Cargo.toml +++ /dev/null @@ -1,670 +0,0 @@ -# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO -# -# When uploading crates to the registry Cargo will automatically -# "normalize" Cargo.toml files for maximal compatibility -# with all versions of Cargo and also rewrite `path` dependencies -# to registry (e.g., crates.io) dependencies. -# -# If you are reading this file be aware that the original Cargo.toml -# will likely look very different (and much more reasonable). -# See Cargo.toml.orig for the original contents. - -[package] -edition = "2024" -name = "gpui-pre" -version = "0.3.3" -authors = ["Nathan Sobo "] -build = "build.rs" -publish = true -autolib = false -autobins = false -autoexamples = false -autotests = false -autobenches = false -description = "Zed's GPU-accelerated UI framework (gpui-pre snapshot of zed@5b055fa)" -homepage = "https://gpui.rs" -readme = "README.md" -keywords = [ - "desktop", - "gui", - "immediate", -] -categories = ["gui"] -license = "Apache-2.0" -repository = "https://github.com/zed-industries/zed" -resolver = "2" - -[package.metadata.cargo-shear] -ignored = [ - "bindgen", - "font-kit", - "getrandom", - "objc2", - "objc2-metal", - "tracing", -] -ignored-paths = ["src/_ownership_and_data_flow.rs"] - -# `ztracing::instrument` re-exports tracing's attribute when its cfg is on, and -# that macro expands to `tracing::` paths resolved in this crate; machete greps -# tokens and cannot see the expansion. The vendored manifest keeps upstream's -# direct dependency rather than drifting from the published crate. -[package.metadata.cargo-machete] -ignored = ["tracing"] - -[package.metadata.gpui-pre] -zed-crate = "gpui" -zed-version = "0.2.2" -zed-rev = "5b055fa789a8b8d38ac951a6e0cde272f66b4495" - -[features] -bench = ["bench-support"] -bench-support = [ - "profiler", - "dep:criterion", -] -default = [ - "font-kit", - "wayland", - "x11", - "windows-manifest", -] -inspector = ["gpui_macros/inspector"] -leak-detection = ["backtrace"] -profiler = ["dep:hdrhistogram"] -screen-capture = ["scap"] -stacker = ["dep:stacksafe"] -test-support = [ - "leak-detection", - "collections/test-support", - "http_client/test-support", - "wayland", - "x11", - "proptest", -] -wayland = [] -windows-manifest = ["dep:embed-resource"] -x11 = ["scap?/x11"] - -[lib] -name = "gpui" -path = "src/gpui.rs" -doctest = false - -[[example]] -name = "a11y" -path = "examples/a11y.rs" - -[[example]] -name = "active_state_bug" -path = "examples/active_state_bug.rs" - -[[example]] -name = "anchor" -path = "examples/anchor.rs" - -[[example]] -name = "animation" -path = "examples/animation.rs" - -[[example]] -name = "data_table" -path = "examples/data_table.rs" - -[[example]] -name = "drag_drop" -path = "examples/drag_drop.rs" - -[[example]] -name = "focus_visible" -path = "examples/focus_visible.rs" - -[[example]] -name = "gif_viewer" -path = "examples/gif_viewer.rs" - -[[example]] -name = "gradient" -path = "examples/gradient.rs" - -[[example]] -name = "grid_layout" -path = "examples/grid_layout.rs" - -[[example]] -name = "hello_world" -path = "examples/hello_world.rs" - -[[example]] -name = "image" -path = "examples/image/image.rs" - -[[example]] -name = "image_gallery" -path = "examples/image_gallery.rs" - -[[example]] -name = "image_loading" -path = "examples/image_loading.rs" - -[[example]] -name = "input" -path = "examples/input.rs" - -[[example]] -name = "layer_shell" -path = "examples/layer_shell.rs" - -[[example]] -name = "list_example" -path = "examples/list_example.rs" - -[[example]] -name = "mouse_pressure" -path = "examples/mouse_pressure.rs" - -[[example]] -name = "move_entity_between_windows" -path = "examples/move_entity_between_windows.rs" - -[[example]] -name = "on_window_close_quit" -path = "examples/on_window_close_quit.rs" - -[[example]] -name = "opacity" -path = "examples/opacity.rs" - -[[example]] -name = "ownership_post" -path = "examples/ownership_post.rs" - -[[example]] -name = "painting" -path = "examples/painting.rs" - -[[example]] -name = "paths_bench" -path = "examples/paths_bench.rs" - -[[example]] -name = "pattern" -path = "examples/pattern.rs" - -[[example]] -name = "popover" -path = "examples/popover.rs" - -[[example]] -name = "scrollable" -path = "examples/scrollable.rs" - -[[example]] -name = "set_menus" -path = "examples/set_menus.rs" - -[[example]] -name = "shadow" -path = "examples/shadow.rs" - -[[example]] -name = "svg" -path = "examples/svg/svg.rs" - -[[example]] -name = "system_notifications" -path = "examples/system_notifications.rs" - -[[example]] -name = "tab_stop" -path = "examples/tab_stop.rs" - -[[example]] -name = "testing" -path = "examples/testing.rs" - -[[example]] -name = "text" -path = "examples/text.rs" - -[[example]] -name = "text_layout" -path = "examples/text_layout.rs" - -[[example]] -name = "text_wrapper" -path = "examples/text_wrapper.rs" - -[[example]] -name = "tree" -path = "examples/tree.rs" - -[[example]] -name = "uniform_list" -path = "examples/uniform_list.rs" - -[[example]] -name = "view_example" -path = "examples/view_example/view_example_main.rs" - -[[example]] -name = "window" -path = "examples/window.rs" - -[[example]] -name = "window_movable" -path = "examples/window_movable.rs" - -[[example]] -name = "window_positioning" -path = "examples/window_positioning.rs" - -[[example]] -name = "window_shadow" -path = "examples/window_shadow.rs" - -[[test]] -name = "action_macros" -path = "tests/action_macros.rs" - -[dependencies.accesskit] -version = "0.24.0" -features = ["enumn"] - -[dependencies.anyhow] -version = "1.0.86" - -[dependencies.async-channel] -version = "2.5.0" - -[dependencies.async-task] -version = "4.7" - -[dependencies.backtrace] -version = "0.3" -optional = true - -[dependencies.bitflags] -version = "2.6.0" - -[dependencies.chrono] -version = "0.4" -features = ["serde"] - -[dependencies.collections] -version = "=0.3.3" -package = "gpui-pre-collections" - -[dependencies.criterion] -version = "0.5" -features = ["html_reports"] -optional = true - -[dependencies.ctor] -version = "1.0.12" - -[dependencies.derive_more] -version = "2.1.1" -features = [ - "add", - "add_assign", - "deref", - "deref_mut", - "display", - "from", - "from_str", - "mul", - "mul_assign", - "not", -] - -[dependencies.etagere] -version = "0.2" - -[dependencies.futures] -version = "0.3.32" - -[dependencies.futures-concurrency] -version = "7.7.1" - -[dependencies.gpui_macros] -version = "=0.3.3" -package = "gpui-pre-macros" - -[dependencies.gpui_shared_string] -version = "=0.3.3" -package = "gpui-pre-shared-string" - -[dependencies.gpui_util] -version = "=0.3.3" -package = "gpui-pre-util" - -[dependencies.hdrhistogram] -version = "7" -optional = true -default-features = false - -[dependencies.heapless] -version = "0.9.2" - -[dependencies.http_client] -version = "=0.3.3" -package = "gpui-pre-http-client" - -[dependencies.image] -version = "0.25.1" -features = [ - "bmp", - "dds", - "exr", - "ff", - "gif", - "hdr", - "ico", - "jpeg", - "png", - "pnm", - "qoi", - "rayon", - "tga", - "tiff", - "webp", -] -default-features = false - -[dependencies.inventory] -version = "0.3.19" - -[dependencies.itertools] -version = "0.14.0" - -[dependencies.log] -version = "0.4.16" -features = [ - "kv_unstable_serde", - "serde", -] - -[dependencies.lyon] -version = "1.0" - -[dependencies.num_cpus] -version = "1.13" - -[dependencies.parking] -version = "2.0.0" - -[dependencies.parking_lot] -version = "0.12.1" - -[dependencies.pin-project] -version = "1.1.10" - -[dependencies.pollster] -version = "0.4.0" - -[dependencies.postage] -version = "0.5" -features = ["futures-traits"] - -[dependencies.profiling] -version = "1" - -[dependencies.proptest] -version = "1" -features = ["attr-macro"] -optional = true -package = "proptest" - -[dependencies.rand] -version = "0.9" - -[dependencies.raw-window-handle] -version = "0.6" - -[dependencies.refineable] -version = "=0.3.3" -package = "gpui-pre-refineable" - -[dependencies.regex] -version = "1.5" - -[dependencies.resvg] -version = "0.46.0" -features = [ - "text", - "system-fonts", - "memmap-fonts", - "raster-images", -] -default-features = false - -[dependencies.scheduler] -version = "=0.3.3" -package = "gpui-pre-scheduler" - -[dependencies.schemars] -version = "1.0" -features = ["indexmap2"] - -[dependencies.seahash] -version = "4.1" - -[dependencies.serde] -version = "1.0.221" -features = [ - "derive", - "rc", -] - -[dependencies.serde_json] -version = "1.0.144" -features = [ - "preserve_order", - "raw_value", -] - -[dependencies.slotmap] -version = "1.0.6" - -[dependencies.smallvec] -version = "1.6" -features = [ - "union", - "const_new", -] - -[dependencies.spin] -version = "0.10.0" - -[dependencies.stacksafe] -version = "1.0" -optional = true - -[dependencies.strum] -version = "0.28" -features = ["derive"] - -[dependencies.sum_tree] -version = "=0.3.3" -package = "gpui-pre-sum-tree" - -[dependencies.taffy] -version = "=0.13.0" - -[dependencies.thiserror] -version = "2.0.12" - -[dependencies.tracing] -version = "0.1.40" - -[dependencies.ttf-parser] -version = "0.25" - -[dependencies.url] -version = "2.2" - -[dependencies.usvg] -version = "0.46.0" -default-features = false - -[dependencies.util_macros] -version = "=0.3.3" -package = "gpui-pre-util-macros" - -[dependencies.uuid] -version = "1.1.2" -features = [ - "v4", - "v5", - "v7", - "serde", -] - -[dependencies.waker-fn] -version = "1.2.0" - -[dependencies.web-time] -version = "1.1.0" - -[dependencies.ztracing] -version = "=0.3.3" -package = "gpui-pre-ztracing" - -[build-dependencies.embed-resource] -version = "3.0" -optional = true - -[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))'.dependencies.scap] -version = "0.0.8-zed" -optional = true -default-features = false -package = "zed-scap" - -[target.'cfg(target_family = "wasm")'.dependencies.getrandom] -version = "0.3.4" -features = ["wasm_js"] - -[target.'cfg(target_family = "wasm")'.dependencies.uuid] -version = "1.1.2" -features = [ - "v4", - "v5", - "v7", - "serde", - "js", -] - -[target.'cfg(target_os = "macos")'.dependencies.core-video] -version = "0.5.2" -features = ["metal"] - -[target.'cfg(target_os = "macos")'.dependencies.font-kit] -version = "0.14.1-zed" -optional = true -package = "zed-font-kit" - -[target.'cfg(target_os = "macos")'.dependencies.log] -version = "0.4.16" -features = [ - "kv_unstable_serde", - "serde", -] - -[target.'cfg(target_os = "macos")'.dependencies.objc2] -version = "0.6" -optional = true - -[target.'cfg(target_os = "macos")'.dependencies.objc2-metal] -version = "0.3" -optional = true - -[target.'cfg(target_os = "macos")'.build-dependencies.bindgen] -version = "0.72" - -[target.'cfg(target_os = "windows")'.dependencies.windows] -version = "0.62" -features = [ - "Data_Xml_Dom", - "Foundation_Numerics", - "Globalization_DateTimeFormatting", - "Storage_Search", - "Storage_Streams", - "System_Threading", - "UI_Notifications", - "UI_ViewManagement", - "Wdk_System_SystemServices", - "Win32_Foundation", - "Win32_Globalization", - "Win32_Graphics_Direct3D", - "Win32_Graphics_Direct3D11", - "Win32_Graphics_Direct3D_Fxc", - "Win32_Graphics_DirectComposition", - "Win32_Graphics_DirectWrite", - "Win32_Graphics_DirectManipulation", - "Win32_Graphics_Dwm", - "Win32_Graphics_Dxgi", - "Win32_Graphics_Dxgi_Common", - "Win32_Graphics_Gdi", - "Win32_Graphics_Imaging", - "Win32_Graphics_Hlsl", - "Win32_Networking_WinSock", - "Win32_Security", - "Win32_Security_Credentials", - "Win32_Security_Cryptography", - "Win32_Storage_FileSystem", - "Win32_Storage_Packaging_Appx", - "Win32_System_Com", - "Win32_System_Com_StructuredStorage", - "Win32_System_Console", - "Win32_System_Diagnostics_Debug", - "Win32_System_DataExchange", - "Win32_System_IO", - "Win32_System_JobObjects", - "Win32_System_LibraryLoader", - "Win32_System_Memory", - "Win32_System_Ole", - "Win32_System_Performance", - "Win32_System_Pipes", - "Win32_System_RestartManager", - "Win32_System_SystemInformation", - "Win32_System_SystemServices", - "Win32_System_Threading", - "Win32_System_Variant", - "Win32_System_WinRT", - "Win32_UI_Controls", - "Win32_UI_HiDpi", - "Win32_UI_Input_Ime", - "Win32_UI_Input_KeyboardAndMouse", - "Win32_UI_Input_Pointer", - "Win32_UI_Shell", - "Win32_UI_Shell_Common", - "Win32_UI_Shell_PropertiesSystem", - "Win32_UI_WindowsAndMessaging", - "Win32_Media", - "Win32_Foundation", - "Win32_System_Power", -] - -[lints.clippy] -dbg_macro = "deny" -declare_interior_mutable_const = "deny" -disallowed_methods = "deny" -large_enum_variant = "allow" -let_underscore_future = "allow" -nonminimal_bool = "allow" -redundant_clone = "deny" -single_range_in_vec_init = "allow" -todo = "deny" -too_many_arguments = "allow" -type_complexity = "allow" - -[lints.clippy.style] -level = "allow" -priority = -1 - -[lints.rust.unexpected_cfgs] -level = "allow" -priority = 0 - -[workspace] diff --git a/crates/gpui_pre/LICENSE-APACHE b/crates/gpui_pre/LICENSE-APACHE deleted file mode 100644 index 461a0fe..0000000 --- a/crates/gpui_pre/LICENSE-APACHE +++ /dev/null @@ -1,222 +0,0 @@ -Copyright 2022 - 2025 Zed Industries, Inc. - - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - - http://www.apache.org/licenses/LICENSE-2.0 - - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - - - -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 diff --git a/crates/gpui_pre/README.md b/crates/gpui_pre/README.md deleted file mode 100644 index 132e5ac..0000000 --- a/crates/gpui_pre/README.md +++ /dev/null @@ -1,98 +0,0 @@ -# Welcome to GPUI! - -GPUI is a hybrid immediate and retained mode, GPU accelerated, UI framework -for Rust, designed to support a wide variety of applications. - -## Getting Started - -GPUI is still in active development as we work on the Zed code editor, and is still pre-1.0. There will often be breaking changes between versions. You'll also need to use the latest version of stable Rust. Add `gpui`, and optionally `gpui_platform`, to your `Cargo.toml`: - -```toml -gpui = { version = "*" } -gpui_platform = { version = "*", features = ["font-kit", "wayland", "x11"] } -``` - -Everything in a standalone GPUI app starts with an `Application`. You can create one with `gpui_platform::application()`, which picks the windowing and text backends for the host OS, and kick off your application by passing a callback to `Application::run()`. Inside this callback, you can create a new window with `App::open_window()` and register your first root view. - -```rust,no_run -use gpui::*; - -fn main() { - gpui_platform::application().run(|cx: &mut App| { - // .. - }); -} -``` - -### `gpui_platform` - -The features on `gpui_platform` are platform-specific, so the list above is a safe cross-platform default. If you build for a single platform, you can trim it: - -- **macOS** — Rendering uses Metal and is always available, but glyph rasterization needs `font-kit`. Without it, GPUI falls back to a placeholder text system that lays text out but renders no glyphs. - - ```toml - gpui_platform = { version = "*", features = ["font-kit"] } - ``` - -- **Linux / FreeBSD** — enable at least one windowing backend for desktop windows: `wayland`, `x11`, or both. These features also compile the renderer and text system, so no separate text feature is needed. - - ```toml - gpui_platform = { version = "*", features = ["wayland", "x11"] } - ``` - -- **Windows** — no features are required. Windowing uses Win32 and text uses DirectWrite. `font-kit` has no effect here. - -### Additional Topics - -- [Ownership and data flow](_ownership_and_data_flow) -- [Accessibility](_accessibility) - -### Dependencies - -GPUI has various system dependencies that it needs in order to work. - -#### macOS - -On macOS, GPUI uses Metal for rendering. In order to use Metal, you need to do the following: - -- Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store, or from the [Apple Developer](https://developer.apple.com/download/all/) website. Note this requires a developer account. - -> Ensure you launch Xcode after installing, and install the macOS components, which is the default option. - -- Install [Xcode command line tools](https://developer.apple.com/xcode/resources/) - - ```sh - xcode-select --install - ``` - -- Ensure that the Xcode command line tools are using your newly installed copy of Xcode: - - ```sh - sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer - ``` - -## The Big Picture - -GPUI offers three different [registers]() depending on your needs: - -- State management and communication with `Entity`'s. Whenever you need to store application state that communicates between different parts of your application, you'll want to use GPUI's entities. Entities are owned by GPUI and are only accessible through an owned smart pointer similar to an `Rc`. See the `app::context` module for more information. - -- High level, declarative UI with views. All UI in GPUI starts with a view. A view is simply an `Entity` that can be rendered, by implementing the `Render` trait. At the start of each frame, GPUI will call this render method on the root view of a given window. Views build a tree of `elements`, lay them out and style them with a tailwind-style API, and then give them to GPUI to turn into pixels. See the `div` element for an all purpose swiss-army knife of rendering. - -- Low level, imperative UI with Elements. Elements are the building blocks of UI in GPUI, and they provide a nice wrapper around an imperative API that provides as much flexibility and control as you need. Elements have total control over how they and their child elements are rendered and can be used for making efficient views into large lists, implement custom layouting for a code editor, and anything else you can think of. See the `element` module for more information. - -Each of these registers has one or more corresponding contexts that can be accessed from all GPUI services. This context is your main interface to GPUI, and is used extensively throughout the framework. - -## Other Resources - -In addition to the systems above, GPUI provides a range of smaller services that are useful for building complex applications: - -- Actions are user-defined structs that are used for converting keystrokes into logical operations in your UI. Use this for implementing keyboard shortcuts, such as cmd-q. See the `action` module for more information. - -- Platform services, such as `quit the app` or `open a URL` are available as methods on the `app::App`. - -- An async executor that is integrated with the platform's event loop. See the `executor` module for more information., - -- The `[gpui::test]` macro provides a convenient way to write tests for your GPUI applications. Tests also have their own kind of context, a `TestAppContext` which provides ways of simulating common platform input. See `app::test_context` and `test` modules for more details. - -Currently, the best way to learn about these APIs is to read the Zed source code or drop a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog). diff --git a/crates/gpui_pre/build.rs b/crates/gpui_pre/build.rs deleted file mode 100644 index b1bfd21..0000000 --- a/crates/gpui_pre/build.rs +++ /dev/null @@ -1,23 +0,0 @@ -#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")] - -fn main() { - println!("cargo::rustc-check-cfg=cfg(gles)"); - - let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); - - if target_os == "windows" { - #[cfg(feature = "windows-manifest")] - embed_resource(); - } -} - -#[cfg(feature = "windows-manifest")] -fn embed_resource() { - let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml"); - let rc_file = std::path::Path::new("resources/windows/gpui.rc"); - println!("cargo:rerun-if-changed={}", manifest.display()); - println!("cargo:rerun-if-changed={}", rc_file.display()); - embed_resource::compile(rc_file, embed_resource::NONE) - .manifest_required() - .unwrap(); -} diff --git a/crates/gpui_pre/docs/contexts.md b/crates/gpui_pre/docs/contexts.md deleted file mode 100644 index 9295c77..0000000 --- a/crates/gpui_pre/docs/contexts.md +++ /dev/null @@ -1,33 +0,0 @@ -# Contexts - -GPUI makes extensive use of _context parameters_ (typically named `cx`) to provide access to application state and services. These contexts are references passed to functions, enabling interaction with global state, windows, entities, and system services. - ---- - -## `App` - -The root context granting access to the application's global state. This context owns all entities' data and can be used to read or update the data referenced by an `Entity`. - -## `Context` - -A context provided when interacting with an `Entity`, with additional methods related to that specific entity such as notifying observers and emitting events. This context dereferences into `App`, meaning any function which can take an `App` reference can also take a `Context` reference, allowing you to access the application's global state. - -## `AsyncApp` and `AsyncWindowContext` - -Whereas the above contexts are always passed to your code as references, you can call `to_async` on the reference to create an async context, which has a static lifetime and can be held across `await` points in async code. When you interact with entities with an async context, the calls become fallible, because the context may outlive the window or even the app itself. - -## `TestAppContext` - -These are similar to the async contexts above, but they panic if you attempt to access a non-existent app or window, and they also contain other features specific to tests. - ---- - -# Non-Context Core Types - -## `Window` - -Provides access to the state of an application window. This type has a root view (an `Entity` implementing `Render`) which it can read/update, but since it is not a context, you must pass a `&mut App` (or a context which dereferences to it) to do so, along with other functions interacting with global state. You can obtain a `Window` from an `WindowHandle` by calling `WindowHandle::update`. - -## `Entity` - -A handle to a structure requiring state. This data is owned by the `App` and can be accessed and modified via references to contexts. If `T` implements `Render`, then the entity is sometimes referred to as a view. Entities can be observed by other entities and windows, allowing a closure to be called when `notify` is called on the entity's `Context`. diff --git a/crates/gpui_pre/docs/key_dispatch.md b/crates/gpui_pre/docs/key_dispatch.md deleted file mode 100644 index ae7d828..0000000 --- a/crates/gpui_pre/docs/key_dispatch.md +++ /dev/null @@ -1,100 +0,0 @@ -# Key Dispatch - -GPUI is designed for keyboard-first interactivity. - -To expose functionality to the mouse, you render a button with a click handler. - -To expose functionality to the keyboard, you bind an _action_ in a _key context_. - -Actions are similar to framework-level events like `MouseDown`, `KeyDown`, etc, but you can define them yourself: - -```rust -mod menu { - #[gpui::action] - struct MoveUp; - - #[gpui::action] - struct MoveDown; -} -``` - -Actions are frequently unit structs, for which we have a macro. The above could also be written: - -```rust -mod menu { - actions!(gpui, [MoveUp, MoveDown]); -} -``` - -Actions can also be more complex types: - -```rust -mod menu { - #[gpui::action] - struct Move { - direction: Direction, - select: bool, - } -} -``` - -To bind actions, chain `on_action` on to your element: - -```rust -impl Render for Menu { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .on_action(|this: &mut Menu, move: &MoveUp, window: &mut Window, cx: &mut Context| { - // ... - }) - .on_action(|this, move: &MoveDown, cx| { - // ... - }) - .children(unimplemented!()) - } -} -``` - -In order to bind keys to actions, you need to declare a _key context_ for part of the element tree by calling `key_context`. - -```rust -impl Render for Menu { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .key_context("menu") - .on_action(|this: &mut Menu, move: &MoveUp, window: &mut Window, cx: &mut Context| { - // ... - }) - .on_action(|this, move: &MoveDown, cx| { - // ... - }) - .children(unimplemented!()) - } -} -``` - -Now you can target your context in the keymap. Note how actions are identified in the keymap by their fully-qualified type name. - -```json -{ - "context": "menu", - "bindings": { - "up": "menu::MoveUp", - "down": "menu::MoveDown" - } -} -``` - -If you had opted for the more complex type definition, you'd provide the serialized representation of the action alongside the name: - -```json -{ - "context": "menu", - "bindings": { - "up": ["menu::Move", {direction: "up", select: false}] - "down": ["menu::Move", {direction: "down", select: false}] - "shift-up": ["menu::Move", {direction: "up", select: true}] - "shift-down": ["menu::Move", {direction: "down", select: true}] - } -} -``` diff --git a/crates/gpui_pre/examples/README.md b/crates/gpui_pre/examples/README.md deleted file mode 100644 index dab1c30..0000000 --- a/crates/gpui_pre/examples/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# GPUI Examples - -Examples can be run from the Zed repository root: - -```sh -cargo run -p gpui --example hello_world -``` - -## Where to start - -- `hello_world` shows the basic shape of a GPUI application: create an - `Application`, open a window, create a root view, and render a `div`. -- `input` demonstrates text input, focus, selections, clipboard actions, and - keyboard bindings. -- `uniform_list` shows how to render a simple virtualized list. -- `testing` demonstrates `#[gpui::test]`, `TestAppContext`, actions, focus, and - window-based tests. - -## Layout and styling - -- `grid_layout` demonstrates CSS-grid-style layout. -- `opacity` demonstrates opacity styling. -- `pattern` shows patterned backgrounds. -- `shadow` demonstrates box shadows. -- `text` shows styled text rendering. -- `text_layout` demonstrates text alignment, decoration, weights, and wrapping. -- `text_wrapper` shows wrapping text content. - -## Interaction - -- `anchor` demonstrates anchored positioning. -- `data_table` combines virtualized list rendering with table-style rows and a - custom scrollbar. -- `drag_drop` shows draggable elements and drop targets. -- `focus_visible` demonstrates keyboard-visible focus styling. -- `mouse_pressure` demonstrates pressure-sensitive pointer input where supported. -- `popover` shows floating layers with `deferred` and `anchored`. -- `scrollable` demonstrates scrollable content. -- `tab_stop` shows keyboard tab navigation. - -## Images, drawing, and animation - -- `animation` demonstrates GPUI animations and animated SVG transforms. -- `gif_viewer` shows GIF rendering. -- `gradient` demonstrates linear gradients and color spaces. -- `image` shows local and remote image loading, image sizing, and asset setup. -- `image_gallery` demonstrates image caching and loading remote images. -- `image_loading` shows image loading states and asset loading. -- `painting` demonstrates custom drawing with paths and canvas. -- `svg` shows SVG rendering. - -## Windows and application behavior - -- `move_entity_between_windows` shows moving an entity between windows. -- `on_window_close_quit` demonstrates quitting when a window closes. -- `set_menus` shows application menu setup. -- `system_notifications` demonstrates posting, replacing, dismissing, and responding to operating-system notifications. -- `window` demonstrates creating normal, dialog, popup, and floating windows. -- `window_positioning` demonstrates window bounds and placement. -- `window_shadow` demonstrates window shadow styling. - -## Specialized examples - -These examples are useful when working on GPUI itself, but they may not be the -best starting point for new applications: - -- `active_state_bug` is a focused active-state reproduction. -- `layer_shell` demonstrates Linux layer-shell windows. -- `list_example` demonstrates bottom-aligned list state and scrollbar behavior. -- `ownership_post` supports the ownership and data-flow documentation. -- `paths_bench` is a path rendering benchmark. -- `tree` renders a deep tree of nested elements. diff --git a/crates/gpui_pre/examples/a11y.rs b/crates/gpui_pre/examples/a11y.rs deleted file mode 100644 index ff389f3..0000000 --- a/crates/gpui_pre/examples/a11y.rs +++ /dev/null @@ -1,272 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -//! Accessibility (AccessKit) demo app. -//! -//! Run with: `cargo run -p gpui --example a11y` -//! -//! Or on Linux: `cargo run -p gpui --features gpui_platform/wayland,gpui_platform/x11 --example a11y` -//! -//! This app uses GPUI's accessibility APIs to attach structured information to -//! the element tree, which allows assistive technology to see and interact with -//! the UI programmatically. -//! -//! The app behaves as follows: -//! - It opens a single window. -//! - The window's title is "GPUI Accessibility Demo". -//! - The window has a sequence of UI elements, stacked vertically: -//! - A heading with the text "Accessibility Demo". -//! - A row containing two elements: -//! - A spin button (role `SpinButton`) labelled "Counter: ", where -//! `` is the current count. It supports `Increment` and `Decrement` -//! accessible actions, and also increments on click. The numeric value -//! is clamped to a minimum of 0. -//! - A button labelled "Reset counter" that resets the count to 0. -//! - A row containing two elements: -//! - A switch, that can be toggled, and starts disabled. Toggling the switch -//! does nothing. -//! - The text "Enable feature". -//! - A "to-do" list, with three items, each represented with a `Text` element: -//! - "1. Write code" -//! - "2. Run tests" -//! - "3. Ship it" - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - AccessibleAction, App, Bounds, Context, FocusHandle, KeyBinding, Role, SharedString, Toggled, - Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, rgb, size, text, -}; -use gpui_platform::application; - -actions!(a11y_example, [Tab, TabPrev]); - -struct A11yDemo { - focus_handle: FocusHandle, - count: i32, - enabled: bool, -} - -impl A11yDemo { - fn new(window: &mut Window, cx: &mut Context) -> Self { - let focus_handle = cx.focus_handle(); - window.focus(&focus_handle, cx); - Self { - focus_handle, - count: 0, - enabled: false, - } - } -} - -impl Render for A11yDemo { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .id("root") - .role(Role::Application) - .aria_label("Accessibility Demo") - .track_focus(&self.focus_handle) - .on_action(cx.listener(|_, _: &Tab, window, cx| window.focus_next(cx))) - .on_action(cx.listener(|_, _: &TabPrev, window, cx| window.focus_prev(cx))) - .size_full() - .flex() - .flex_col() - .gap_4() - .p_4() - .bg(rgb(0x1e1e2e)) - .text_color(rgb(0xcdd6f4)) - // Heading - .child( - div() - .id("heading") - .role(Role::Heading) - .aria_level(1) - .aria_label("Accessibility Demo") - .text_xl() - .font_weight(gpui::FontWeight::BOLD) - .child(text!("Accessibility Demo")), - ) - // Counter — uses a SpinButton role with Increment/Decrement - // actions so screen readers can adjust the value directly. - // Click also works via the built-in handler. - .child( - div() - .flex() - .items_center() - .gap_3() - .child( - div() - .id("counter") - .focusable() - .tab_stop(true) - .role(Role::SpinButton) - .aria_label(SharedString::from(format!("Counter: {}", self.count))) - .aria_numeric_value(self.count as f64) - .aria_min_numeric_value(0.0) - .on_a11y_action(AccessibleAction::Increment, { - let this = cx.entity().downgrade(); - move |_, _, cx| { - this.update(cx, |this, cx| { - this.count += 1; - cx.notify(); - }) - .ok(); - } - }) - .on_a11y_action(AccessibleAction::Decrement, { - let this = cx.entity().downgrade(); - move |_, _, cx| { - this.update(cx, |this, cx| { - this.count = (this.count - 1).max(0); - cx.notify(); - }) - .ok(); - } - }) - .on_click(cx.listener(|this, _, _, cx| { - this.count += 1; - cx.notify(); - })) - .px_3() - .py_1() - .rounded_md() - .bg(rgb(0x89b4fa)) - .text_color(rgb(0x1e1e2e)) - .cursor_pointer() - .child(text!(format!("Count: {}", self.count))), - ) - .child( - div() - .id("reset") - .focusable() - .tab_stop(true) - .role(Role::Button) - .aria_label("Reset counter") - .px_3() - .py_1() - .rounded_md() - .bg(rgb(0x585b70)) - .cursor_pointer() - .on_click(cx.listener(|this, _, _, cx| { - this.count = 0; - cx.notify(); - })) - .child(text!("Reset")), - ), - ) - // A toggle switch - .child( - div() - .flex() - .items_center() - .gap_2() - .child( - div() - .id("toggle") - .focusable() - .tab_stop(true) - .role(Role::Switch) - .aria_label("Enable feature") - .aria_toggled(if self.enabled { - Toggled::True - } else { - Toggled::False - }) - .w(px(44.)) - .h(px(24.)) - .rounded_full() - .cursor_pointer() - .when(self.enabled, |el| el.bg(rgb(0x89b4fa))) - .when(!self.enabled, |el| el.bg(rgb(0x585b70))) - .child( - div() - .size(px(20.)) - .rounded_full() - .bg(gpui::white()) - .mt(px(2.)) - .when(self.enabled, |el| el.ml(px(22.))) - .when(!self.enabled, |el| el.ml(px(2.))), - ) - .on_click(cx.listener(|this, _, _, cx| { - this.enabled = !this.enabled; - cx.notify(); - })), - ) - .child(text!("Enable feature")), - ) - // A short list - .child( - div() - .id("task-list") - .role(Role::List) - .aria_label("Tasks") - .flex() - .flex_col() - .gap_1() - .children( - ["Write code", "Run tests", "Ship it"] - .iter() - .enumerate() - .map(|(i, label)| { - div() - .id(("task", i)) - .role(Role::ListItem) - .aria_label(SharedString::from(*label)) - .aria_position_in_set(i + 1) - .aria_size_of_set(3) - .py_1() - .px_2() - // Note: even though this `text!` macro - // produces multiple elements, it doesn't - // need its own unique ID because the parent - // div has different IDs for each string. - .child(text!(format!("{}. {}", i + 1, label))) - }), - ), - ) - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - cx.bind_keys([ - KeyBinding::new("tab", Tab, None), - KeyBinding::new("shift-tab", TabPrev, None), - ]); - - let bounds = Bounds::centered(None, size(px(500.), px(400.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - titlebar: Some(gpui::TitlebarOptions { - title: Some("GPUI Accessibility Demo".into()), - ..Default::default() - }), - ..Default::default() - }, - |window, cx| cx.new(|cx| A11yDemo::new(window, cx)), - ) - .unwrap(); - - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - env_logger::builder() - .filter_level(log::LevelFilter::Warn) - .filter_module("gpui", log::LevelFilter::Info) - .init(); - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/active_state_bug.rs b/crates/gpui_pre/examples/active_state_bug.rs deleted file mode 100644 index f767ed2..0000000 --- a/crates/gpui_pre/examples/active_state_bug.rs +++ /dev/null @@ -1,47 +0,0 @@ -/// Click the button — the `.active()` background gets stuck on every other click. -use gpui::*; -use gpui_platform::application; - -struct Example; - -impl Render for Example { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - // Colors from Zed's default dark theme - let bg = hsla(215. / 360., 0.12, 0.15, 1.); - let text = hsla(221. / 360., 0.11, 0.86, 1.); - let hover = hsla(225. / 360., 0.118, 0.267, 1.); - let active = hsla(220. / 360., 0.118, 0.20, 1.); - - div().bg(bg).size_full().p_1().child( - div() - .id("button") - .px_2() - .py_0p5() - .rounded_md() - .text_sm() - .text_color(text) - .hover(|s| s.bg(hover)) - .active(|s| s.bg(active)) - .on_click(|_, _, _| {}) - .child("Click me"), - ) - } -} - -fn main() { - application().run(|cx: &mut App| { - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(Bounds::centered( - None, - size(px(200.), px(60.)), - cx, - ))), - ..Default::default() - }, - |_, cx| cx.new(|_| Example), - ) - .unwrap(); - cx.activate(true); - }); -} diff --git a/crates/gpui_pre/examples/anchor.rs b/crates/gpui_pre/examples/anchor.rs deleted file mode 100644 index b4b280b..0000000 --- a/crates/gpui_pre/examples/anchor.rs +++ /dev/null @@ -1,207 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - Anchor, AnchoredPositionMode, App, Axis, Bounds, Context, Half as _, InteractiveElement, - ParentElement, Pixels, Point, Render, SharedString, Size, Window, WindowBounds, WindowOptions, - anchored, deferred, div, point, prelude::*, px, rgb, size, -}; -use gpui_platform::application; - -struct AnchorDemo { - hovered_button: Option, -} - -struct ButtonDemo { - label: SharedString, - corner: Option, -} - -fn resolved_position(corner: Anchor, button_size: Size) -> Point { - let offset = Point { - x: px(0.), - y: -button_size.height, - }; - - offset - + match corner.other_side_along(Axis::Vertical) { - Anchor::TopLeft => point(px(0.0), px(0.0)), - Anchor::TopCenter => point(button_size.width.half(), px(0.0)), - Anchor::TopRight => point(button_size.width, px(0.0)), - Anchor::LeftCenter => point(button_size.width, button_size.height.half()), - Anchor::RightCenter => point(px(0.), button_size.height.half()), - Anchor::BottomLeft => point(px(0.0), button_size.height), - Anchor::BottomCenter => point(button_size.width / 2.0, button_size.height), - Anchor::BottomRight => point(button_size.width, button_size.height), - } -} - -impl AnchorDemo { - fn buttons() -> Vec { - vec![ - ButtonDemo { - label: "TopLeft".into(), - corner: Some(Anchor::TopLeft), - }, - ButtonDemo { - label: "TopCenter".into(), - corner: Some(Anchor::TopCenter), - }, - ButtonDemo { - label: "TopRight".into(), - corner: Some(Anchor::TopRight), - }, - ButtonDemo { - label: "LeftCenter".into(), - corner: Some(Anchor::LeftCenter), - }, - ButtonDemo { - label: "Center".into(), - corner: None, - }, - ButtonDemo { - label: "RightCenter".into(), - corner: Some(Anchor::RightCenter), - }, - ButtonDemo { - label: "BottomLeft".into(), - corner: Some(Anchor::BottomLeft), - }, - ButtonDemo { - label: "BottomCenter".into(), - corner: Some(Anchor::BottomCenter), - }, - ButtonDemo { - label: "BottomRight".into(), - corner: Some(Anchor::BottomRight), - }, - ] - } -} - -impl Render for AnchorDemo { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let buttons = Self::buttons(); - let button_size = size(px(120.0), px(65.0)); - - div() - .flex() - .flex_col() - .size_full() - .items_center() - .justify_center() - .bg(gpui::white()) - .gap_4() - .p_10() - .child("Popover with Anchor") - .child( - div() - .size_128() - .grid() - .grid_cols(3) - .gap_6() - .relative() - .children(buttons.iter().enumerate().map(|(index, button)| { - let is_hovered = self.hovered_button == Some(index); - let is_hoverable = button.corner.is_some(); - div() - .relative() - .child( - div() - .id(("button", index)) - .w(button_size.width) - .h(button_size.height) - .flex() - .items_center() - .justify_center() - .bg(gpui::white()) - .when(is_hoverable, |this| { - this.border_1() - .rounded_lg() - .border_color(gpui::black()) - .hover(|style| { - style.bg(gpui::black()).text_color(gpui::white()) - }) - .on_hover(cx.listener( - move |this, hovered, _window, cx| { - if *hovered { - this.hovered_button = Some(index); - } else if this.hovered_button == Some(index) { - this.hovered_button = None; - } - cx.notify(); - }, - )) - .child(button.label.clone()) - }), - ) - .when_some(self.hovered_button.filter(|_| is_hovered), |this, index| { - let button = &buttons[index]; - let Some(corner) = button.corner else { - return this; - }; - - let position = resolved_position(corner, button_size); - this.child(deferred( - anchored() - .anchor(corner) - .position(position) - .position_mode(AnchoredPositionMode::Local) - .snap_to_window() - .child( - div() - .py_0p5() - .px_2() - .bg(gpui::black().opacity(0.75)) - .text_color(rgb(0xffffff)) - .rounded_sm() - .shadow_sm() - .min_w(px(100.0)) - .text_sm() - .child(button.label.clone()), - ), - )) - }) - })), - ) - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(Bounds::centered( - None, - size(px(750.), px(600.)), - cx, - ))), - ..Default::default() - }, - |_, cx| { - cx.new(|_| AnchorDemo { - hovered_button: None, - }) - }, - ) - .unwrap(); - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/animation.rs b/crates/gpui_pre/examples/animation.rs deleted file mode 100644 index e055b7d..0000000 --- a/crates/gpui_pre/examples/animation.rs +++ /dev/null @@ -1,373 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use std::time::Duration; - -use anyhow::Result; -use gpui::{ - Animation, AnimationExt as _, AnimationPhase, App, AssetSource, Bounds, Context, MouseButton, - MouseDownEvent, MouseMoveEvent, Pixels, SharedString, SpringAnimation, SpringConfig, - Transformation, Window, WindowBounds, WindowOptions, bounce, div, ease_in_out, percentage, - prelude::*, px, relative, rgba, size, svg, -}; -use gpui_platform::application; - -struct Assets {} - -impl AssetSource for Assets { - fn load(&self, path: &str) -> Result>> { - std::fs::read(path) - .map(Into::into) - .map_err(Into::into) - .map(Some) - } - - fn list(&self, path: &str) -> Result> { - Ok(std::fs::read_dir(path)? - .filter_map(|entry| { - Some(SharedString::from( - entry.ok()?.path().to_string_lossy().into_owned(), - )) - }) - .collect::>()) - } -} - -const ARROW_CIRCLE_SVG: &str = concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/image/arrow_circle.svg" -); - -struct AnimationExample { - spring_phase: u8, - spring_damping: f32, - spring_damping_drag: Option<(Pixels, f32)>, -} - -impl Render for AnimationExample { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - const MINIMUM_DAMPING: f32 = 2.0; - const MAXIMUM_DAMPING: f32 = 32.0; - const DAMPING_SLIDER_WIDTH: Pixels = px(224.0); - - let spring_phase = self.spring_phase; - let spring_position = px(98.0 * f32::from(spring_phase)); - let spring_damping = self.spring_damping; - let spring = SpringConfig::new(170.0, spring_damping, 1.0); - let (_, damping_ratio) = spring.canonical(); - let damping_fraction = - (spring_damping - MINIMUM_DAMPING) / (MAXIMUM_DAMPING - MINIMUM_DAMPING); - - div() - .flex() - .flex_col() - .size_full() - .bg(gpui::white()) - .text_color(gpui::black()) - .justify_around() - .child( - div() - .flex() - .flex_col() - .size_full() - .justify_around() - .child( - div() - .id("content") - .flex() - .flex_col() - .h(px(150.)) - .overflow_y_scroll() - .w_full() - .flex_1() - .justify_center() - .items_center() - .text_xl() - .gap_4() - .child("Hello Animation") - .child( - div() - .id("spring-demo") - .flex() - .flex_col() - .gap_2() - .p_2() - .w(px(240.0)) - .rounded_md() - .bg(rgba(0x00000010)) - .cursor_pointer() - .on_mouse_down( - MouseButton::Left, - cx.listener(|this, _, _, cx| { - this.spring_phase = (this.spring_phase + 1) % 3; - cx.notify(); - }), - ) - .child(format!( - "Target phase {spring_phase}: click rapidly to redirect momentum" - )) - .child( - div() - .id("spring-damping-controls") - .flex() - .flex_col() - .gap_1() - .text_sm() - .on_click(|_, _, cx| cx.stop_propagation()) - .child(format!( - "Drag damping: {spring_damping:.1} (ζ {damping_ratio:.2})" - )) - .child( - div() - .id("spring-damping") - .relative() - .h(px(20.0)) - .w(DAMPING_SLIDER_WIDTH) - .cursor_pointer() - .on_mouse_down( - MouseButton::Left, - cx.listener( - |this, - event: &MouseDownEvent, - _, - cx| { - this.spring_damping_drag = Some(( - event.position.x, - this.spring_damping, - )); - cx.stop_propagation(); - }, - ), - ) - .on_mouse_move(cx.listener( - |this, - event: &MouseMoveEvent, - _, - cx| { - let Some(( - start_position, - start_damping, - )) = this.spring_damping_drag - else { - return; - }; - - let delta = (event.position.x - - start_position) - / DAMPING_SLIDER_WIDTH; - this.spring_damping = (start_damping - + delta - * (MAXIMUM_DAMPING - - MINIMUM_DAMPING)) - .clamp( - MINIMUM_DAMPING, - MAXIMUM_DAMPING, - ); - cx.stop_propagation(); - cx.notify(); - }, - )) - .on_mouse_up( - MouseButton::Left, - cx.listener(|this, _, _, cx| { - this.spring_damping_drag = None; - cx.stop_propagation(); - }), - ) - .on_mouse_up_out( - MouseButton::Left, - cx.listener(|this, _, _, cx| { - this.spring_damping_drag = None; - cx.stop_propagation(); - }), - ) - .child( - div() - .absolute() - .top(px(8.0)) - .h(px(4.0)) - .w_full() - .rounded_full() - .bg(rgba(0x00000030)), - ) - .child( - div() - .absolute() - .top(px(8.0)) - .h(px(4.0)) - .w(relative(damping_fraction)) - .rounded_full() - .bg(rgba(0x3b82f6ff)), - ) - .child( - div() - .absolute() - .top(px(3.0)) - .left(relative(damping_fraction)) - .ml(px(-7.0)) - .size(px(14.0)) - .rounded_full() - .bg(rgba(0x2563ebff)), - ), - ), - ) - .child( - div().relative().h(px(32.0)).w_full().child( - div() - .absolute() - .top_1() - .size(px(24.0)) - .rounded_full() - .bg(rgba(0x3b82f6ff)) - .cursor_pointer() - .debug_selector(|| "spring-position".into()) - .with_spring( - "spring-position", - SpringAnimation::new(spring) - .to(spring_position) - .with_epsilon(0.25), - |this, left| this.left(left), - ), - ), - ) - .child( - div().h(px(24.0)).rounded_sm().with_spring( - "spring-phase", - SpringAnimation::new(spring) - .to(AnimationPhase(f32::from(spring_phase))) - .with_epsilon(0.001), - |this, phase| { - let (width, color) = if phase.0 <= 1.0 { - ( - phase.interpolate_between( - 0.0..=1.0, - px(48.0), - px(224.0), - ), - phase.interpolate_between_clamped( - 0.0..=1.0, - rgba(0xf97316ff), - rgba(0x22c55eff), - ), - ) - } else { - ( - phase.interpolate_between( - 1.0..=2.0, - px(224.0), - px(96.0), - ), - phase.interpolate_between_clamped( - 1.0..=2.0, - rgba(0x22c55eff), - rgba(0xa855f7ff), - ), - ) - }; - this.w(width).bg(color) - }, - ), - ), - ) - .child( - svg() - .size_20() - .overflow_hidden() - .path(ARROW_CIRCLE_SVG) - .text_color(gpui::black()) - .with_animation( - "image_circle", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(bounce(ease_in_out)), - |svg, delta| { - svg.with_transformation(Transformation::rotate( - percentage(delta), - )) - }, - ), - ), - ) - .child( - div() - .flex() - .h(px(64.)) - .w_full() - .p_2() - .justify_center() - .items_center() - .border_t_1() - .border_color(gpui::black().opacity(0.1)) - .bg(gpui::black().opacity(0.05)) - .child("Other Panel"), - ), - ) - } -} - -fn run_example() { - application().with_assets(Assets {}).run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let options = WindowOptions { - window_bounds: Some(WindowBounds::Windowed(Bounds::centered( - None, - size(px(300.), px(300.)), - cx, - ))), - ..Default::default() - }; - cx.open_window(options, |_, cx| { - cx.activate(false); - cx.new(|_| AnimationExample { - spring_phase: 0, - spring_damping: 14.0, - spring_damping_drag: None, - }) - }) - .unwrap(); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} - -#[cfg(all(test, feature = "test-support"))] -mod tests { - use gpui::{Modifiers, TestAppContext}; - - use super::*; - - #[gpui::test] - fn clicking_spring_position_changes_the_target(cx: &mut TestAppContext) { - let (view, cx) = cx.add_window_view(|_, _| AnimationExample { - spring_phase: 0, - spring_damping: 14.0, - spring_damping_drag: None, - }); - cx.simulate_resize(size(px(300.0), px(300.0))); - cx.run_until_parked(); - - let position_bounds = cx - .debug_bounds("spring-position") - .expect("spring position should be rendered"); - let position = position_bounds.center(); - cx.simulate_mouse_move(position, None, Modifiers::default()); - cx.simulate_click(position, Modifiers::default()); - cx.run_until_parked(); - - assert_eq!(cx.update(|_, cx| view.read(cx).spring_phase), 1); - } -} diff --git a/crates/gpui_pre/examples/data_table.rs b/crates/gpui_pre/examples/data_table.rs deleted file mode 100644 index 2bee476..0000000 --- a/crates/gpui_pre/examples/data_table.rs +++ /dev/null @@ -1,494 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use std::{ops::Range, rc::Rc, time::Duration}; - -use gpui::{ - App, Bounds, Context, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, Render, - SharedString, UniformListScrollHandle, Window, WindowBounds, WindowOptions, canvas, div, point, - prelude::*, px, rgb, size, uniform_list, -}; -use gpui_platform::application; - -const TOTAL_ITEMS: usize = 10000; -const SCROLLBAR_THUMB_WIDTH: Pixels = px(8.); -const SCROLLBAR_THUMB_HEIGHT: Pixels = px(100.); - -pub struct Quote { - name: SharedString, - symbol: SharedString, - last_done: f64, - prev_close: f64, - open: f64, - high: f64, - low: f64, - timestamp: Duration, - volume: i64, - turnover: f64, - ttm: f64, - market_cap: f64, - float_cap: f64, - shares: f64, - pb: f64, - pe: f64, - eps: f64, - dividend: f64, - dividend_yield: f64, - dividend_per_share: f64, - dividend_date: SharedString, - dividend_payment: f64, -} - -impl Quote { - pub fn random() -> Self { - use rand::Rng; - let mut rng = rand::rng(); - // simulate a base price in a realistic range - let prev_close = rng.random_range(100.0..200.0); - let change = rng.random_range(-5.0..5.0); - let last_done = prev_close + change; - let open = prev_close + rng.random_range(-3.0..3.0); - let high = (prev_close + rng.random_range::(0.0..10.0)).max(open); - let low = (prev_close - rng.random_range::(0.0..10.0)).min(open); - let timestamp = Duration::from_secs(rng.random_range(0..86400)); - let volume = rng.random_range(1_000_000..100_000_000); - let turnover = last_done * volume as f64; - let symbol = { - let mut ticker = String::new(); - if rng.random_bool(0.5) { - ticker.push_str(&format!( - "{:03}.{}", - rng.random_range(100..1000), - rng.random_range(0..10) - )); - } else { - ticker.push_str(&format!( - "{}{}", - rng.random_range('A'..='Z'), - rng.random_range('A'..='Z') - )); - } - ticker.push_str(&format!(".{}", rng.random_range('A'..='Z'))); - ticker - }; - let name = format!( - "{} {} - #{}", - symbol, - rng.random_range(1..100), - rng.random_range(10000..100000) - ); - let ttm = rng.random_range(0.0..10.0); - let market_cap = rng.random_range(1_000_000.0..10_000_000.0); - let float_cap = market_cap + rng.random_range(1_000.0..10_000.0); - let shares = rng.random_range(100.0..1000.0); - let pb = market_cap / shares; - let pe = market_cap / shares; - let eps = market_cap / shares; - let dividend = rng.random_range(0.0..10.0); - let dividend_yield = rng.random_range(0.0..10.0); - let dividend_per_share = rng.random_range(0.0..10.0); - let dividend_date = SharedString::new(format!( - "{}-{}-{}", - rng.random_range(2000..2023), - rng.random_range(1..12), - rng.random_range(1..28) - )); - let dividend_payment = rng.random_range(0.0..10.0); - - Self { - name: name.into(), - symbol: symbol.into(), - last_done, - prev_close, - open, - high, - low, - timestamp, - volume, - turnover, - pb, - pe, - eps, - ttm, - market_cap, - float_cap, - shares, - dividend, - dividend_yield, - dividend_per_share, - dividend_date, - dividend_payment, - } - } - - fn change(&self) -> f64 { - (self.last_done - self.prev_close) / self.prev_close * 100.0 - } - - fn change_color(&self) -> gpui::Hsla { - if self.change() > 0.0 { - gpui::green() - } else { - gpui::red() - } - } - - fn turnover_ratio(&self) -> f64 { - self.volume as f64 / self.turnover * 100.0 - } -} - -#[derive(IntoElement)] -struct TableRow { - ix: usize, - quote: Rc, -} -impl TableRow { - fn new(ix: usize, quote: Rc) -> Self { - Self { ix, quote } - } - - fn render_cell(&self, key: &str, width: Pixels, color: gpui::Hsla) -> impl IntoElement { - div() - .whitespace_nowrap() - .truncate() - .w(width) - .px_1() - .child(match key { - "id" => div().child(format!("{}", self.ix)), - "symbol" => div().child(self.quote.symbol.clone()), - "name" => div().child(self.quote.name.clone()), - "last_done" => div() - .text_color(color) - .child(format!("{:.3}", self.quote.last_done)), - "prev_close" => div() - .text_color(color) - .child(format!("{:.3}", self.quote.prev_close)), - "change" => div() - .text_color(color) - .child(format!("{:.2}%", self.quote.change())), - "timestamp" => div() - .text_color(color) - .child(format!("{:?}", self.quote.timestamp.as_secs())), - "open" => div() - .text_color(color) - .child(format!("{:.2}", self.quote.open)), - "low" => div() - .text_color(color) - .child(format!("{:.2}", self.quote.low)), - "high" => div() - .text_color(color) - .child(format!("{:.2}", self.quote.high)), - "ttm" => div() - .text_color(color) - .child(format!("{:.2}", self.quote.ttm)), - "eps" => div() - .text_color(color) - .child(format!("{:.2}", self.quote.eps)), - "market_cap" => { - div().child(format!("{:.2} M", self.quote.market_cap / 1_000_000.0)) - } - "float_cap" => div().child(format!("{:.2} M", self.quote.float_cap / 1_000_000.0)), - "turnover" => div().child(format!("{:.2} M", self.quote.turnover / 1_000_000.0)), - "volume" => div().child(format!("{:.2} M", self.quote.volume as f64 / 1_000_000.0)), - "turnover_ratio" => div().child(format!("{:.2}%", self.quote.turnover_ratio())), - "pe" => div().child(format!("{:.2}", self.quote.pe)), - "pb" => div().child(format!("{:.2}", self.quote.pb)), - "shares" => div().child(format!("{:.2}", self.quote.shares)), - "dividend" => div().child(format!("{:.2}", self.quote.dividend)), - "yield" => div().child(format!("{:.2}%", self.quote.dividend_yield)), - "dividend_per_share" => { - div().child(format!("{:.2}", self.quote.dividend_per_share)) - } - "dividend_date" => div().child(format!("{}", self.quote.dividend_date)), - "dividend_payment" => div().child(format!("{:.2}", self.quote.dividend_payment)), - _ => div().child("--"), - }) - } -} - -const FIELDS: [(&str, f32); 24] = [ - ("id", 64.), - ("symbol", 64.), - ("name", 180.), - ("last_done", 80.), - ("prev_close", 80.), - ("open", 80.), - ("low", 80.), - ("high", 80.), - ("ttm", 50.), - ("market_cap", 96.), - ("float_cap", 96.), - ("turnover", 120.), - ("volume", 100.), - ("turnover_ratio", 96.), - ("pe", 64.), - ("pb", 64.), - ("eps", 64.), - ("shares", 96.), - ("dividend", 64.), - ("yield", 64.), - ("dividend_per_share", 64.), - ("dividend_date", 96.), - ("dividend_payment", 64.), - ("timestamp", 120.), -]; - -impl RenderOnce for TableRow { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let color = self.quote.change_color(); - div() - .flex() - .flex_row() - .border_b_1() - .border_color(rgb(0xE0E0E0)) - .bg(if self.ix.is_multiple_of(2) { - rgb(0xFFFFFF) - } else { - rgb(0xFAFAFA) - }) - .py_0p5() - .px_2() - .children(FIELDS.map(|(key, width)| self.render_cell(key, px(width), color))) - } -} - -struct DataTable { - /// Use `Rc` to share the same quote data across multiple items, avoid cloning. - quotes: Vec>, - visible_range: Range, - scroll_handle: UniformListScrollHandle, - /// The position in thumb bounds when dragging start mouse down. - drag_position: Option>, -} - -impl DataTable { - fn new() -> Self { - Self { - quotes: Vec::new(), - visible_range: 0..0, - scroll_handle: UniformListScrollHandle::new(), - drag_position: None, - } - } - - fn generate(&mut self) { - self.quotes = (0..TOTAL_ITEMS).map(|_| Rc::new(Quote::random())).collect(); - } - - fn table_bounds(&self) -> Bounds { - self.scroll_handle.0.borrow().base_handle.bounds() - } - - fn scroll_top(&self) -> Pixels { - self.scroll_handle.0.borrow().base_handle.offset().y - } - - fn scroll_height(&self) -> Pixels { - self.scroll_handle - .0 - .borrow() - .last_item_size - .unwrap_or_default() - .contents - .height - } - - fn render_scrollbar(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let scroll_height = self.scroll_height(); - let table_bounds = self.table_bounds(); - let table_height = table_bounds.size.height; - if table_height == px(0.) { - return div().id("scrollbar"); - } - - let percentage = -self.scroll_top() / scroll_height; - let offset_top = (table_height * percentage).clamp( - px(4.), - (table_height - SCROLLBAR_THUMB_HEIGHT - px(4.)).max(px(4.)), - ); - let entity = cx.entity(); - let scroll_handle = self.scroll_handle.0.borrow().base_handle.clone(); - - div() - .id("scrollbar") - .absolute() - .top(offset_top) - .right_1() - .h(SCROLLBAR_THUMB_HEIGHT) - .w(SCROLLBAR_THUMB_WIDTH) - .bg(rgb(0xC0C0C0)) - .hover(|this| this.bg(rgb(0xA0A0A0))) - .rounded_lg() - .child( - canvas( - |_, _, _| (), - move |thumb_bounds, _, window, _| { - window.on_mouse_event({ - let entity = entity.clone(); - move |ev: &MouseDownEvent, _, _, cx| { - if !thumb_bounds.contains(&ev.position) { - return; - } - - entity.update(cx, |this, _| { - this.drag_position = Some( - ev.position - thumb_bounds.origin - table_bounds.origin, - ); - }) - } - }); - window.on_mouse_event({ - let entity = entity.clone(); - move |_: &MouseUpEvent, _, _, cx| { - entity.update(cx, |this, _| { - this.drag_position = None; - }) - } - }); - - window.on_mouse_event(move |ev: &MouseMoveEvent, _, _, cx| { - if !ev.dragging() { - return; - } - - let Some(drag_pos) = entity.read(cx).drag_position else { - return; - }; - - let inside_offset = drag_pos.y; - let percentage = ((ev.position.y - table_bounds.origin.y - + inside_offset) - / (table_bounds.size.height)) - .clamp(0., 1.); - - let offset_y = ((scroll_height - table_bounds.size.height) - * percentage) - .clamp(px(0.), scroll_height - SCROLLBAR_THUMB_HEIGHT); - scroll_handle.set_offset(point(px(0.), -offset_y)); - cx.notify(entity.entity_id()); - }) - }, - ) - .size_full(), - ) - } -} - -impl Render for DataTable { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .bg(gpui::white()) - .text_sm() - .size_full() - .p_4() - .gap_2() - .flex() - .flex_col() - .child(format!( - "Total {} items, visible range: {:?}", - self.quotes.len(), - self.visible_range - )) - .child( - div() - .flex() - .flex_col() - .flex_1() - .overflow_hidden() - .border_1() - .border_color(rgb(0xE0E0E0)) - .rounded_sm() - .child( - div() - .flex() - .flex_row() - .w_full() - .overflow_hidden() - .border_b_1() - .border_color(rgb(0xE0E0E0)) - .text_color(rgb(0x555555)) - .bg(rgb(0xF0F0F0)) - .py_1() - .px_2() - .text_xs() - .children(FIELDS.map(|(key, width)| { - div() - .whitespace_nowrap() - .flex_shrink_0() - .truncate() - .px_1() - .w(px(width)) - .child(key.replace("_", " ").to_uppercase()) - })), - ) - .child( - div() - .relative() - .size_full() - .child( - uniform_list( - "items", - self.quotes.len(), - cx.processor(move |this, range: Range, _, _| { - this.visible_range = range.clone(); - let mut items = Vec::with_capacity(range.end - range.start); - for i in range { - if let Some(quote) = this.quotes.get(i) { - items.push(TableRow::new(i, quote.clone())); - } - } - items - }), - ) - .size_full() - .track_scroll(&self.scroll_handle), - ) - .child(self.render_scrollbar(window, cx)), - ), - ) - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - cx.open_window( - WindowOptions { - focus: true, - window_bounds: Some(WindowBounds::Windowed(Bounds::centered( - None, - size(px(1280.0), px(1000.0)), - cx, - ))), - ..Default::default() - }, - |_, cx| { - cx.new(|_| { - let mut table = DataTable::new(); - table.generate(); - table - }) - }, - ) - .unwrap(); - - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/drag_drop.rs b/crates/gpui_pre/examples/drag_drop.rs deleted file mode 100644 index 8f038ef..0000000 --- a/crates/gpui_pre/examples/drag_drop.rs +++ /dev/null @@ -1,158 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - App, Bounds, Context, Half, Hsla, Pixels, Point, Window, WindowBounds, WindowOptions, div, - prelude::*, px, rgb, size, -}; -use gpui_platform::application; - -#[derive(Clone, Copy)] -struct DragInfo { - ix: usize, - color: Hsla, - position: Point, -} - -impl DragInfo { - fn new(ix: usize, color: Hsla) -> Self { - Self { - ix, - color, - position: Point::default(), - } - } - - fn position(mut self, pos: Point) -> Self { - self.position = pos; - self - } -} - -impl Render for DragInfo { - fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement { - let size = gpui::size(px(120.), px(50.)); - - div() - .pl(self.position.x - size.width.half()) - .pt(self.position.y - size.height.half()) - .child( - div() - .flex() - .justify_center() - .items_center() - .w(size.width) - .h(size.height) - .bg(self.color.opacity(0.5)) - .text_color(gpui::white()) - .text_xs() - .shadow_md() - .child(format!("Item {}", self.ix)), - ) - } -} - -struct DragDrop { - drop_on: Option, -} - -impl DragDrop { - fn new() -> Self { - Self { drop_on: None } - } -} - -impl Render for DragDrop { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let items = [gpui::blue(), gpui::red(), gpui::green()]; - - div() - .size_full() - .flex() - .flex_col() - .gap_5() - .bg(gpui::white()) - .justify_center() - .items_center() - .text_color(rgb(0x333333)) - .child(div().text_xl().text_center().child("Drop & Drop")) - .child( - div() - .w_full() - .mb_10() - .justify_center() - .flex() - .flex_row() - .gap_4() - .items_center() - .children(items.into_iter().enumerate().map(|(ix, color)| { - let drag_info = DragInfo::new(ix, color); - - div() - .id(("item", ix)) - .size_32() - .flex() - .justify_center() - .items_center() - .border_2() - .border_color(color) - .text_color(color) - .cursor_move() - .hover(|this| this.bg(color.opacity(0.2))) - .child(format!("Item ({})", ix)) - .on_drag(drag_info, |info: &DragInfo, position, _, cx| { - cx.new(|_| info.position(position)) - }) - })), - ) - .child( - div() - .id("drop-target") - .w_128() - .h_32() - .flex() - .justify_center() - .items_center() - .border_3() - .border_color(self.drop_on.map(|info| info.color).unwrap_or(gpui::black())) - .when_some(self.drop_on, |this, info| this.bg(info.color.opacity(0.5))) - .on_drop(cx.listener(|this, info: &DragInfo, _, _| { - this.drop_on = Some(*info); - })) - .child("Drop items here"), - ) - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| DragDrop::new()), - ) - .unwrap(); - - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/example_support/fonts.rs b/crates/gpui_pre/examples/example_support/fonts.rs deleted file mode 100644 index 39c3be1..0000000 --- a/crates/gpui_pre/examples/example_support/fonts.rs +++ /dev/null @@ -1,44 +0,0 @@ -#[cfg(target_family = "wasm")] -use std::borrow::Cow; - -use gpui::App; - -#[cfg(target_family = "wasm")] -pub fn load_fonts(cx: &App) -> bool { - let fonts = [ - Cow::Borrowed( - include_bytes!("../../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf") - .as_slice(), - ), - Cow::Borrowed( - include_bytes!("../../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf") - .as_slice(), - ), - Cow::Borrowed( - include_bytes!("../../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBold.ttf") - .as_slice(), - ), - Cow::Borrowed( - include_bytes!("../../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBoldItalic.ttf") - .as_slice(), - ), - Cow::Borrowed( - include_bytes!("../../../../assets/fonts/lilex/Lilex-Regular.ttf").as_slice(), - ), - Cow::Borrowed(include_bytes!("../../../../assets/fonts/lilex/Lilex-Bold.ttf").as_slice()), - Cow::Borrowed(include_bytes!("../../../../assets/fonts/lilex/Lilex-Italic.ttf").as_slice()), - Cow::Borrowed( - include_bytes!("../../../../assets/fonts/lilex/Lilex-BoldItalic.ttf").as_slice(), - ), - ]; - if let Err(error) = cx.text_system().add_fonts(fonts.into()) { - web_sys::console::error_1(&format!("failed to load application fonts: {error:#}").into()); - return false; - } - true -} - -#[cfg(not(target_family = "wasm"))] -pub fn load_fonts(_cx: &App) -> bool { - true -} diff --git a/crates/gpui_pre/examples/focus_visible.rs b/crates/gpui_pre/examples/focus_visible.rs deleted file mode 100644 index c5b3d5d..0000000 --- a/crates/gpui_pre/examples/focus_visible.rs +++ /dev/null @@ -1,235 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - App, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString, Stateful, Window, - WindowBounds, WindowOptions, actions, div, prelude::*, px, size, -}; -use gpui_platform::application; - -actions!(example, [Tab, TabPrev, Quit]); - -struct Example { - focus_handle: FocusHandle, - items: Vec<(FocusHandle, &'static str)>, - message: SharedString, -} - -impl Example { - fn new(window: &mut Window, cx: &mut Context) -> Self { - let items = vec![ - ( - cx.focus_handle().tab_index(1).tab_stop(true), - "Button with .focus() - always shows border when focused", - ), - ( - cx.focus_handle().tab_index(2).tab_stop(true), - "Button with .focus_visible() - only shows border with keyboard", - ), - ( - cx.focus_handle().tab_index(3).tab_stop(true), - "Button with both .focus() and .focus_visible()", - ), - ]; - - let focus_handle = cx.focus_handle(); - window.focus(&focus_handle, cx); - - Self { - focus_handle, - items, - message: SharedString::from( - "Try clicking vs tabbing! Click shows no border, Tab shows border.", - ), - } - } - - fn on_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context) { - window.focus_next(cx); - self.message = SharedString::from("Pressed Tab - focus-visible border should appear!"); - } - - fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context) { - window.focus_prev(cx); - self.message = - SharedString::from("Pressed Shift-Tab - focus-visible border should appear!"); - } - - fn on_quit(&mut self, _: &Quit, _window: &mut Window, cx: &mut Context) { - cx.quit(); - } -} - -impl Render for Example { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - fn button_base(id: impl Into, label: &'static str) -> Stateful
{ - div() - .id(id) - .h_16() - .w_full() - .flex() - .justify_center() - .items_center() - .bg(gpui::rgb(0x2563eb)) - .text_color(gpui::white()) - .rounded_md() - .cursor_pointer() - .hover(|style| style.bg(gpui::rgb(0x1d4ed8))) - .child(label) - } - - div() - .id("app") - .track_focus(&self.focus_handle) - .on_action(cx.listener(Self::on_tab)) - .on_action(cx.listener(Self::on_tab_prev)) - .on_action(cx.listener(Self::on_quit)) - .size_full() - .flex() - .flex_col() - .p_8() - .gap_6() - .bg(gpui::rgb(0xf3f4f6)) - .child( - div() - .text_2xl() - .font_weight(gpui::FontWeight::BOLD) - .text_color(gpui::rgb(0x111827)) - .child("CSS focus-visible Demo"), - ) - .child( - div() - .p_4() - .rounded_md() - .bg(gpui::rgb(0xdbeafe)) - .text_color(gpui::rgb(0x1e3a8a)) - .child(self.message.clone()), - ) - .child( - div() - .flex() - .flex_col() - .gap_4() - .child( - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_sm() - .font_weight(gpui::FontWeight::BOLD) - .text_color(gpui::rgb(0x374151)) - .child("1. Regular .focus() - always visible:"), - ) - .child( - button_base("button1", self.items[0].1) - .track_focus(&self.items[0].0) - .focus(|style| { - style.border_4().border_color(gpui::rgb(0xfbbf24)) - }) - .on_click(cx.listener(|this, _, _, cx| { - this.message = - "Clicked button 1 - focus border is visible!".into(); - cx.notify(); - })), - ), - ) - .child( - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_sm() - .font_weight(gpui::FontWeight::BOLD) - .text_color(gpui::rgb(0x374151)) - .child("2. New .focus_visible() - only keyboard:"), - ) - .child( - button_base("button2", self.items[1].1) - .track_focus(&self.items[1].0) - .focus_visible(|style| { - style.border_4().border_color(gpui::rgb(0x10b981)) - }) - .on_click(cx.listener(|this, _, _, cx| { - this.message = - "Clicked button 2 - no border! Try Tab instead.".into(); - cx.notify(); - })), - ), - ) - .child( - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_sm() - .font_weight(gpui::FontWeight::BOLD) - .text_color(gpui::rgb(0x374151)) - .child( - "3. Both .focus() (yellow) and .focus_visible() (green):", - ), - ) - .child( - button_base("button3", self.items[2].1) - .track_focus(&self.items[2].0) - .focus(|style| { - style.border_4().border_color(gpui::rgb(0xfbbf24)) - }) - .focus_visible(|style| { - style.border_4().border_color(gpui::rgb(0x10b981)) - }) - .on_click(cx.listener(|this, _, _, cx| { - this.message = - "Clicked button 3 - yellow border. Tab shows green!" - .into(); - cx.notify(); - })), - ), - ), - ) - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - cx.bind_keys([ - KeyBinding::new("tab", Tab, None), - KeyBinding::new("shift-tab", TabPrev, None), - KeyBinding::new("cmd-q", Quit, None), - ]); - - let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |window, cx| cx.new(|cx| Example::new(window, cx)), - ) - .unwrap(); - - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/gif_viewer.rs b/crates/gpui_pre/examples/gif_viewer.rs deleted file mode 100644 index 80b56aa..0000000 --- a/crates/gpui_pre/examples/gif_viewer.rs +++ /dev/null @@ -1,62 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{App, Context, Render, Window, WindowOptions, div, img, prelude::*}; -use gpui_platform::application; -use std::path::PathBuf; - -struct GifViewer { - gif_path: PathBuf, -} - -impl GifViewer { - fn new(gif_path: PathBuf) -> Self { - Self { gif_path } - } -} - -impl Render for GifViewer { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().child( - img(self.gif_path.clone()) - .size_full() - .object_fit(gpui::ObjectFit::Contain) - .id("gif"), - ) - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let gif_path = - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/image/black-cat-typing.gif"); - - cx.open_window( - WindowOptions { - focus: true, - ..Default::default() - }, - |_, cx| cx.new(|_| GifViewer::new(gif_path)), - ) - .unwrap(); - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - env_logger::init(); - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/gradient.rs b/crates/gpui_pre/examples/gradient.rs deleted file mode 100644 index df1398f..0000000 --- a/crates/gpui_pre/examples/gradient.rs +++ /dev/null @@ -1,278 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - App, Bounds, ColorSpace, Context, Half, Render, Window, WindowOptions, canvas, div, - linear_color_stop, linear_gradient, point, prelude::*, px, size, -}; -use gpui_platform::application; - -struct GradientViewer { - color_space: ColorSpace, -} - -impl GradientViewer { - fn new() -> Self { - Self { - color_space: ColorSpace::default(), - } - } -} - -impl Render for GradientViewer { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let color_space = self.color_space; - - div() - .bg(gpui::white()) - .size_full() - .p_4() - .flex() - .flex_col() - .gap_3() - .child( - div() - .flex() - .gap_2() - .justify_between() - .items_center() - .child("Gradient Examples") - .child( - div().flex().gap_2().items_center().child( - div() - .id("method") - .flex() - .px_3() - .py_1() - .text_sm() - .bg(gpui::black()) - .text_color(gpui::white()) - .child(format!("{}", color_space)) - .active(|this| this.opacity(0.8)) - .on_click(cx.listener(move |this, _, _, cx| { - this.color_space = match this.color_space { - ColorSpace::Oklab => ColorSpace::Srgb, - ColorSpace::Srgb => ColorSpace::Oklab, - }; - cx.notify(); - })), - ), - ), - ) - .child( - div() - .flex() - .flex_1() - .gap_3() - .child( - div() - .size_full() - .rounded_xl() - .flex() - .items_center() - .justify_center() - .bg(gpui::red()) - .text_color(gpui::white()) - .child("Solid Color"), - ) - .child( - div() - .size_full() - .rounded_xl() - .flex() - .items_center() - .justify_center() - .bg(gpui::blue()) - .text_color(gpui::white()) - .child("Solid Color"), - ), - ) - .child( - div() - .flex() - .flex_1() - .gap_3() - .h_24() - .text_color(gpui::white()) - .child( - div().flex_1().rounded_xl().bg(linear_gradient( - 45., - linear_color_stop(gpui::red(), 0.), - linear_color_stop(gpui::blue(), 1.), - ) - .color_space(color_space)), - ) - .child( - div().flex_1().rounded_xl().bg(linear_gradient( - 135., - linear_color_stop(gpui::red(), 0.), - linear_color_stop(gpui::green(), 1.), - ) - .color_space(color_space)), - ) - .child( - div().flex_1().rounded_xl().bg(linear_gradient( - 225., - linear_color_stop(gpui::green(), 0.), - linear_color_stop(gpui::blue(), 1.), - ) - .color_space(color_space)), - ) - .child( - div().flex_1().rounded_xl().bg(linear_gradient( - 315., - linear_color_stop(gpui::green(), 0.), - linear_color_stop(gpui::yellow(), 1.), - ) - .color_space(color_space)), - ), - ) - .child( - div() - .flex() - .flex_1() - .gap_3() - .h_24() - .text_color(gpui::white()) - .child( - div().flex_1().rounded_xl().bg(linear_gradient( - 0., - linear_color_stop(gpui::red(), 0.), - linear_color_stop(gpui::white(), 1.), - ) - .color_space(color_space)), - ) - .child( - div().flex_1().rounded_xl().bg(linear_gradient( - 90., - linear_color_stop(gpui::blue(), 0.), - linear_color_stop(gpui::white(), 1.), - ) - .color_space(color_space)), - ) - .child( - div().flex_1().rounded_xl().bg(linear_gradient( - 180., - linear_color_stop(gpui::green(), 0.), - linear_color_stop(gpui::white(), 1.), - ) - .color_space(color_space)), - ) - .child( - div().flex_1().rounded_xl().bg(linear_gradient( - 360., - linear_color_stop(gpui::yellow(), 0.), - linear_color_stop(gpui::white(), 1.), - ) - .color_space(color_space)), - ), - ) - .child( - div().flex_1().rounded_xl().bg(linear_gradient( - 0., - linear_color_stop(gpui::green(), 0.05), - linear_color_stop(gpui::yellow(), 0.95), - ) - .color_space(color_space)), - ) - .child( - div().flex_1().rounded_xl().bg(linear_gradient( - 90., - linear_color_stop(gpui::blue(), 0.05), - linear_color_stop(gpui::red(), 0.95), - ) - .color_space(color_space)), - ) - .child( - div() - .flex() - .flex_1() - .gap_3() - .child( - div().flex().flex_1().gap_3().child( - div().flex_1().rounded_xl().bg(linear_gradient( - 90., - linear_color_stop(gpui::blue(), 0.5), - linear_color_stop(gpui::red(), 0.5), - ) - .color_space(color_space)), - ), - ) - .child( - div().flex_1().rounded_xl().bg(linear_gradient( - 180., - linear_color_stop(gpui::green(), 0.), - linear_color_stop(gpui::blue(), 0.5), - ) - .color_space(color_space)), - ), - ) - .child(div().h_24().child(canvas( - move |_, _, _| {}, - move |bounds, _, window, _| { - let size = size(bounds.size.width * 0.8, px(80.)); - let square_bounds = Bounds { - origin: point( - bounds.size.width.half() - size.width.half(), - bounds.origin.y, - ), - size, - }; - let height = square_bounds.size.height; - let horizontal_offset = height; - let vertical_offset = px(30.); - let mut builder = gpui::PathBuilder::fill(); - builder.move_to(square_bounds.bottom_left()); - builder - .line_to(square_bounds.origin + point(horizontal_offset, vertical_offset)); - builder.line_to( - square_bounds.top_right() + point(-horizontal_offset, vertical_offset), - ); - - builder.line_to(square_bounds.bottom_right()); - builder.line_to(square_bounds.bottom_left()); - let path = builder.build().unwrap(); - window.paint_path( - path, - linear_gradient( - 180., - linear_color_stop(gpui::red(), 0.), - linear_color_stop(gpui::blue(), 1.), - ) - .color_space(color_space), - ); - }, - ))) - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - cx.open_window( - WindowOptions { - focus: true, - ..Default::default() - }, - |_, cx| cx.new(|_| GradientViewer::new()), - ) - .unwrap(); - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/grid_layout.rs b/crates/gpui_pre/examples/grid_layout.rs deleted file mode 100644 index 51f9b13..0000000 --- a/crates/gpui_pre/examples/grid_layout.rs +++ /dev/null @@ -1,95 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - App, Bounds, Context, Hsla, Window, WindowBounds, WindowOptions, container_query, div, - prelude::*, px, rgb, size, -}; -use gpui_platform::application; - -// https://en.wikipedia.org/wiki/Holy_grail_(web_design) -// -// Resize the window: the layout is chosen by `container_query` based on the -// measured size of the container, collapsing to a single stacked column when -// it becomes too narrow for the three-column grid. -struct HolyGrailExample {} - -impl Render for HolyGrailExample { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - container_query(|container_size, _window, _cx| { - let block = |color: Hsla| { - div() - .size_full() - .bg(color) - .border_1() - .border_dashed() - .rounded_md() - .border_color(gpui::white()) - .items_center() - }; - - let header = block(gpui::white()).child(format!("Header — {}", container_size.width)); - let table_of_contents = block(gpui::red()).child("Table of contents"); - let content = block(gpui::green()).child("Content"); - let ad = block(gpui::blue()).child("AD :(").text_color(gpui::white()); - let footer = block(gpui::black()) - .text_color(gpui::white()) - .child("Footer"); - - let container = div().gap_1().bg(rgb(0x505050)).shadow_lg().size_full(); - - if container_size.width < px(400.) { - container - .flex() - .flex_col() - .child(header.h_12().flex_none()) - .child(table_of_contents.h_20().flex_none()) - .child(content.flex_1()) - .child(ad.h_20().flex_none()) - .child(footer.h_12().flex_none()) - } else { - container - .grid() - .grid_cols(5) - .grid_rows(5) - .child(header.row_span(1).col_span_full()) - .child(table_of_contents.col_span(1).h_56()) - .child(content.col_span(3).row_span(3)) - .child(ad.col_span(1).row_span(3)) - .child(footer.row_span(1).col_span_full()) - } - }) - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| HolyGrailExample {}), - ) - .unwrap(); - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/hello_world.rs b/crates/gpui_pre/examples/hello_world.rs deleted file mode 100644 index d18cb04..0000000 --- a/crates/gpui_pre/examples/hello_world.rs +++ /dev/null @@ -1,127 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - App, Bounds, Context, SharedString, Window, WindowBounds, WindowOptions, div, prelude::*, px, - rgb, size, -}; -use gpui_platform::application; - -struct HelloWorld { - text: SharedString, -} - -impl Render for HelloWorld { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .flex() - .flex_col() - .gap_3() - .bg(rgb(0x505050)) - .size(px(500.0)) - .justify_center() - .items_center() - .shadow_lg() - .border_1() - .border_color(rgb(0x0000ff)) - .text_xl() - .text_color(rgb(0xffffff)) - .child(format!("Hello, {}!", self.text)) - .child( - div() - .flex() - .gap_2() - .child( - div() - .size_8() - .bg(gpui::red()) - .border_1() - .border_dashed() - .rounded_md() - .border_color(gpui::white()), - ) - .child( - div() - .size_8() - .bg(gpui::green()) - .border_1() - .border_dashed() - .rounded_md() - .border_color(gpui::white()), - ) - .child( - div() - .size_8() - .bg(gpui::blue()) - .border_1() - .border_dashed() - .rounded_md() - .border_color(gpui::white()), - ) - .child( - div() - .size_8() - .bg(gpui::yellow()) - .border_1() - .border_dashed() - .rounded_md() - .border_color(gpui::white()), - ) - .child( - div() - .size_8() - .bg(gpui::black()) - .border_1() - .border_dashed() - .rounded_md() - .rounded_md() - .border_color(gpui::white()), - ) - .child( - div() - .size_8() - .bg(gpui::white()) - .border_1() - .border_dashed() - .rounded_md() - .border_color(gpui::black()), - ), - ) - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| { - cx.new(|_| HelloWorld { - text: "World".into(), - }) - }, - ) - .unwrap(); - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/image/app-icon.png b/crates/gpui_pre/examples/image/app-icon.png deleted file mode 100644 index 08b6d8a..0000000 Binary files a/crates/gpui_pre/examples/image/app-icon.png and /dev/null differ diff --git a/crates/gpui_pre/examples/image/arrow_circle.svg b/crates/gpui_pre/examples/image/arrow_circle.svg deleted file mode 100644 index 90e352b..0000000 --- a/crates/gpui_pre/examples/image/arrow_circle.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/crates/gpui_pre/examples/image/black-cat-typing.gif b/crates/gpui_pre/examples/image/black-cat-typing.gif deleted file mode 100644 index 671a102..0000000 Binary files a/crates/gpui_pre/examples/image/black-cat-typing.gif and /dev/null differ diff --git a/crates/gpui_pre/examples/image/color.svg b/crates/gpui_pre/examples/image/color.svg deleted file mode 100644 index a080681..0000000 --- a/crates/gpui_pre/examples/image/color.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - diff --git a/crates/gpui_pre/examples/image/exif-orientation-rotate-180.jpg b/crates/gpui_pre/examples/image/exif-orientation-rotate-180.jpg deleted file mode 100644 index 9c09565..0000000 Binary files a/crates/gpui_pre/examples/image/exif-orientation-rotate-180.jpg and /dev/null differ diff --git a/crates/gpui_pre/examples/image/image.rs b/crates/gpui_pre/examples/image/image.rs deleted file mode 100644 index 30160c6..0000000 --- a/crates/gpui_pre/examples/image/image.rs +++ /dev/null @@ -1,223 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "../example_support/fonts.rs"] -mod example_support; - -use std::fs; -use std::path::PathBuf; -use std::sync::Arc; - -use anyhow::Result; -use gpui::{ - App, AppContext, AssetSource, Bounds, Context, ImageSource, KeyBinding, Menu, MenuItem, Point, - SharedString, SharedUri, TitlebarOptions, Window, WindowBounds, WindowOptions, actions, div, - img, prelude::*, px, rgb, size, -}; -#[cfg(not(target_family = "wasm"))] -use reqwest_client::ReqwestClient; - -struct Assets { - base: PathBuf, -} - -impl AssetSource for Assets { - fn load(&self, path: &str) -> Result>> { - fs::read(self.base.join(path)) - .map(|data| Some(std::borrow::Cow::Owned(data))) - .map_err(|e| e.into()) - } - - fn list(&self, path: &str) -> Result> { - fs::read_dir(self.base.join(path)) - .map(|entries| { - entries - .filter_map(|entry| { - entry - .ok() - .and_then(|entry| entry.file_name().into_string().ok()) - .map(SharedString::from) - }) - .collect() - }) - .map_err(|e| e.into()) - } -} - -#[derive(IntoElement)] -struct ImageContainer { - text: SharedString, - src: ImageSource, -} - -impl ImageContainer { - pub fn new(text: impl Into, src: impl Into) -> Self { - Self { - text: text.into(), - src: src.into(), - } - } -} - -impl RenderOnce for ImageContainer { - fn render(self, _window: &mut Window, _: &mut App) -> impl IntoElement { - div().child( - div() - .flex_row() - .size_full() - .gap_4() - .child(self.text) - .child(img(self.src).size(px(256.0))), - ) - } -} - -struct ImageShowcase { - local_resource: Arc, - remote_resource: SharedUri, - asset_resource: SharedString, -} - -impl Render for ImageShowcase { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .id("main") - .bg(gpui::white()) - .overflow_y_scroll() - .p_5() - .size_full() - .child( - div() - .flex() - .flex_col() - .justify_center() - .items_center() - .gap_8() - .child(img( - "https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg", - )) - .child( - div() - .flex() - .flex_row() - .justify_center() - .items_center() - .gap_8() - .child(ImageContainer::new( - "Image loaded from a local file with EXIF orientation", - self.local_resource.clone(), - )) - .child(ImageContainer::new( - "Image loaded from a remote resource", - self.remote_resource.clone(), - )) - .child(ImageContainer::new( - "Image loaded from an asset", - self.asset_resource.clone(), - )), - ) - .child( - div() - .flex() - .flex_row() - .gap_8() - .child( - div() - .flex_col() - .child("Auto Width") - .child(img("https://picsum.photos/800/400").h(px(180.))), - ) - .child( - div() - .flex_col() - .child("Auto Height") - .child(img("https://picsum.photos/800/400").w(px(180.))), - ), - ) - .child( - div() - .flex() - .flex_col() - .justify_center() - .items_center() - .w_full() - .border_1() - .border_color(rgb(0xC0C0C0)) - .child("image with max width 100%") - .child(img("https://picsum.photos/800/400").max_w_full()), - ), - ) - } -} - -actions!(image, [Quit]); - -fn run_example() { - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - - #[cfg(not(target_family = "wasm"))] - let app = gpui_platform::application(); - #[cfg(target_family = "wasm")] - let app = gpui_platform::application(); - app.with_assets(Assets { - base: manifest_dir.join("examples"), - }) - .run(move |cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - #[cfg(not(target_family = "wasm"))] - { - let http_client = ReqwestClient::user_agent("gpui example").unwrap(); - cx.set_http_client(Arc::new(http_client)); - } - - cx.activate(true); - cx.on_action(|_: &Quit, cx| cx.quit()); - cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); - cx.set_menus(vec![Menu { - name: "Image".into(), - items: vec![MenuItem::action("Quit", Quit)], - disabled: false, - }]); - - let window_options = WindowOptions { - titlebar: Some(TitlebarOptions { - title: Some(SharedString::from("Image Example")), - appears_transparent: false, - ..Default::default() - }), - - window_bounds: Some(WindowBounds::Windowed(Bounds { - size: size(px(1100.), px(600.)), - origin: Point::new(px(200.), px(200.)), - })), - - ..Default::default() - }; - - cx.open_window(window_options, |_, cx| { - cx.new(|_| ImageShowcase { - // Relative path to your root project path - local_resource: manifest_dir - .join("examples/image/exif-orientation-rotate-180.jpg") - .into(), - remote_resource: "https://picsum.photos/800/400".into(), - asset_resource: "image/color.svg".into(), - }) - }) - .unwrap(); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - env_logger::init(); - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/image_gallery.rs b/crates/gpui_pre/examples/image_gallery.rs deleted file mode 100644 index 16b9b17..0000000 --- a/crates/gpui_pre/examples/image_gallery.rs +++ /dev/null @@ -1,313 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use futures::FutureExt; -use gpui::{ - App, AppContext, Asset as _, AssetLogger, Bounds, ClickEvent, Context, ElementId, Entity, - ImageAssetLoader, ImageCache, ImageCacheProvider, KeyBinding, Menu, MenuItem, - RetainAllImageCache, SharedString, TitlebarOptions, Window, WindowBounds, WindowOptions, - actions, div, hash, image_cache, img, prelude::*, px, rgb, size, -}; -#[cfg(not(target_family = "wasm"))] -use reqwest_client::ReqwestClient; -use std::{collections::HashMap, sync::Arc}; - -const IMAGES_IN_GALLERY: usize = 30; - -struct ImageGallery { - image_key: String, - items_count: usize, - total_count: usize, - image_cache: Entity, -} - -impl ImageGallery { - fn on_next_image(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { - self.image_cache - .update(cx, |image_cache, cx| image_cache.clear(window, cx)); - - let t = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_millis(); - - self.image_key = format!("{}", t); - self.total_count += self.items_count; - cx.notify(); - } -} - -impl Render for ImageGallery { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let image_url: SharedString = - format!("https://picsum.photos/400/200?t={}", self.image_key).into(); - - div() - .flex() - .flex_col() - .text_color(gpui::white()) - .child("Manually managed image cache:") - .child( - div() - .image_cache(self.image_cache.clone()) - .id("main") - .text_color(gpui::black()) - .bg(rgb(0xE9E9E9)) - .overflow_y_scroll() - .p_4() - .size_full() - .flex() - .flex_col() - .items_center() - .gap_2() - .child( - div() - .w_full() - .flex() - .flex_row() - .justify_between() - .child(format!( - "Example to show images and test memory usage (Rendered: {} images).", - self.total_count - )) - .child( - div() - .id("btn") - .py_1() - .px_4() - .bg(gpui::black()) - .hover(|this| this.opacity(0.8)) - .text_color(gpui::white()) - .text_center() - .w_40() - .child("Next Photos") - .on_click(cx.listener(Self::on_next_image)), - ), - ) - .child( - div() - .id("image-gallery") - .flex() - .flex_row() - .flex_wrap() - .gap_x_4() - .gap_y_2() - .justify_around() - .children( - (0..self.items_count) - .map(|ix| img(format!("{}-{}", image_url, ix)).size_20()), - ), - ), - ) - .child( - "Automatically managed image cache:" - ) - .child(image_cache(simple_lru_cache("lru-cache", IMAGES_IN_GALLERY)).child( - div() - .id("main") - .bg(rgb(0xE9E9E9)) - .text_color(gpui::black()) - .overflow_y_scroll() - .p_4() - .size_full() - .flex() - .flex_col() - .items_center() - .gap_2() - .child( - div() - .id("image-gallery") - .flex() - .flex_row() - .flex_wrap() - .gap_x_4() - .gap_y_2() - .justify_around() - .children( - (0..self.items_count) - .map(|ix| img(format!("{}-{}", image_url, ix)).size_20()), - ), - ) - )) - } -} - -fn simple_lru_cache(id: impl Into, max_items: usize) -> SimpleLruCacheProvider { - SimpleLruCacheProvider { - id: id.into(), - max_items, - } -} - -struct SimpleLruCacheProvider { - id: ElementId, - max_items: usize, -} - -impl ImageCacheProvider for SimpleLruCacheProvider { - fn provide(&mut self, window: &mut Window, cx: &mut App) -> gpui::AnyImageCache { - window - .with_global_id(self.id.clone(), |global_id, window| { - window.with_element_state::, _>( - global_id, - |lru_cache, _window| { - let mut lru_cache = lru_cache.unwrap_or_else(|| { - cx.new(|cx| SimpleLruCache::new(self.max_items, cx)) - }); - if lru_cache.read(cx).max_items != self.max_items { - lru_cache = cx.new(|cx| SimpleLruCache::new(self.max_items, cx)); - } - (lru_cache.clone(), lru_cache) - }, - ) - }) - .into() - } -} - -struct SimpleLruCache { - max_items: usize, - usages: Vec, - cache: HashMap, -} - -impl SimpleLruCache { - fn new(max_items: usize, cx: &mut Context) -> Self { - cx.on_release(|simple_cache, cx| { - for (_, mut item) in std::mem::take(&mut simple_cache.cache) { - if let Some(Ok(image)) = item.get() { - cx.drop_image(image, None); - } - } - }) - .detach(); - - Self { - max_items, - usages: Vec::with_capacity(max_items), - cache: HashMap::with_capacity(max_items), - } - } -} - -impl ImageCache for SimpleLruCache { - fn load( - &mut self, - resource: &gpui::Resource, - window: &mut Window, - cx: &mut App, - ) -> Option, gpui::ImageCacheError>> { - assert_eq!(self.usages.len(), self.cache.len()); - assert!(self.cache.len() <= self.max_items); - - let hash = hash(resource); - - if let Some(item) = self.cache.get_mut(&hash) { - let current_ix = self - .usages - .iter() - .position(|item| *item == hash) - .expect("cache and usages must stay in sync"); - self.usages.remove(current_ix); - self.usages.insert(0, hash); - - return item.get(); - } - - let fut = AssetLogger::::load(resource.clone(), cx); - let task = cx.background_executor().spawn(fut).shared(); - if self.usages.len() == self.max_items { - let oldest = self.usages.pop().unwrap(); - let mut image = self - .cache - .remove(&oldest) - .expect("cache and usages must be in sync"); - if let Some(Ok(image)) = image.get() { - cx.drop_image(image, Some(window)); - } - } - self.cache - .insert(hash, gpui::ImageCacheItem::Loading(task.clone())); - self.usages.insert(0, hash); - - let entity = window.current_view(); - window - .spawn(cx, { - async move |cx| { - _ = task.await; - cx.on_next_frame(move |_, cx| { - cx.notify(entity); - }); - } - }) - .detach(); - - None - } -} - -actions!(image, [Quit]); - -fn run_example() { - #[cfg(not(target_family = "wasm"))] - let app = gpui_platform::application(); - #[cfg(target_family = "wasm")] - let app = gpui_platform::single_threaded_web(); - - app.run(move |cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - #[cfg(not(target_family = "wasm"))] - { - let http_client = ReqwestClient::user_agent("gpui example").unwrap(); - cx.set_http_client(Arc::new(http_client)); - } - - cx.activate(true); - cx.on_action(|_: &Quit, cx| cx.quit()); - cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); - cx.set_menus([Menu::new("Image Gallery").items([MenuItem::action("Quit", Quit)])]); - - let window_options = WindowOptions { - titlebar: Some(TitlebarOptions { - title: Some(SharedString::from("Image Gallery")), - appears_transparent: false, - ..Default::default() - }), - - window_bounds: Some(WindowBounds::Windowed(Bounds::centered( - None, - size(px(1100.), px(860.)), - cx, - ))), - - ..Default::default() - }; - - cx.open_window(window_options, |_, cx| { - cx.new(|ctx| ImageGallery { - image_key: "".into(), - items_count: IMAGES_IN_GALLERY, - total_count: 0, - image_cache: RetainAllImageCache::new(ctx), - }) - }) - .unwrap(); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - env_logger::init(); - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/image_loading.rs b/crates/gpui_pre/examples/image_loading.rs deleted file mode 100644 index b682891..0000000 --- a/crates/gpui_pre/examples/image_loading.rs +++ /dev/null @@ -1,232 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use std::{path::Path, sync::Arc, time::Duration}; - -use gpui::{ - Animation, AnimationExt, App, Asset, AssetLogger, AssetSource, Bounds, Context, Hsla, - ImageAssetLoader, ImageCacheError, ImgResourceLoader, LOADING_DELAY, Length, RenderImage, - Resource, SharedString, Window, WindowBounds, WindowOptions, black, div, img, prelude::*, - pulsating_between, px, red, size, -}; -use gpui_platform::application; - -struct Assets {} - -impl AssetSource for Assets { - fn load(&self, path: &str) -> anyhow::Result>> { - std::fs::read(path) - .map(Into::into) - .map_err(Into::into) - .map(Some) - } - - fn list(&self, path: &str) -> anyhow::Result> { - Ok(std::fs::read_dir(path)? - .filter_map(|entry| { - Some(SharedString::from( - entry.ok()?.path().to_string_lossy().into_owned(), - )) - }) - .collect::>()) - } -} - -const IMAGE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/image/app-icon.png"); - -#[derive(Copy, Clone, Hash)] -struct LoadImageParameters { - timeout: Duration, - fail: bool, -} - -struct LoadImageWithParameters {} - -impl Asset for LoadImageWithParameters { - type Source = LoadImageParameters; - - type Output = Result, ImageCacheError>; - - fn load( - parameters: Self::Source, - cx: &mut App, - ) -> impl std::future::Future + Send + 'static { - let timer = cx.background_executor().timer(parameters.timeout); - let data = AssetLogger::::load( - Resource::Path(Path::new(IMAGE).to_path_buf().into()), - cx, - ); - async move { - timer.await; - if parameters.fail { - log::error!("Intentionally failed to load image"); - Err(anyhow::anyhow!("Failed to load image").into()) - } else { - data.await - } - } - } -} - -struct ImageLoadingExample {} - -impl ImageLoadingExample { - fn loading_element() -> impl IntoElement { - div().size_full().flex_none().p_0p5().rounded_xs().child( - div().size_full().with_animation( - "loading-bg", - Animation::new(Duration::from_secs(3)) - .repeat() - .with_easing(pulsating_between(0.04, 0.24)), - move |this, delta| this.bg(black().opacity(delta)), - ), - ) - } - - fn fallback_element() -> impl IntoElement { - let fallback_color: Hsla = black().opacity(0.5); - - div().size_full().flex_none().p_0p5().child( - div() - .size_full() - .flex() - .items_center() - .justify_center() - .rounded_xs() - .text_sm() - .text_color(fallback_color) - .border_1() - .border_color(fallback_color) - .child("?"), - ) - } -} - -impl Render for ImageLoadingExample { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().flex().flex_col().size_full().justify_around().child( - div().flex().flex_row().w_full().justify_around().child( - div() - .flex() - .bg(gpui::white()) - .size(Length::Definite(px(300.0).into())) - .justify_center() - .items_center() - .child({ - let image_source = LoadImageParameters { - timeout: LOADING_DELAY.saturating_sub(Duration::from_millis(25)), - fail: false, - }; - - // Load within the 'loading delay', should not show loading fallback - img(move |window: &mut Window, cx: &mut App| { - window.use_asset::(&image_source, cx) - }) - .id("image-1") - .border_1() - .size_12() - .with_fallback(|| Self::fallback_element().into_any_element()) - .border_color(red()) - .with_loading(|| Self::loading_element().into_any_element()) - .on_click(move |_, _, cx| { - cx.remove_asset::(&image_source); - }) - }) - .child({ - // Load after a long delay - let image_source = LoadImageParameters { - timeout: Duration::from_secs(5), - fail: false, - }; - - img(move |window: &mut Window, cx: &mut App| { - window.use_asset::(&image_source, cx) - }) - .id("image-2") - .with_fallback(|| Self::fallback_element().into_any_element()) - .with_loading(|| Self::loading_element().into_any_element()) - .size_12() - .border_1() - .border_color(red()) - .on_click(move |_, _, cx| { - cx.remove_asset::(&image_source); - }) - }) - .child({ - // Fail to load image after a long delay - let image_source = LoadImageParameters { - timeout: Duration::from_secs(5), - fail: true, - }; - - // Fail to load after a long delay - img(move |window: &mut Window, cx: &mut App| { - window.use_asset::(&image_source, cx) - }) - .id("image-3") - .with_fallback(|| Self::fallback_element().into_any_element()) - .with_loading(|| Self::loading_element().into_any_element()) - .size_12() - .border_1() - .border_color(red()) - .on_click(move |_, _, cx| { - cx.remove_asset::(&image_source); - }) - }) - .child({ - // Ensure that the normal image loader doesn't spam logs - let image_source = Path::new( - "this/file/really/shouldn't/exist/or/won't/be/an/image/I/hope", - ) - .to_path_buf(); - img(image_source.clone()) - .id("image-4") - .border_1() - .size_12() - .with_fallback(|| Self::fallback_element().into_any_element()) - .border_color(red()) - .with_loading(|| Self::loading_element().into_any_element()) - .on_click(move |_, _, cx| { - cx.remove_asset::(&image_source.clone().into()); - }) - }), - ), - ) - } -} - -fn run_example() { - application().with_assets(Assets {}).run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let options = WindowOptions { - window_bounds: Some(WindowBounds::Windowed(Bounds::centered( - None, - size(px(300.), px(300.)), - cx, - ))), - ..Default::default() - }; - cx.open_window(options, |_, cx| { - cx.activate(false); - cx.new(|_| ImageLoadingExample {}) - }) - .unwrap(); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - env_logger::init(); - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/input.rs b/crates/gpui_pre/examples/input.rs deleted file mode 100644 index eadc2d2..0000000 --- a/crates/gpui_pre/examples/input.rs +++ /dev/null @@ -1,784 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use std::ops::Range; - -use gpui::{ - App, Bounds, ClipboardItem, Context, CursorStyle, ElementId, ElementInputHandler, Entity, - EntityInputHandler, FocusHandle, Focusable, GlobalElementId, KeyBinding, Keystroke, LayoutId, - MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point, - ShapedLine, SharedString, Style, TextRun, UTF16Selection, UnderlineStyle, Window, WindowBounds, - WindowOptions, actions, black, div, fill, hsla, opaque_grey, point, prelude::*, px, relative, - rgb, rgba, size, white, yellow, -}; -use gpui_platform::application; -use unicode_segmentation::*; - -actions!( - text_input, - [ - Backspace, - Delete, - Left, - Right, - SelectLeft, - SelectRight, - SelectAll, - Home, - End, - ShowCharacterPalette, - Paste, - Cut, - Copy, - Quit, - ] -); - -struct TextInput { - focus_handle: FocusHandle, - content: SharedString, - placeholder: SharedString, - selected_range: Range, - selection_reversed: bool, - marked_range: Option>, - last_layout: Option, - last_bounds: Option>, - is_selecting: bool, -} - -impl TextInput { - fn left(&mut self, _: &Left, _: &mut Window, cx: &mut Context) { - if self.selected_range.is_empty() { - self.move_to(self.previous_boundary(self.cursor_offset()), cx); - } else { - self.move_to(self.selected_range.start, cx) - } - } - - fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context) { - if self.selected_range.is_empty() { - self.move_to(self.next_boundary(self.selected_range.end), cx); - } else { - self.move_to(self.selected_range.end, cx) - } - } - - fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context) { - self.select_to(self.previous_boundary(self.cursor_offset()), cx); - } - - fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context) { - self.select_to(self.next_boundary(self.cursor_offset()), cx); - } - - fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context) { - self.move_to(0, cx); - self.select_to(self.content.len(), cx) - } - - fn home(&mut self, _: &Home, _: &mut Window, cx: &mut Context) { - self.move_to(0, cx); - } - - fn end(&mut self, _: &End, _: &mut Window, cx: &mut Context) { - self.move_to(self.content.len(), cx); - } - - fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context) { - if self.selected_range.is_empty() { - let prev = self.previous_boundary(self.cursor_offset()); - if self.cursor_offset() == prev { - window.play_system_bell(); - return; - } - self.select_to(prev, cx) - } - self.replace_text_in_range(None, "", window, cx) - } - - fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context) { - if self.selected_range.is_empty() { - let next = self.next_boundary(self.cursor_offset()); - if self.cursor_offset() == next { - window.play_system_bell(); - return; - } - self.select_to(next, cx) - } - self.replace_text_in_range(None, "", window, cx) - } - - fn on_mouse_down( - &mut self, - event: &MouseDownEvent, - _window: &mut Window, - cx: &mut Context, - ) { - self.is_selecting = true; - - if event.modifiers.shift { - self.select_to(self.index_for_mouse_position(event.position), cx); - } else { - self.move_to(self.index_for_mouse_position(event.position), cx) - } - } - - fn on_mouse_up(&mut self, _: &MouseUpEvent, _window: &mut Window, _: &mut Context) { - self.is_selecting = false; - } - - fn on_mouse_move(&mut self, event: &MouseMoveEvent, _: &mut Window, cx: &mut Context) { - if self.is_selecting { - self.select_to(self.index_for_mouse_position(event.position), cx); - } - } - - fn show_character_palette( - &mut self, - _: &ShowCharacterPalette, - window: &mut Window, - _: &mut Context, - ) { - window.show_character_palette(); - } - - fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context) { - if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) { - self.replace_text_in_range(None, &text.replace("\n", " "), window, cx); - } - } - - fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context) { - if !self.selected_range.is_empty() { - cx.write_to_clipboard(ClipboardItem::new_string( - self.content[self.selected_range.clone()].to_string(), - )); - } - } - fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context) { - if !self.selected_range.is_empty() { - cx.write_to_clipboard(ClipboardItem::new_string( - self.content[self.selected_range.clone()].to_string(), - )); - self.replace_text_in_range(None, "", window, cx) - } - } - - fn move_to(&mut self, offset: usize, cx: &mut Context) { - self.selected_range = offset..offset; - cx.notify() - } - - fn cursor_offset(&self) -> usize { - if self.selection_reversed { - self.selected_range.start - } else { - self.selected_range.end - } - } - - fn index_for_mouse_position(&self, position: Point) -> usize { - if self.content.is_empty() { - return 0; - } - - let (Some(bounds), Some(line)) = (self.last_bounds.as_ref(), self.last_layout.as_ref()) - else { - return 0; - }; - if position.y < bounds.top() { - return 0; - } - if position.y > bounds.bottom() { - return self.content.len(); - } - line.closest_index_for_x(position.x - bounds.left()) - } - - fn select_to(&mut self, offset: usize, cx: &mut Context) { - if self.selection_reversed { - self.selected_range.start = offset - } else { - self.selected_range.end = offset - }; - if self.selected_range.end < self.selected_range.start { - self.selection_reversed = !self.selection_reversed; - self.selected_range = self.selected_range.end..self.selected_range.start; - } - cx.notify() - } - - fn offset_from_utf16(&self, offset: usize) -> usize { - let mut utf8_offset = 0; - let mut utf16_count = 0; - - for ch in self.content.chars() { - if utf16_count >= offset { - break; - } - utf16_count += ch.len_utf16(); - utf8_offset += ch.len_utf8(); - } - - utf8_offset - } - - fn offset_to_utf16(&self, offset: usize) -> usize { - let mut utf16_offset = 0; - let mut utf8_count = 0; - - for ch in self.content.chars() { - if utf8_count >= offset { - break; - } - utf8_count += ch.len_utf8(); - utf16_offset += ch.len_utf16(); - } - - utf16_offset - } - - fn range_to_utf16(&self, range: &Range) -> Range { - self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end) - } - - fn range_from_utf16(&self, range_utf16: &Range) -> Range { - self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end) - } - - fn previous_boundary(&self, offset: usize) -> usize { - self.content - .grapheme_indices(true) - .rev() - .find_map(|(idx, _)| (idx < offset).then_some(idx)) - .unwrap_or(0) - } - - fn next_boundary(&self, offset: usize) -> usize { - self.content - .grapheme_indices(true) - .find_map(|(idx, _)| (idx > offset).then_some(idx)) - .unwrap_or(self.content.len()) - } - - fn reset(&mut self) { - self.content = "".into(); - self.selected_range = 0..0; - self.selection_reversed = false; - self.marked_range = None; - self.last_layout = None; - self.last_bounds = None; - self.is_selecting = false; - } -} - -impl EntityInputHandler for TextInput { - fn text_for_range( - &mut self, - range_utf16: Range, - actual_range: &mut Option>, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - let range = self.range_from_utf16(&range_utf16); - actual_range.replace(self.range_to_utf16(&range)); - Some(self.content[range].to_string()) - } - - fn selected_text_range( - &mut self, - _ignore_disabled_input: bool, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - Some(UTF16Selection { - range: self.range_to_utf16(&self.selected_range), - reversed: self.selection_reversed, - }) - } - - fn marked_text_range( - &self, - _window: &mut Window, - _cx: &mut Context, - ) -> Option> { - self.marked_range - .as_ref() - .map(|range| self.range_to_utf16(range)) - } - - fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context) { - self.marked_range = None; - } - - fn replace_text_in_range( - &mut self, - range_utf16: Option>, - new_text: &str, - _: &mut Window, - cx: &mut Context, - ) { - let range = range_utf16 - .as_ref() - .map(|range_utf16| self.range_from_utf16(range_utf16)) - .or(self.marked_range.clone()) - .unwrap_or(self.selected_range.clone()); - - self.content = - (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..]) - .into(); - self.selected_range = range.start + new_text.len()..range.start + new_text.len(); - self.marked_range.take(); - cx.notify(); - } - - fn replace_and_mark_text_in_range( - &mut self, - range_utf16: Option>, - new_text: &str, - new_selected_range_utf16: Option>, - _window: &mut Window, - cx: &mut Context, - ) { - let range = range_utf16 - .as_ref() - .map(|range_utf16| self.range_from_utf16(range_utf16)) - .or(self.marked_range.clone()) - .unwrap_or(self.selected_range.clone()); - - self.content = - (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..]) - .into(); - if !new_text.is_empty() { - self.marked_range = Some(range.start..range.start + new_text.len()); - } else { - self.marked_range = None; - } - self.selected_range = new_selected_range_utf16 - .as_ref() - .map(|range_utf16| self.range_from_utf16(range_utf16)) - .map(|new_range| new_range.start + range.start..new_range.end + range.end) - .unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len()); - - cx.notify(); - } - - fn bounds_for_range( - &mut self, - range_utf16: Range, - bounds: Bounds, - _window: &mut Window, - _cx: &mut Context, - ) -> Option> { - let last_layout = self.last_layout.as_ref()?; - let range = self.range_from_utf16(&range_utf16); - Some(Bounds::from_corners( - point( - bounds.left() + last_layout.x_for_index(range.start), - bounds.top(), - ), - point( - bounds.left() + last_layout.x_for_index(range.end), - bounds.bottom(), - ), - )) - } - - fn character_index_for_point( - &mut self, - point: gpui::Point, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - let line_point = self.last_bounds?.localize(&point)?; - let last_layout = self.last_layout.as_ref()?; - - assert_eq!(last_layout.text, self.content); - let utf8_index = last_layout.index_for_x(point.x - line_point.x)?; - Some(self.offset_to_utf16(utf8_index)) - } -} - -struct TextElement { - input: Entity, -} - -struct PrepaintState { - line: Option, - cursor: Option, - selection: Option, -} - -impl IntoElement for TextElement { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -impl Element for TextElement { - type RequestLayoutState = (); - type PrepaintState = PrepaintState; - - fn id(&self) -> Option { - None - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - let mut style = Style::default(); - style.size.width = relative(1.).into(); - style.size.height = window.line_height().into(); - (window.request_layout(style, [], cx), ()) - } - - fn prepaint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState { - let input = self.input.read(cx); - let content = input.content.clone(); - let selected_range = input.selected_range.clone(); - let cursor = input.cursor_offset(); - let style = window.text_style(); - - let (display_text, text_color) = if content.is_empty() { - (input.placeholder.clone(), hsla(0., 0., 0., 0.2)) - } else { - (content, style.color) - }; - - let run = TextRun { - len: display_text.len(), - font: style.font(), - color: text_color, - background_color: None, - underline: None, - strikethrough: None, - }; - let runs = if let Some(marked_range) = input.marked_range.as_ref() { - vec![ - TextRun { - len: marked_range.start, - ..run.clone() - }, - TextRun { - len: marked_range.end - marked_range.start, - underline: Some(UnderlineStyle { - color: Some(run.color), - thickness: px(1.0), - wavy: false, - }), - ..run.clone() - }, - TextRun { - len: display_text.len() - marked_range.end, - ..run - }, - ] - .into_iter() - .filter(|run| run.len > 0) - .collect() - } else { - vec![run] - }; - - let font_size = style.font_size.to_pixels(window.rem_size()); - let line = window - .text_system() - .shape_line(display_text, font_size, &runs, None); - - let cursor_pos = line.x_for_index(cursor); - let (selection, cursor) = if selected_range.is_empty() { - ( - None, - Some(fill( - Bounds::new( - point(bounds.left() + cursor_pos, bounds.top()), - size(px(2.), bounds.bottom() - bounds.top()), - ), - gpui::blue(), - )), - ) - } else { - ( - Some(fill( - Bounds::from_corners( - point( - bounds.left() + line.x_for_index(selected_range.start), - bounds.top(), - ), - point( - bounds.left() + line.x_for_index(selected_range.end), - bounds.bottom(), - ), - ), - rgba(0x3311ff30), - )), - None, - ) - }; - PrepaintState { - line: Some(line), - cursor, - selection, - } - } - - fn paint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - prepaint: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - let focus_handle = self.input.read(cx).focus_handle.clone(); - window.handle_input( - &focus_handle, - ElementInputHandler::new(bounds, self.input.clone()), - cx, - ); - if let Some(selection) = prepaint.selection.take() { - window.paint_quad(selection) - } - let line = prepaint.line.take().unwrap(); - line.paint( - bounds.origin, - window.line_height(), - gpui::TextAlign::Left, - None, - window, - cx, - ) - .unwrap(); - - if focus_handle.is_focused(window) - && let Some(cursor) = prepaint.cursor.take() - { - window.paint_quad(cursor); - } - - self.input.update(cx, |input, _cx| { - input.last_layout = Some(line); - input.last_bounds = Some(bounds); - }); - } -} - -impl Render for TextInput { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .flex() - .key_context("TextInput") - .track_focus(&self.focus_handle(cx)) - .cursor(CursorStyle::IBeam) - .on_action(cx.listener(Self::backspace)) - .on_action(cx.listener(Self::delete)) - .on_action(cx.listener(Self::left)) - .on_action(cx.listener(Self::right)) - .on_action(cx.listener(Self::select_left)) - .on_action(cx.listener(Self::select_right)) - .on_action(cx.listener(Self::select_all)) - .on_action(cx.listener(Self::home)) - .on_action(cx.listener(Self::end)) - .on_action(cx.listener(Self::show_character_palette)) - .on_action(cx.listener(Self::paste)) - .on_action(cx.listener(Self::cut)) - .on_action(cx.listener(Self::copy)) - .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down)) - .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up)) - .on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up)) - .on_mouse_move(cx.listener(Self::on_mouse_move)) - .bg(rgb(0xeeeeee)) - .line_height(px(30.)) - .text_size(px(24.)) - .child( - div() - .h(px(30. + 4. * 2.)) - .w_full() - .p(px(4.)) - .bg(white()) - .child(TextElement { input: cx.entity() }), - ) - } -} - -impl Focusable for TextInput { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -struct InputExample { - text_input: Entity, - recent_keystrokes: Vec, - focus_handle: FocusHandle, -} - -impl Focusable for InputExample { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl InputExample { - fn on_reset_click(&mut self, _: &MouseUpEvent, _window: &mut Window, cx: &mut Context) { - self.recent_keystrokes.clear(); - self.text_input - .update(cx, |text_input, _cx| text_input.reset()); - cx.notify(); - } -} - -impl Render for InputExample { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .bg(rgb(0xaaaaaa)) - .track_focus(&self.focus_handle(cx)) - .flex() - .flex_col() - .size_full() - .child( - div() - .bg(white()) - .border_b_1() - .border_color(black()) - .flex() - .flex_row() - .justify_between() - .child(format!("Keyboard {}", cx.keyboard_layout().name())) - .child( - div() - .border_1() - .border_color(black()) - .px_2() - .bg(yellow()) - .child("Reset") - .hover(|style| { - style - .bg(yellow().blend(opaque_grey(0.5, 0.5))) - .cursor_pointer() - }) - .on_mouse_up(MouseButton::Left, cx.listener(Self::on_reset_click)), - ), - ) - .child(self.text_input.clone()) - .children(self.recent_keystrokes.iter().rev().map(|ks| { - format!( - "{:} {}", - ks.unparse(), - if let Some(key_char) = ks.key_char.as_ref() { - format!("-> {:?}", key_char) - } else { - "".to_owned() - } - ) - })) - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx); - cx.bind_keys([ - KeyBinding::new("backspace", Backspace, None), - KeyBinding::new("delete", Delete, None), - KeyBinding::new("left", Left, None), - KeyBinding::new("right", Right, None), - KeyBinding::new("shift-left", SelectLeft, None), - KeyBinding::new("shift-right", SelectRight, None), - KeyBinding::new("cmd-a", SelectAll, None), - KeyBinding::new("cmd-v", Paste, None), - KeyBinding::new("cmd-c", Copy, None), - KeyBinding::new("cmd-x", Cut, None), - KeyBinding::new("home", Home, None), - KeyBinding::new("end", End, None), - KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None), - ]); - - let window = cx - .open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| { - let text_input = cx.new(|cx| TextInput { - focus_handle: cx.focus_handle(), - content: "".into(), - placeholder: "Type here...".into(), - selected_range: 0..0, - selection_reversed: false, - marked_range: None, - last_layout: None, - last_bounds: None, - is_selecting: false, - }); - cx.new(|cx| InputExample { - text_input, - recent_keystrokes: vec![], - focus_handle: cx.focus_handle(), - }) - }, - ) - .unwrap(); - let view = window.update(cx, |_, _, cx| cx.entity()).unwrap(); - cx.observe_keystrokes(move |ev, _, cx| { - view.update(cx, |view, cx| { - view.recent_keystrokes.push(ev.keystroke.clone()); - cx.notify(); - }) - }) - .detach(); - cx.on_keyboard_layout_change({ - move |cx| { - window.update(cx, |_, _, cx| cx.notify()).ok(); - } - }) - .detach(); - - window - .update(cx, |view, window, cx| { - window.focus(&view.text_input.focus_handle(cx), cx); - cx.activate(true); - }) - .unwrap(); - cx.on_action(|_: &Quit, cx| cx.quit()); - cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/layer_shell.rs b/crates/gpui_pre/examples/layer_shell.rs deleted file mode 100644 index 1437b05..0000000 --- a/crates/gpui_pre/examples/layer_shell.rs +++ /dev/null @@ -1,101 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -fn run_example() { - #[cfg(all(target_os = "linux", feature = "wayland"))] - example::main(); - - #[cfg(not(all(target_os = "linux", feature = "wayland")))] - panic!("This example requires the `wayland` feature and a linux system."); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} - -#[cfg(all(target_os = "linux", feature = "wayland"))] -mod example { - use std::time::{Duration, SystemTime, UNIX_EPOCH}; - - use gpui::{ - App, Bounds, Context, FontWeight, Size, Window, WindowBackgroundAppearance, WindowBounds, - WindowKind, WindowOptions, div, layer_shell::*, point, prelude::*, px, rems, rgba, white, - }; - use gpui_platform::application; - - struct LayerShellExample; - - impl LayerShellExample { - fn new(cx: &mut Context) -> Self { - cx.spawn(async move |this, cx| { - loop { - let _ = this.update(cx, |_, cx| cx.notify()); - cx.background_executor() - .timer(Duration::from_millis(500)) - .await; - } - }) - .detach(); - - LayerShellExample - } - } - - impl Render for LayerShellExample { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - let hours = (now / 3600) % 24; - let minutes = (now / 60) % 60; - let seconds = now % 60; - - div() - .size_full() - .flex() - .items_center() - .justify_center() - .text_size(rems(4.5)) - .font_weight(FontWeight::EXTRA_BOLD) - .text_color(white()) - .bg(rgba(0x0000044)) - .rounded_xl() - .child(format!("{:02}:{:02}:{:02}", hours, minutes, seconds)) - } - } - - pub fn main() { - application().run(|cx: &mut App| { - cx.open_window( - WindowOptions { - titlebar: None, - window_bounds: Some(WindowBounds::Windowed(Bounds { - origin: point(px(0.), px(0.)), - size: Size::new(px(500.), px(200.)), - })), - app_id: Some("gpui-layer-shell-example".to_string()), - window_background: WindowBackgroundAppearance::Transparent, - kind: WindowKind::LayerShell(LayerShellOptions { - namespace: "gpui".to_string(), - anchor: Anchor::LEFT | Anchor::RIGHT | Anchor::BOTTOM, - margin: Some((px(0.), px(0.), px(40.), px(0.))), - keyboard_interactivity: KeyboardInteractivity::None, - ..Default::default() - }), - ..Default::default() - }, - |_, cx| cx.new(LayerShellExample::new), - ) - .unwrap(); - }); - } -} diff --git a/crates/gpui_pre/examples/list_example.rs b/crates/gpui_pre/examples/list_example.rs deleted file mode 100644 index 542b7e3..0000000 --- a/crates/gpui_pre/examples/list_example.rs +++ /dev/null @@ -1,176 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - App, Bounds, Context, ListAlignment, ListState, Render, Window, WindowBounds, WindowOptions, - div, list, prelude::*, px, rgb, size, -}; -use gpui_platform::application; - -const ITEM_COUNT: usize = 40; -const SCROLLBAR_WIDTH: f32 = 12.; - -struct BottomListDemo { - list_state: ListState, -} - -impl BottomListDemo { - fn new() -> Self { - Self { - list_state: ListState::new(ITEM_COUNT, ListAlignment::Bottom, px(500.)).measure_all(), - } - } -} - -impl Render for BottomListDemo { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let max_offset = self.list_state.max_offset_for_scrollbar().y; - let current_offset = -self.list_state.scroll_px_offset_for_scrollbar().y; - - let viewport_height = self.list_state.viewport_bounds().size.height; - - let raw_fraction = if max_offset > px(0.) { - current_offset / max_offset - } else { - 0. - }; - - let total_height = viewport_height + max_offset; - let thumb_height = if total_height > px(0.) { - px(viewport_height.as_f32() * viewport_height.as_f32() / total_height.as_f32()) - .max(px(30.)) - } else { - px(30.) - }; - - let track_space = viewport_height - thumb_height; - let thumb_top = track_space * raw_fraction; - - let bug_detected = raw_fraction > 1.0; - - div() - .size_full() - .bg(rgb(0xFFFFFF)) - .flex() - .flex_col() - .p_4() - .gap_2() - .child( - div() - .text_sm() - .flex() - .flex_col() - .gap_1() - .child(format!( - "offset: {:.0} / max: {:.0} | fraction: {:.3}", - current_offset.as_f32(), - max_offset.as_f32(), - raw_fraction, - )) - .child( - div() - .text_color(if bug_detected { - rgb(0xCC0000) - } else { - rgb(0x008800) - }) - .child(if bug_detected { - format!( - "BUG: fraction is {:.3} (> 1.0) — thumb is off-track!", - raw_fraction - ) - } else { - "OK: fraction <= 1.0 — thumb is within track.".to_string() - }), - ), - ) - .child( - div() - .flex_1() - .flex() - .flex_row() - .overflow_hidden() - .border_1() - .border_color(rgb(0xCCCCCC)) - .rounded_sm() - .child( - list(self.list_state.clone(), |index, _window, _cx| { - let height = px(30. + (index % 5) as f32 * 10.); - div() - .h(height) - .w_full() - .flex() - .items_center() - .px_3() - .border_b_1() - .border_color(rgb(0xEEEEEE)) - .bg(if index % 2 == 0 { - rgb(0xFAFAFA) - } else { - rgb(0xFFFFFF) - }) - .text_sm() - .child(format!("Item {index}")) - .into_any() - }) - .flex_1(), - ) - // Scrollbar track - .child( - div() - .w(px(SCROLLBAR_WIDTH)) - .h_full() - .flex_shrink_0() - .bg(rgb(0xE0E0E0)) - .relative() - .child( - // Thumb — position is unclamped to expose the bug - div() - .absolute() - .top(thumb_top) - .w_full() - .h(thumb_height) - .bg(if bug_detected { - rgb(0xCC0000) - } else { - rgb(0x888888) - }) - .rounded_sm(), - ), - ), - ) - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let bounds = Bounds::centered(None, size(px(400.), px(500.)), cx); - cx.open_window( - WindowOptions { - focus: true, - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| BottomListDemo::new()), - ) - .unwrap(); - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/mouse_pressure.rs b/crates/gpui_pre/examples/mouse_pressure.rs deleted file mode 100644 index 3470bf1..0000000 --- a/crates/gpui_pre/examples/mouse_pressure.rs +++ /dev/null @@ -1,87 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - App, Bounds, Context, MousePressureEvent, PressureStage, Window, WindowBounds, WindowOptions, - div, prelude::*, px, rgb, size, -}; -use gpui_platform::application; - -struct MousePressureExample { - pressure_stage: PressureStage, - pressure_amount: f32, -} - -impl Render for MousePressureExample { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .flex() - .flex_col() - .gap_3() - .bg(rgb(0x505050)) - .size(px(500.0)) - .justify_center() - .items_center() - .shadow_lg() - .border_1() - .border_color(rgb(0x0000ff)) - .text_xl() - .text_color(rgb(0xffffff)) - .child(format!("Pressure stage: {:?}", self.pressure_stage)) - .child(format!("Pressure amount: {:.2}", self.pressure_amount)) - .on_mouse_pressure(cx.listener(Self::on_mouse_pressure)) - } -} - -impl MousePressureExample { - fn on_mouse_pressure( - &mut self, - pressure_event: &MousePressureEvent, - _window: &mut Window, - cx: &mut Context, - ) { - self.pressure_amount = pressure_event.pressure; - self.pressure_stage = pressure_event.stage; - - cx.notify(); - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx); - - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| { - cx.new(|_| MousePressureExample { - pressure_stage: PressureStage::Zero, - pressure_amount: 0.0, - }) - }, - ) - .unwrap(); - - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/move_entity_between_windows.rs b/crates/gpui_pre/examples/move_entity_between_windows.rs deleted file mode 100644 index e0f1646..0000000 --- a/crates/gpui_pre/examples/move_entity_between_windows.rs +++ /dev/null @@ -1,160 +0,0 @@ -//! An entity registers callbacks via the `_in` API family and then gets -//! re-hosted in a new window via a click. The point of the example is to -//! demonstrate that callbacks dispatched after the move correctly target the -//! entity's *current* window rather than the window it was in at -//! registration time. -//! -//! To run: cargo run -p gpui --example move_entity_between_windows - -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use std::time::Duration; - -use gpui::{ - App, AppContext as _, Bounds, Context, EventEmitter, MouseButton, Render, SharedString, - Subscription, Task, Window, WindowBounds, WindowOptions, div, prelude::*, px, rgb, size, -}; -use gpui_platform::application; - -struct MoveToNewWindow; - -struct HelloWorld { - text: SharedString, - tick_count: u32, - move_count: u32, - _tasks: Vec>, - _subscriptions: Vec, -} - -impl EventEmitter for HelloWorld {} - -impl HelloWorld { - fn new(window: &mut Window, cx: &mut Context) -> Self { - let self_entity = cx.entity(); - - let task = cx.spawn_in(window, async move |this, cx| { - loop { - cx.background_executor().timer(Duration::from_secs(1)).await; - let result = this.update_in(cx, |this, window, _cx| { - this.tick_count += 1; - println!( - "tick #{} fired in entity's current window {}", - this.tick_count, - window.window_handle().window_id().as_u64(), - ); - }); - if let Err(err) = result { - println!("tick task giving up: {err}"); - return; - } - } - }); - - let subscription = cx.subscribe_in::<_, MoveToNewWindow>( - &self_entity, - window, - move |this, _emitter, _event, window, cx| { - let entered_window_id = window.window_handle().window_id().as_u64(); - println!( - "MoveToNewWindow handler fired in entity's current window {entered_window_id}", - ); - - this.move_count += 1; - cx.notify(); - - let entity = cx.entity(); - let old_window = window.window_handle(); - cx.defer(move |cx| { - let bounds = Bounds::centered(None, size(px(500.0), px(500.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - move |_, _| entity, - ) - .expect("failed to open new window"); - old_window - .update(cx, |_, window, _| window.remove_window()) - .ok(); - }); - }, - ); - - Self { - text: "World".into(), - tick_count: 0, - move_count: 0, - _tasks: vec![task], - _subscriptions: vec![subscription], - } - } -} - -impl Render for HelloWorld { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let window_id = window.window_handle().window_id().as_u64(); - - div() - .flex() - .flex_col() - .gap_3() - .bg(rgb(0x505050)) - .size(px(500.0)) - .justify_center() - .items_center() - .text_xl() - .text_color(rgb(0xffffff)) - .child(format!("Hello, {}!", self.text)) - .child(format!("Rendering in window: {window_id}")) - .child(format!("Ticks observed by entity: {}", self.tick_count)) - .child(format!("Moves observed by entity: {}", self.move_count)) - .child( - div() - .px_4() - .py_2() - .bg(rgb(0x4040ff)) - .rounded_md() - .child("Move me to a new window") - .on_mouse_down( - MouseButton::Left, - cx.listener(|_this, _, _window, cx| { - cx.emit(MoveToNewWindow); - }), - ), - ) - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let bounds = Bounds::centered(None, size(px(500.0), px(500.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |window, cx| cx.new(|cx| HelloWorld::new(window, cx)), - ) - .unwrap(); - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/on_window_close_quit.rs b/crates/gpui_pre/examples/on_window_close_quit.rs deleted file mode 100644 index d61e23e..0000000 --- a/crates/gpui_pre/examples/on_window_close_quit.rs +++ /dev/null @@ -1,103 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - App, Bounds, Context, FocusHandle, KeyBinding, Window, WindowBounds, WindowOptions, actions, - div, prelude::*, px, rgb, size, -}; -use gpui_platform::application; - -actions!(example, [CloseWindow]); - -struct ExampleWindow { - focus_handle: FocusHandle, -} - -impl Render for ExampleWindow { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .on_action(|_: &CloseWindow, window, _| { - window.remove_window(); - }) - .track_focus(&self.focus_handle) - .flex() - .flex_col() - .gap_3() - .bg(rgb(0x505050)) - .size(px(500.0)) - .justify_center() - .items_center() - .shadow_lg() - .border_1() - .border_color(rgb(0x0000ff)) - .text_xl() - .text_color(rgb(0xffffff)) - .child( - "Closing this window with cmd-w or the traffic lights should quit the application!", - ) - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let mut bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx); - - cx.bind_keys([KeyBinding::new("cmd-w", CloseWindow, None)]); - cx.on_window_closed(|cx, _window_id| { - if cx.windows().is_empty() { - cx.quit(); - } - }) - .detach(); - - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |window, cx| { - cx.activate(false); - cx.new(|cx| { - let focus_handle = cx.focus_handle(); - focus_handle.focus(window, cx); - ExampleWindow { focus_handle } - }) - }, - ) - .unwrap(); - - bounds.origin.x += bounds.size.width; - - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |window, cx| { - cx.new(|cx| { - let focus_handle = cx.focus_handle(); - focus_handle.focus(window, cx); - ExampleWindow { focus_handle } - }) - }, - ) - .unwrap(); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/opacity.rs b/crates/gpui_pre/examples/opacity.rs deleted file mode 100644 index c2af342..0000000 --- a/crates/gpui_pre/examples/opacity.rs +++ /dev/null @@ -1,195 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use std::{fs, path::PathBuf}; - -use anyhow::Result; -use gpui::{ - App, AssetSource, Bounds, BoxShadow, ClickEvent, Context, SharedString, Task, Window, - WindowBounds, WindowOptions, div, hsla, img, prelude::*, px, rgb, size, svg, -}; -use gpui_platform::application; - -struct Assets { - base: PathBuf, -} - -impl AssetSource for Assets { - fn load(&self, path: &str) -> Result>> { - fs::read(self.base.join(path)) - .map(|data| Some(std::borrow::Cow::Owned(data))) - .map_err(|e| e.into()) - } - - fn list(&self, path: &str) -> Result> { - fs::read_dir(self.base.join(path)) - .map(|entries| { - entries - .filter_map(|entry| { - entry - .ok() - .and_then(|entry| entry.file_name().into_string().ok()) - .map(SharedString::from) - }) - .collect() - }) - .map_err(|e| e.into()) - } -} - -struct HelloWorld { - _task: Option>, - opacity: f32, - animating: bool, -} - -impl HelloWorld { - fn new(_window: &mut Window, _: &mut Context) -> Self { - Self { - _task: None, - opacity: 0.5, - animating: false, - } - } - - fn start_animation(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context) { - self.opacity = 0.0; - self.animating = true; - cx.notify(); - } -} - -impl Render for HelloWorld { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - if self.animating { - self.opacity += 0.005; - if self.opacity >= 1.0 { - self.animating = false; - self.opacity = 1.0; - } else { - window.request_animation_frame(); - } - } - - div() - .flex() - .flex_row() - .size_full() - .bg(rgb(0xe0e0e0)) - .text_xl() - .child( - div() - .flex() - .size_full() - .justify_center() - .items_center() - .border_1() - .text_color(gpui::blue()) - .child(div().child("This is background text.")), - ) - .child( - div() - .id("panel") - .on_click(cx.listener(Self::start_animation)) - .absolute() - .top_8() - .left_8() - .right_8() - .bottom_8() - .opacity(self.opacity) - .flex() - .justify_center() - .items_center() - .bg(gpui::white()) - .border_3() - .border_color(gpui::red()) - .text_color(gpui::yellow()) - .child( - div() - .flex() - .flex_col() - .gap_2() - .justify_center() - .items_center() - .size(px(300.)) - .bg(gpui::blue()) - .border_3() - .border_color(gpui::black()) - .shadow(vec![ - BoxShadow::new(px(10.0), px(10.0), hsla(0.0, 0.0, 0.0, 0.5)) - .blur_radius(px(1.0)) - .spread_radius(px(5.0)), - ]) - .child(img("image/app-icon.png").size_8()) - .child("Opacity Panel (Click to test)") - .child( - div() - .id("deep-level-text") - .flex() - .justify_center() - .items_center() - .p_4() - .bg(gpui::black()) - .text_color(gpui::white()) - .text_decoration_2() - .text_decoration_wavy() - .text_decoration_color(gpui::red()) - .child(format!("opacity: {:.1}", self.opacity)), - ) - .child( - svg() - .path("image/arrow_circle.svg") - .text_color(gpui::black()) - .text_2xl() - .size_8(), - ) - .child( - div() - .flex() - .children(["🎊", "✈️", "🎉", "🎈", "🎁", "🎂"].map(|emoji| { - div() - .child(emoji.to_string()) - .hover(|style| style.opacity(0.5)) - })), - ) - .child(img("image/black-cat-typing.gif").size_12()), - ), - ) - } -} - -fn run_example() { - application() - .with_assets(Assets { - base: PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples"), - }) - .run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let bounds = Bounds::centered(None, size(px(500.0), px(500.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |window, cx| cx.new(|cx| HelloWorld::new(window, cx)), - ) - .unwrap(); - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/ownership_post.rs b/crates/gpui_pre/examples/ownership_post.rs deleted file mode 100644 index 04a0b6f..0000000 --- a/crates/gpui_pre/examples/ownership_post.rs +++ /dev/null @@ -1,56 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{App, Context, Entity, EventEmitter, prelude::*}; -use gpui_platform::application; - -struct Counter { - count: usize, -} - -struct Change { - increment: usize, -} - -impl EventEmitter for Counter {} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let counter: Entity = cx.new(|_cx| Counter { count: 0 }); - let subscriber = cx.new(|cx: &mut Context| { - cx.subscribe(&counter, |subscriber, _emitter, event, _cx| { - subscriber.count += event.increment * 2; - }) - .detach(); - - Counter { - count: counter.read(cx).count * 2, - } - }); - - counter.update(cx, |counter, cx| { - counter.count += 2; - cx.notify(); - cx.emit(Change { increment: 2 }); - }); - - assert_eq!(subscriber.read(cx).count, 4); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/painting.rs b/crates/gpui_pre/examples/painting.rs deleted file mode 100644 index 3d89b0f..0000000 --- a/crates/gpui_pre/examples/painting.rs +++ /dev/null @@ -1,478 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - Background, Bounds, ColorSpace, Context, MouseDownEvent, Path, PathBuilder, PathStyle, Pixels, - Point, Render, StrokeOptions, Window, WindowOptions, canvas, div, linear_color_stop, - linear_gradient, point, prelude::*, px, quad, rgb, size, -}; -use gpui_platform::application; - -struct PaintingViewer { - default_lines: Vec<(Path, Background)>, - background_quads: Vec<(Bounds, Background)>, - lines: Vec>>, - start: Point, - dashed: bool, - _painting: bool, -} - -impl PaintingViewer { - fn new(_window: &mut Window, _cx: &mut Context) -> Self { - let mut lines = vec![]; - - // Black squares beneath transparent paths. - let background_quads = vec![ - ( - Bounds { - origin: point(px(70.), px(70.)), - size: size(px(40.), px(40.)), - }, - gpui::black().into(), - ), - ( - Bounds { - origin: point(px(170.), px(70.)), - size: size(px(40.), px(40.)), - }, - gpui::black().into(), - ), - ( - Bounds { - origin: point(px(270.), px(70.)), - size: size(px(40.), px(40.)), - }, - gpui::black().into(), - ), - ( - Bounds { - origin: point(px(370.), px(70.)), - size: size(px(40.), px(40.)), - }, - gpui::black().into(), - ), - ( - Bounds { - origin: point(px(450.), px(50.)), - size: size(px(80.), px(80.)), - }, - gpui::black().into(), - ), - ]; - - // 50% opaque red path that extends across black quad. - let mut builder = PathBuilder::fill(); - builder.move_to(point(px(50.), px(50.))); - builder.line_to(point(px(130.), px(50.))); - builder.line_to(point(px(130.), px(130.))); - builder.line_to(point(px(50.), px(130.))); - builder.close(); - let path = builder.build().unwrap(); - let red = rgb(0xFF0000).alpha(0.5); - lines.push((path, red.into())); - - // 50% opaque blue path that extends across black quad. - let mut builder = PathBuilder::fill(); - builder.move_to(point(px(150.), px(50.))); - builder.line_to(point(px(230.), px(50.))); - builder.line_to(point(px(230.), px(130.))); - builder.line_to(point(px(150.), px(130.))); - builder.close(); - let path = builder.build().unwrap(); - let blue = rgb(0x0000FF).alpha(0.5); - lines.push((path, blue.into())); - - // 50% opaque green path that extends across black quad. - let mut builder = PathBuilder::fill(); - builder.move_to(point(px(250.), px(50.))); - builder.line_to(point(px(330.), px(50.))); - builder.line_to(point(px(330.), px(130.))); - builder.line_to(point(px(250.), px(130.))); - builder.close(); - let path = builder.build().unwrap(); - let green = rgb(0x00FF00).alpha(0.5); - lines.push((path, green.into())); - - // 50% opaque black path that extends across black quad. - let mut builder = PathBuilder::fill(); - builder.move_to(point(px(350.), px(50.))); - builder.line_to(point(px(430.), px(50.))); - builder.line_to(point(px(430.), px(130.))); - builder.line_to(point(px(350.), px(130.))); - builder.close(); - let path = builder.build().unwrap(); - let black = rgb(0x000000).alpha(0.5); - lines.push((path, black.into())); - - // Two 50% opaque red circles overlapping - center should be darker red - let mut builder = PathBuilder::fill(); - let center = point(px(530.), px(85.)); - let radius = px(30.); - builder.move_to(point(center.x + radius, center.y)); - builder.arc_to( - point(radius, radius), - px(0.), - false, - false, - point(center.x - radius, center.y), - ); - builder.arc_to( - point(radius, radius), - px(0.), - false, - false, - point(center.x + radius, center.y), - ); - builder.close(); - let path = builder.build().unwrap(); - let red1 = rgb(0xFF0000).alpha(0.5); - lines.push((path, red1.into())); - - let mut builder = PathBuilder::fill(); - let center = point(px(570.), px(85.)); - let radius = px(30.); - builder.move_to(point(center.x + radius, center.y)); - builder.arc_to( - point(radius, radius), - px(0.), - false, - false, - point(center.x - radius, center.y), - ); - builder.arc_to( - point(radius, radius), - px(0.), - false, - false, - point(center.x + radius, center.y), - ); - builder.close(); - let path = builder.build().unwrap(); - let red2 = rgb(0xFF0000).alpha(0.5); - lines.push((path, red2.into())); - - // draw a Rust logo - let mut builder = lyon::path::Path::svg_builder(); - lyon::extra::rust_logo::build_logo_path(&mut builder); - // move down the Path - let mut builder: PathBuilder = builder.into(); - builder.translate(point(px(10.), px(200.))); - builder.scale(0.9); - let path = builder.build().unwrap(); - lines.push((path, gpui::black().into())); - - // draw a lightening bolt ⚡ - let mut builder = PathBuilder::fill(); - builder.add_polygon( - &[ - point(px(150.), px(300.)), - point(px(200.), px(225.)), - point(px(200.), px(275.)), - point(px(250.), px(200.)), - ], - false, - ); - let path = builder.build().unwrap(); - lines.push((path, rgb(0x1d4ed8).into())); - - // draw a ⭐ - let mut builder = PathBuilder::fill(); - builder.move_to(point(px(350.), px(200.))); - builder.line_to(point(px(370.), px(260.))); - builder.line_to(point(px(430.), px(260.))); - builder.line_to(point(px(380.), px(300.))); - builder.line_to(point(px(400.), px(360.))); - builder.line_to(point(px(350.), px(320.))); - builder.line_to(point(px(300.), px(360.))); - builder.line_to(point(px(320.), px(300.))); - builder.line_to(point(px(270.), px(260.))); - builder.line_to(point(px(330.), px(260.))); - builder.line_to(point(px(350.), px(200.))); - let path = builder.build().unwrap(); - lines.push(( - path, - linear_gradient( - 180., - linear_color_stop(rgb(0xFACC15), 0.7), - linear_color_stop(rgb(0xD56D0C), 1.), - ) - .color_space(ColorSpace::Oklab), - )); - - // draw linear gradient - let square_bounds = Bounds { - origin: point(px(450.), px(200.)), - size: size(px(200.), px(80.)), - }; - let height = square_bounds.size.height; - let horizontal_offset = height; - let vertical_offset = px(30.); - let mut builder = PathBuilder::fill(); - builder.move_to(square_bounds.bottom_left()); - builder.curve_to( - square_bounds.origin + point(horizontal_offset, vertical_offset), - square_bounds.origin + point(px(0.0), vertical_offset), - ); - builder.line_to(square_bounds.top_right() + point(-horizontal_offset, vertical_offset)); - builder.curve_to( - square_bounds.bottom_right(), - square_bounds.top_right() + point(px(0.0), vertical_offset), - ); - builder.line_to(square_bounds.bottom_left()); - let path = builder.build().unwrap(); - lines.push(( - path, - linear_gradient( - 180., - linear_color_stop(gpui::blue(), 0.4), - linear_color_stop(gpui::red(), 1.), - ), - )); - - // draw a pie chart - let center = point(px(96.), px(96.)); - let pie_center = point(px(775.), px(255.)); - let segments = [ - ( - point(px(871.), px(255.)), - point(px(747.), px(163.)), - rgb(0x1374e9), - ), - ( - point(px(747.), px(163.)), - point(px(679.), px(263.)), - rgb(0xe13527), - ), - ( - point(px(679.), px(263.)), - point(px(754.), px(349.)), - rgb(0x0751ce), - ), - ( - point(px(754.), px(349.)), - point(px(854.), px(310.)), - rgb(0x209742), - ), - ( - point(px(854.), px(310.)), - point(px(871.), px(255.)), - rgb(0xfbc10a), - ), - ]; - - for (start, end, color) in segments { - let mut builder = PathBuilder::fill(); - builder.move_to(start); - builder.arc_to(center, px(0.), false, false, end); - builder.line_to(pie_center); - builder.close(); - let path = builder.build().unwrap(); - lines.push((path, color.into())); - } - - // draw a wave - let options = StrokeOptions::default() - .with_line_width(1.) - .with_line_join(lyon::path::LineJoin::Bevel); - let mut builder = PathBuilder::stroke(px(1.)).with_style(PathStyle::Stroke(options)); - builder.move_to(point(px(40.), px(420.))); - for i in 1..50 { - builder.line_to(point( - px(40.0 + i as f32 * 10.0), - px(420.0 + (i as f32 * 10.0).sin() * 40.0), - )); - } - let path = builder.build().unwrap(); - lines.push((path, gpui::green().into())); - - Self { - default_lines: lines.clone(), - background_quads, - lines: vec![], - start: point(px(0.), px(0.)), - dashed: false, - _painting: false, - } - } - - fn clear(&mut self, cx: &mut Context) { - self.lines.clear(); - cx.notify(); - } -} - -fn button( - text: &str, - cx: &mut Context, - on_click: impl Fn(&mut PaintingViewer, &mut Context) + 'static, -) -> impl IntoElement { - div() - .id(text.to_string()) - .child(text.to_string()) - .bg(gpui::black()) - .text_color(gpui::white()) - .active(|this| this.opacity(0.8)) - .flex() - .px_3() - .py_1() - .on_click(cx.listener(move |this, _, _, cx| on_click(this, cx))) -} - -impl Render for PaintingViewer { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let default_lines = self.default_lines.clone(); - let background_quads = self.background_quads.clone(); - let lines = self.lines.clone(); - let dashed = self.dashed; - - div() - .bg(gpui::white()) - .size_full() - .p_4() - .flex() - .flex_col() - .child( - div() - .flex() - .gap_2() - .justify_between() - .items_center() - .child("Mouse down any point and drag to draw lines (Hold on shift key to draw straight lines)") - .child( - div() - .flex() - .gap_x_2() - .child(button( - if dashed { "Solid" } else { "Dashed" }, - cx, - move |this, _| this.dashed = !dashed, - )) - .child(button("Clear", cx, |this, cx| this.clear(cx))), - ), - ) - .child( - div() - .size_full() - .child( - canvas( - move |_, _, _| {}, - move |_, _, window, _| { - // First draw background quads - for (bounds, color) in background_quads.iter() { - window.paint_quad(quad( - *bounds, - px(0.), - *color, - px(0.), - gpui::transparent_black(), - Default::default(), - )); - } - - // Then draw the default paths on top - for (path, color) in default_lines { - window.paint_path(path, color); - } - - for points in lines { - if points.len() < 2 { - continue; - } - - let mut builder = PathBuilder::stroke(px(1.)); - if dashed { - builder = builder.dash_array(&[px(4.), px(2.)]); - } - for (i, p) in points.into_iter().enumerate() { - if i == 0 { - builder.move_to(p); - } else { - builder.line_to(p); - } - } - - if let Ok(path) = builder.build() { - window.paint_path(path, gpui::black()); - } - } - }, - ) - .size_full(), - ) - .on_mouse_down( - gpui::MouseButton::Left, - cx.listener(|this, ev: &MouseDownEvent, _, _| { - this._painting = true; - this.start = ev.position; - let path = vec![ev.position]; - this.lines.push(path); - }), - ) - .on_mouse_move(cx.listener(|this, ev: &gpui::MouseMoveEvent, _, cx| { - if !this._painting { - return; - } - - let is_shifted = ev.modifiers.shift; - let mut pos = ev.position; - // When holding shift, draw a straight line - if is_shifted { - let dx = pos.x - this.start.x; - let dy = pos.y - this.start.y; - if dx.abs() > dy.abs() { - pos.y = this.start.y; - } else { - pos.x = this.start.x; - } - } - - if let Some(path) = this.lines.last_mut() { - path.push(pos); - } - - cx.notify(); - })) - .on_mouse_up( - gpui::MouseButton::Left, - cx.listener(|this, _, _, _| { - this._painting = false; - }), - ), - ) - } -} - -fn run_example() { - application().run(|cx| { - if !example_support::load_fonts(cx) { - return; - } - cx.open_window( - WindowOptions { - focus: true, - ..Default::default() - }, - |window, cx| cx.new(|cx| PaintingViewer::new(window, cx)), - ) - .unwrap(); - cx.on_window_closed(|cx, _window_id| { - cx.quit(); - }) - .detach(); - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/paths_bench.rs b/crates/gpui_pre/examples/paths_bench.rs deleted file mode 100644 index 236e70e..0000000 --- a/crates/gpui_pre/examples/paths_bench.rs +++ /dev/null @@ -1,113 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - Background, Bounds, ColorSpace, Context, Path, PathBuilder, Pixels, Render, TitlebarOptions, - Window, WindowBounds, WindowOptions, canvas, div, linear_color_stop, linear_gradient, point, - prelude::*, px, rgb, size, -}; -use gpui_platform::application; - -const DEFAULT_WINDOW_WIDTH: Pixels = px(1024.0); -const DEFAULT_WINDOW_HEIGHT: Pixels = px(768.0); - -struct PaintingViewer { - default_lines: Vec<(Path, Background)>, - _painting: bool, -} - -impl PaintingViewer { - fn new(_window: &mut Window, _cx: &mut Context) -> Self { - let mut lines = vec![]; - - // draw a lightening bolt ⚡ - for _ in 0..2000 { - // draw a ⭐ - let mut builder = PathBuilder::fill(); - builder.move_to(point(px(350.), px(100.))); - builder.line_to(point(px(370.), px(160.))); - builder.line_to(point(px(430.), px(160.))); - builder.line_to(point(px(380.), px(200.))); - builder.line_to(point(px(400.), px(260.))); - builder.line_to(point(px(350.), px(220.))); - builder.line_to(point(px(300.), px(260.))); - builder.line_to(point(px(320.), px(200.))); - builder.line_to(point(px(270.), px(160.))); - builder.line_to(point(px(330.), px(160.))); - builder.line_to(point(px(350.), px(100.))); - let path = builder.build().unwrap(); - lines.push(( - path, - linear_gradient( - 180., - linear_color_stop(rgb(0xFACC15), 0.7), - linear_color_stop(rgb(0xD56D0C), 1.), - ) - .color_space(ColorSpace::Oklab), - )); - } - - Self { - default_lines: lines, - _painting: false, - } - } -} - -impl Render for PaintingViewer { - fn render(&mut self, window: &mut Window, _: &mut Context) -> impl IntoElement { - window.request_animation_frame(); - let lines = self.default_lines.clone(); - div().size_full().child( - canvas( - move |_, _, _| {}, - move |_, _, window, _| { - for (path, color) in lines { - window.paint_path(path, color); - } - }, - ) - .size_full(), - ) - } -} - -fn run_example() { - application().run(|cx| { - if !example_support::load_fonts(cx) { - return; - } - cx.open_window( - WindowOptions { - titlebar: Some(TitlebarOptions { - title: Some("Vulkan".into()), - ..Default::default() - }), - focus: true, - window_bounds: Some(WindowBounds::Windowed(Bounds::centered( - None, - size(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT), - cx, - ))), - ..Default::default() - }, - |window, cx| cx.new(|cx| PaintingViewer::new(window, cx)), - ) - .unwrap(); - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/pattern.rs b/crates/gpui_pre/examples/pattern.rs deleted file mode 100644 index 7d2e98d..0000000 --- a/crates/gpui_pre/examples/pattern.rs +++ /dev/null @@ -1,136 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - App, AppContext, Bounds, Context, Window, WindowBounds, WindowOptions, div, linear_color_stop, - linear_gradient, pattern_slash, prelude::*, px, rgb, size, -}; -use gpui_platform::application; - -struct PatternExample; - -impl Render for PatternExample { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .flex() - .flex_col() - .gap_3() - .bg(rgb(0xffffff)) - .size(px(600.0)) - .justify_center() - .items_center() - .shadow_lg() - .text_xl() - .text_color(rgb(0x000000)) - .child("Pattern Example") - .child( - div() - .flex() - .flex_col() - .border_1() - .border_color(gpui::blue()) - .child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash( - gpui::red(), - 18.0 / 4.0, - 18.0 / 4.0, - ))) - .child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash( - gpui::red(), - 18.0 / 4.0, - 18.0 / 4.0, - ))) - .child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash( - gpui::red(), - 18.0 / 4.0, - 18.0 / 4.0, - ))) - .child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash( - gpui::red(), - 18.0 / 4.0, - 18.0 / 2.0, - ))), - ) - .child( - div() - .flex() - .flex_col() - .border_1() - .border_color(gpui::blue()) - .bg(gpui::green().opacity(0.16)) - .child("Elements the same height should align") - .child(div().w(px(256.0)).h(px(56.0)).bg(pattern_slash( - gpui::red(), - 56.0 / 6.0, - 56.0 / 6.0, - ))) - .child(div().w(px(256.0)).h(px(56.0)).bg(pattern_slash( - gpui::green(), - 56.0 / 6.0, - 56.0 / 6.0, - ))) - .child(div().w(px(256.0)).h(px(56.0)).bg(pattern_slash( - gpui::blue(), - 56.0 / 6.0, - 56.0 / 6.0, - ))) - .child(div().w(px(256.0)).h(px(26.0)).bg(pattern_slash( - gpui::yellow(), - 56.0 / 6.0, - 56.0 / 6.0, - ))), - ) - .child( - div() - .border_1() - .border_color(gpui::blue()) - .w(px(240.0)) - .h(px(40.0)) - .bg(gpui::red()), - ) - .child( - div() - .border_1() - .border_color(gpui::blue()) - .w(px(240.0)) - .h(px(40.0)) - .bg(linear_gradient( - 45., - linear_color_stop(gpui::red(), 0.), - linear_color_stop(gpui::blue(), 1.), - )), - ) - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_window, cx| cx.new(|_cx| PatternExample), - ) - .unwrap(); - - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/popover.rs b/crates/gpui_pre/examples/popover.rs deleted file mode 100644 index 7678a6a..0000000 --- a/crates/gpui_pre/examples/popover.rs +++ /dev/null @@ -1,197 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - Anchor, App, Context, Div, Hsla, Stateful, Window, WindowOptions, anchored, deferred, div, - prelude::*, px, -}; -use gpui_platform::application; - -/// An example show use deferred to create a floating layers. -struct HelloWorld { - open: bool, - secondary_open: bool, -} - -fn button(id: &'static str) -> Stateful
{ - div() - .id(id) - .bg(gpui::black()) - .text_color(gpui::white()) - .px_3() - .py_1() -} - -fn popover() -> Div { - div() - .flex() - .flex_col() - .items_center() - .justify_center() - .shadow_lg() - .p_3() - .rounded_md() - .bg(gpui::white()) - .text_color(gpui::black()) - .border_1() - .text_sm() - .border_color(gpui::black().opacity(0.1)) -} - -fn line(color: Hsla) -> Div { - div().w(px(480.)).h_2().bg(color.opacity(0.25)) -} - -impl HelloWorld { - fn render_secondary_popover( - &mut self, - _window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - button("secondary-btn") - .mt_2() - .child("Child Popover") - .on_click(cx.listener(|this, _, _, cx| { - this.secondary_open = true; - cx.notify(); - })) - .when(self.secondary_open, |this| { - this.child( - // Now GPUI supports nested deferred! - deferred( - anchored() - .anchor(Anchor::TopLeft) - .snap_to_window_with_margin(px(8.)) - .child( - popover() - .child("This is second level Popover with nested deferred!") - .bg(gpui::white()) - .border_color(gpui::blue()) - .on_mouse_down_out(cx.listener(|this, _, _, cx| { - this.secondary_open = false; - cx.notify(); - })), - ), - ) - .priority(2), - ) - }) - } -} - -impl Render for HelloWorld { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .flex() - .flex_col() - .gap_3() - .size_full() - .bg(gpui::white()) - .text_color(gpui::black()) - .justify_center() - .items_center() - .child( - div() - .flex() - .flex_row() - .gap_4() - .child( - button("popover0").child("Opened Popover").child( - deferred( - anchored() - .anchor(Anchor::TopLeft) - .snap_to_window_with_margin(px(8.)) - .child(popover().w_96().gap_3().child( - "This is a default opened Popover, \ - we can use deferred to render it \ - in a floating layer.", - )), - ) - .priority(0), - ), - ) - .child( - button("popover1") - .child("Open Popover") - .on_click(cx.listener(|this, _, _, cx| { - this.open = true; - cx.notify(); - })) - .when(self.open, |this| { - this.child( - deferred( - anchored() - .anchor(Anchor::TopLeft) - .snap_to_window_with_margin(px(8.)) - .child( - popover() - .w_96() - .gap_3() - .child( - "This is first level Popover, \ - we can use deferred to render it \ - in a floating layer.\n\ - Click outside to close.", - ) - .when(!self.secondary_open, |this| { - this.on_mouse_down_out(cx.listener( - |this, _, _, cx| { - this.open = false; - cx.notify(); - }, - )) - }) - // Here we need render popover after the content - // to ensure it will be on top layer. - .child( - self.render_secondary_popover(window, cx), - ), - ), - ) - .priority(1), - ) - }), - ), - ) - .child( - "Here is an example text rendered, \ - to ensure the Popover will float above this contents.", - ) - .children([ - line(gpui::red()), - line(gpui::yellow()), - line(gpui::blue()), - line(gpui::green()), - ]) - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - cx.open_window(WindowOptions::default(), |_, cx| { - cx.new(|_| HelloWorld { - open: false, - secondary_open: false, - }) - }) - .unwrap(); - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/scrollable.rs b/crates/gpui_pre/examples/scrollable.rs deleted file mode 100644 index c817956..0000000 --- a/crates/gpui_pre/examples/scrollable.rs +++ /dev/null @@ -1,78 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{App, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px, size}; -use gpui_platform::application; - -struct Scrollable {} - -impl Render for Scrollable { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .size_full() - .id("vertical") - .p_4() - .overflow_scroll() - .bg(gpui::white()) - .child("Example for test 2 way scroll in nested layout") - .child( - div() - .h(px(5000.)) - .border_1() - .border_color(gpui::blue()) - .bg(gpui::blue().opacity(0.05)) - .p_4() - .child( - div() - .mb_5() - .w_full() - .id("horizontal") - .overflow_scroll() - .child( - div() - .w(px(2000.)) - .h(px(150.)) - .bg(gpui::green().opacity(0.1)) - .hover(|this| this.bg(gpui::green().opacity(0.2))) - .border_1() - .border_color(gpui::green()) - .p_4() - .child("Scroll Horizontal"), - ), - ) - .child("Scroll Vertical"), - ) - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| Scrollable {}), - ) - .unwrap(); - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/set_menus.rs b/crates/gpui_pre/examples/set_menus.rs deleted file mode 100644 index 508a60e..0000000 --- a/crates/gpui_pre/examples/set_menus.rs +++ /dev/null @@ -1,134 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - App, Context, Global, Menu, MenuItem, SharedString, SystemMenuType, Window, WindowOptions, - actions, div, prelude::*, -}; -use gpui_platform::application; - -struct SetMenus; - -impl Render for SetMenus { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .flex() - .bg(gpui::white()) - .size_full() - .justify_center() - .items_center() - .text_xl() - .text_color(gpui::black()) - .child("Set Menus Example") - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - cx.set_global(AppState::new()); - - // Bring the menu bar to the foreground (so you can see the menu bar) - cx.activate(true); - // Register the `quit` function so it can be referenced - // by the `MenuItem::action` in the menu bar - cx.on_action(quit); - cx.on_action(toggle_check); - // Add menu items - set_app_menus(cx); - cx.open_window(WindowOptions::default(), |_, cx| cx.new(|_| SetMenus {})) - .unwrap(); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} - -#[derive(PartialEq)] -enum ViewMode { - List, - Grid, -} - -impl ViewMode { - fn toggle(&mut self) { - *self = match self { - ViewMode::List => ViewMode::Grid, - ViewMode::Grid => ViewMode::List, - } - } -} - -impl Into for ViewMode { - fn into(self) -> SharedString { - match self { - ViewMode::List => "List", - ViewMode::Grid => "Grid", - } - .into() - } -} - -struct AppState { - view_mode: ViewMode, -} - -impl AppState { - fn new() -> Self { - Self { - view_mode: ViewMode::List, - } - } -} - -impl Global for AppState {} - -fn set_app_menus(cx: &mut App) { - let app_state = cx.global::(); - cx.set_menus([Menu::new("set_menus").items([ - MenuItem::os_submenu("Services", SystemMenuType::Services), - MenuItem::separator(), - MenuItem::action("Disabled Item", gpui::NoAction).disabled(true), - MenuItem::submenu(Menu::new("Disabled Submenu").disabled(true)), - MenuItem::separator(), - MenuItem::action("List Mode", ToggleCheck).checked(app_state.view_mode == ViewMode::List), - MenuItem::submenu( - Menu::new("Mode").items([ - MenuItem::action(ViewMode::List, ToggleCheck) - .checked(app_state.view_mode == ViewMode::List), - MenuItem::action(ViewMode::Grid, ToggleCheck) - .checked(app_state.view_mode == ViewMode::Grid), - ]), - ), - MenuItem::separator(), - MenuItem::action("Quit", Quit), - ])]); -} - -// Associate actions using the `actions!` macro (or `Action` derive macro) -actions!(set_menus, [Quit, ToggleCheck]); - -// Define the quit function that is registered with the App -fn quit(_: &Quit, cx: &mut App) { - println!("Gracefully quitting the application..."); - cx.quit(); -} - -fn toggle_check(_: &ToggleCheck, cx: &mut App) { - let app_state = cx.global_mut::(); - app_state.view_mode.toggle(); - set_app_menus(cx); -} diff --git a/crates/gpui_pre/examples/shadow.rs b/crates/gpui_pre/examples/shadow.rs deleted file mode 100644 index 2d8dc09..0000000 --- a/crates/gpui_pre/examples/shadow.rs +++ /dev/null @@ -1,622 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - App, Bounds, BoxShadow, Context, Div, SharedString, Window, WindowBounds, WindowOptions, div, - hsla, prelude::*, px, relative, rgb, size, -}; -use gpui_platform::application; - -struct Shadow {} - -impl Shadow { - fn base() -> Div { - div() - .size_16() - .bg(rgb(0xffffff)) - .rounded_full() - .border_1() - .border_color(hsla(0.0, 0.0, 0.0, 0.1)) - } - - fn square() -> Div { - div() - .size_16() - .bg(rgb(0xffffff)) - .border_1() - .border_color(hsla(0.0, 0.0, 0.0, 0.1)) - } - - fn rounded_small() -> Div { - div() - .size_16() - .bg(rgb(0xffffff)) - .rounded(px(4.)) - .border_1() - .border_color(hsla(0.0, 0.0, 0.0, 0.1)) - } - - fn rounded_medium() -> Div { - div() - .size_16() - .bg(rgb(0xffffff)) - .rounded(px(8.)) - .border_1() - .border_color(hsla(0.0, 0.0, 0.0, 0.1)) - } - - fn rounded_large() -> Div { - div() - .size_16() - .bg(rgb(0xffffff)) - .rounded(px(12.)) - .border_1() - .border_color(hsla(0.0, 0.0, 0.0, 0.1)) - } -} - -fn example(label: impl Into, example: impl IntoElement) -> impl IntoElement { - let label = label.into(); - - div() - .flex() - .flex_col() - .justify_center() - .items_center() - .w(relative(1. / 6.)) - .border_r_1() - .border_color(hsla(0.0, 0.0, 0.0, 1.0)) - .child( - div() - .flex() - .items_center() - .justify_center() - .flex_1() - .py_12() - .child(example), - ) - .child( - div() - .w_full() - .border_t_1() - .border_color(hsla(0.0, 0.0, 0.0, 1.0)) - .p_1() - .flex() - .items_center() - .child(label), - ) -} - -impl Render for Shadow { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .id("shadow-example") - .overflow_y_scroll() - .bg(rgb(0xffffff)) - .size_full() - .text_xs() - .child(div().flex().flex_col().w_full().children(vec![ - div() - .border_b_1() - .border_color(hsla(0.0, 0.0, 0.0, 1.0)) - .flex() - .flex_row() - .children(vec![ - example( - "Square", - Shadow::square().shadow(vec![ - BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.5, 0.5, 0.3)) - .blur_radius(px(8.)), - ]), - ), - example( - "Rounded 4", - Shadow::rounded_small().shadow(vec![ - BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.5, 0.5, 0.3)) - .blur_radius(px(8.)), - ]), - ), - example( - "Rounded 8", - Shadow::rounded_medium().shadow(vec![ - BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.5, 0.5, 0.3)) - .blur_radius(px(8.)), - ]), - ), - example( - "Rounded 16", - Shadow::rounded_large().shadow(vec![ - BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.5, 0.5, 0.3)) - .blur_radius(px(8.)), - ]), - ), - example( - "Circle", - Shadow::base().shadow(vec![ - BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.5, 0.5, 0.3)) - .blur_radius(px(8.)), - ]), - ), - ]), - div() - .border_b_1() - .border_color(hsla(0.0, 0.0, 0.0, 1.0)) - .flex() - .w_full() - .children(vec![ - example("None", Shadow::base()), - // 2Xsmall shadow - example("2X Small", Shadow::base().shadow_2xs()), - // Xsmall shadow - example("Extra Small", Shadow::base().shadow_xs()), - // Small shadow - example("Small", Shadow::base().shadow_sm()), - // Medium shadow - example("Medium", Shadow::base().shadow_md()), - // Large shadow - example("Large", Shadow::base().shadow_lg()), - example("Extra Large", Shadow::base().shadow_xl()), - example("2X Large", Shadow::base().shadow_2xl()), - ]), - // Horizontal list of increasing blur radii - div() - .border_b_1() - .border_color(hsla(0.0, 0.0, 0.0, 1.0)) - .flex() - .children(vec![ - example( - "Blur 0", - Shadow::base().shadow(vec![BoxShadow::new( - px(0.), - px(8.), - hsla(0.0, 0.0, 0.0, 0.3), - )]), - ), - example( - "Blur 2", - Shadow::base().shadow(vec![ - BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) - .blur_radius(px(2.)), - ]), - ), - example( - "Blur 4", - Shadow::base().shadow(vec![ - BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) - .blur_radius(px(4.)), - ]), - ), - example( - "Blur 8", - Shadow::base().shadow(vec![ - BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) - .blur_radius(px(8.)), - ]), - ), - example( - "Blur 16", - Shadow::base().shadow(vec![ - BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) - .blur_radius(px(16.)), - ]), - ), - ]), - // Horizontal list of increasing spread radii - div() - .border_b_1() - .border_color(hsla(0.0, 0.0, 0.0, 1.0)) - .flex() - .children(vec![ - example( - "Spread 0", - Shadow::base().shadow(vec![ - BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) - .blur_radius(px(8.)), - ]), - ), - example( - "Spread 2", - Shadow::base().shadow(vec![ - BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) - .blur_radius(px(8.)) - .spread_radius(px(2.)), - ]), - ), - example( - "Spread 4", - Shadow::base().shadow(vec![ - BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) - .blur_radius(px(8.)) - .spread_radius(px(4.)), - ]), - ), - example( - "Spread 8", - Shadow::base().shadow(vec![ - BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) - .blur_radius(px(8.)) - .spread_radius(px(8.)), - ]), - ), - example( - "Spread 16", - Shadow::base().shadow(vec![ - BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) - .blur_radius(px(8.)) - .spread_radius(px(16.)), - ]), - ), - ]), - // Square spread examples - div() - .border_b_1() - .border_color(hsla(0.0, 0.0, 0.0, 1.0)) - .flex() - .children(vec![ - example( - "Square Spread 0", - Shadow::square().shadow(vec![ - BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) - .blur_radius(px(8.)), - ]), - ), - example( - "Square Spread 8", - Shadow::square().shadow(vec![ - BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) - .blur_radius(px(8.)) - .spread_radius(px(8.)), - ]), - ), - example( - "Square Spread 16", - Shadow::square().shadow(vec![ - BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) - .blur_radius(px(8.)) - .spread_radius(px(16.)), - ]), - ), - ]), - // Rounded large spread examples - div() - .border_b_1() - .border_color(hsla(0.0, 0.0, 0.0, 1.0)) - .flex() - .children(vec![ - example( - "Rounded Large Spread 0", - Shadow::rounded_large().shadow(vec![ - BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) - .blur_radius(px(8.)), - ]), - ), - example( - "Rounded Large Spread 8", - Shadow::rounded_large().shadow(vec![ - BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) - .blur_radius(px(8.)) - .spread_radius(px(8.)), - ]), - ), - example( - "Rounded Large Spread 16", - Shadow::rounded_large().shadow(vec![ - BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) - .blur_radius(px(8.)) - .spread_radius(px(16.)), - ]), - ), - ]), - // Directional shadows - div() - .border_b_1() - .border_color(hsla(0.0, 0.0, 0.0, 1.0)) - .flex() - .children(vec![ - example( - "Left", - Shadow::base().shadow(vec![ - BoxShadow::new(px(-8.), px(0.), hsla(0.0, 0.5, 0.5, 0.3)) - .blur_radius(px(8.)), - ]), - ), - example( - "Right", - Shadow::base().shadow(vec![ - BoxShadow::new(px(8.), px(0.), hsla(0.0, 0.5, 0.5, 0.3)) - .blur_radius(px(8.)), - ]), - ), - example( - "Top", - Shadow::base().shadow(vec![ - BoxShadow::new(px(0.), px(-8.), hsla(0.0, 0.5, 0.5, 0.3)) - .blur_radius(px(8.)), - ]), - ), - example( - "Bottom", - Shadow::base().shadow(vec![ - BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.5, 0.5, 0.3)) - .blur_radius(px(8.)), - ]), - ), - ]), - // Square directional shadows - div() - .border_b_1() - .border_color(hsla(0.0, 0.0, 0.0, 1.0)) - .flex() - .children(vec![ - example( - "Square Left", - Shadow::square().shadow(vec![ - BoxShadow::new(px(-8.), px(0.), hsla(0.0, 0.5, 0.5, 0.3)) - .blur_radius(px(8.)), - ]), - ), - example( - "Square Right", - Shadow::square().shadow(vec![ - BoxShadow::new(px(8.), px(0.), hsla(0.0, 0.5, 0.5, 0.3)) - .blur_radius(px(8.)), - ]), - ), - example( - "Square Top", - Shadow::square().shadow(vec![ - BoxShadow::new(px(0.), px(-8.), hsla(0.0, 0.5, 0.5, 0.3)) - .blur_radius(px(8.)), - ]), - ), - example( - "Square Bottom", - Shadow::square().shadow(vec![ - BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.5, 0.5, 0.3)) - .blur_radius(px(8.)), - ]), - ), - ]), - // Rounded large directional shadows - div() - .border_b_1() - .border_color(hsla(0.0, 0.0, 0.0, 1.0)) - .flex() - .children(vec![ - example( - "Rounded Large Left", - Shadow::rounded_large().shadow(vec![ - BoxShadow::new(px(-8.), px(0.), hsla(0.0, 0.5, 0.5, 0.3)) - .blur_radius(px(8.)), - ]), - ), - example( - "Rounded Large Right", - Shadow::rounded_large().shadow(vec![ - BoxShadow::new(px(8.), px(0.), hsla(0.0, 0.5, 0.5, 0.3)) - .blur_radius(px(8.)), - ]), - ), - example( - "Rounded Large Top", - Shadow::rounded_large().shadow(vec![ - BoxShadow::new(px(0.), px(-8.), hsla(0.0, 0.5, 0.5, 0.3)) - .blur_radius(px(8.)), - ]), - ), - example( - "Rounded Large Bottom", - Shadow::rounded_large().shadow(vec![ - BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.5, 0.5, 0.3)) - .blur_radius(px(8.)), - ]), - ), - ]), - // Multiple shadows for different shapes - div() - .border_b_1() - .border_color(hsla(0.0, 0.0, 0.0, 1.0)) - .flex() - .children(vec![ - example( - "Circle Multiple", - Shadow::base().shadow(vec![ - BoxShadow::new( - px(0.), - px(-12.), - hsla(0.0 / 360., 1.0, 0.5, 0.3), - ) - .blur_radius(px(8.)) - .spread_radius(px(2.)), - BoxShadow::new( - px(12.), - px(0.), - hsla(60.0 / 360., 1.0, 0.5, 0.3), - ) - .blur_radius(px(8.)) - .spread_radius(px(2.)), - BoxShadow::new( - px(0.), - px(12.), - hsla(120.0 / 360., 1.0, 0.5, 0.3), - ) - .blur_radius(px(8.)) - .spread_radius(px(2.)), - BoxShadow::new( - px(-12.), - px(0.), - hsla(240.0 / 360., 1.0, 0.5, 0.3), - ) - .blur_radius(px(8.)) - .spread_radius(px(2.)), - ]), - ), - example( - "Square Multiple", - Shadow::square().shadow(vec![ - BoxShadow::new( - px(0.), - px(-12.), - hsla(0.0 / 360., 1.0, 0.5, 0.3), - ) - .blur_radius(px(8.)) - .spread_radius(px(2.)), - BoxShadow::new( - px(12.), - px(0.), - hsla(60.0 / 360., 1.0, 0.5, 0.3), - ) - .blur_radius(px(8.)) - .spread_radius(px(2.)), - BoxShadow::new( - px(0.), - px(12.), - hsla(120.0 / 360., 1.0, 0.5, 0.3), - ) - .blur_radius(px(8.)) - .spread_radius(px(2.)), - BoxShadow::new( - px(-12.), - px(0.), - hsla(240.0 / 360., 1.0, 0.5, 0.3), - ) - .blur_radius(px(8.)) - .spread_radius(px(2.)), - ]), - ), - example( - "Rounded Large Multiple", - Shadow::rounded_large().shadow(vec![ - BoxShadow::new( - px(0.), - px(-12.), - hsla(0.0 / 360., 1.0, 0.5, 0.3), - ) - .blur_radius(px(8.)) - .spread_radius(px(2.)), - BoxShadow::new( - px(12.), - px(0.), - hsla(60.0 / 360., 1.0, 0.5, 0.3), - ) - .blur_radius(px(8.)) - .spread_radius(px(2.)), - BoxShadow::new( - px(0.), - px(12.), - hsla(120.0 / 360., 1.0, 0.5, 0.3), - ) - .blur_radius(px(8.)) - .spread_radius(px(2.)), - BoxShadow::new( - px(-12.), - px(0.), - hsla(240.0 / 360., 1.0, 0.5, 0.3), - ) - .blur_radius(px(8.)) - .spread_radius(px(2.)), - ]), - ), - ]), - // Inset shadows (CSS `box-shadow: inset ...`). - div() - .border_b_1() - .border_color(hsla(0.0, 0.0, 0.0, 1.0)) - .flex() - .w_full() - .children(vec![ - example( - "Inset basic", - Shadow::base().shadow(vec![ - BoxShadow::new(px(0.), px(0.), hsla(0.0, 0.0, 0.0, 0.5)) - .blur_radius(px(12.)) - .inset(), - ]), - ), - example( - "Inset offset", - Shadow::base().shadow(vec![ - BoxShadow::new(px(6.), px(6.), hsla(0.0, 0.0, 0.0, 0.5)) - .blur_radius(px(8.)) - .inset(), - ]), - ), - example( - "Inset spread", - Shadow::base().shadow(vec![ - BoxShadow::new(px(0.), px(0.), hsla(0.0, 0.0, 0.0, 0.5)) - .blur_radius(px(4.)) - .spread_radius(px(8.)) - .inset(), - ]), - ), - example( - "Inset rounded", - Shadow::rounded_large().shadow(vec![ - BoxShadow::new(px(0.), px(4.), hsla(0.0, 0.0, 0.0, 0.5)) - .blur_radius(px(10.)) - .spread_radius(px(2.)) - .inset(), - ]), - ), - example( - "Inset sharp", - Shadow::square().shadow(vec![ - BoxShadow::new(px(0.), px(0.), hsla(0.0, 0.0, 0.0, 0.6)) - .spread_radius(px(6.)) - .inset(), - ]), - ), - ]), - // Combined: drop + inset shadows on the same element. - div() - .border_b_1() - .border_color(hsla(0.0, 0.0, 0.0, 1.0)) - .flex() - .w_full() - .children(vec![example( - "Drop + Inset", - Shadow::rounded_medium().shadow(vec![ - BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.25)) - .blur_radius(px(12.)), - BoxShadow::new(px(0.), px(2.), hsla(0.0, 0.0, 0.0, 0.4)) - .blur_radius(px(4.)) - .inset(), - ]), - )]), - ])) - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let bounds = Bounds::centered(None, size(px(1000.0), px(800.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| Shadow {}), - ) - .unwrap(); - - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/svg/dragon.svg b/crates/gpui_pre/examples/svg/dragon.svg deleted file mode 100644 index e2de1a0..0000000 --- a/crates/gpui_pre/examples/svg/dragon.svg +++ /dev/null @@ -1,240 +0,0 @@ - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/crates/gpui_pre/examples/svg/svg.rs b/crates/gpui_pre/examples/svg/svg.rs deleted file mode 100644 index c5ac9b4..0000000 --- a/crates/gpui_pre/examples/svg/svg.rs +++ /dev/null @@ -1,108 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "../example_support/fonts.rs"] -mod example_support; - -use std::fs; -use std::path::PathBuf; - -use anyhow::Result; -use gpui::{ - App, AssetSource, Bounds, Context, SharedString, Window, WindowBounds, WindowOptions, div, - prelude::*, px, rgb, size, svg, -}; -use gpui_platform::application; - -struct Assets { - base: PathBuf, -} - -impl AssetSource for Assets { - fn load(&self, path: &str) -> Result>> { - fs::read(self.base.join(path)) - .map(|data| Some(std::borrow::Cow::Owned(data))) - .map_err(|err| err.into()) - } - - fn list(&self, path: &str) -> Result> { - fs::read_dir(self.base.join(path)) - .map(|entries| { - entries - .filter_map(|entry| { - entry - .ok() - .and_then(|entry| entry.file_name().into_string().ok()) - .map(SharedString::from) - }) - .collect() - }) - .map_err(|err| err.into()) - } -} - -struct SvgExample; - -impl Render for SvgExample { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .flex() - .flex_row() - .size_full() - .justify_center() - .items_center() - .gap_8() - .bg(rgb(0xffffff)) - .child( - svg() - .path("svg/dragon.svg") - .size_8() - .text_color(rgb(0xff0000)), - ) - .child( - svg() - .path("svg/dragon.svg") - .size_8() - .text_color(rgb(0x00ff00)), - ) - .child( - svg() - .path("svg/dragon.svg") - .size_8() - .text_color(rgb(0x0000ff)), - ) - } -} - -fn run_example() { - application() - .with_assets(Assets { - base: PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples"), - }) - .run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| SvgExample), - ) - .unwrap(); - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/system_notifications.rs b/crates/gpui_pre/examples/system_notifications.rs deleted file mode 100644 index faf6919..0000000 --- a/crates/gpui_pre/examples/system_notifications.rs +++ /dev/null @@ -1,155 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -//! Demonstrates posting, replacing, dismissing, and responding to system notifications. - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - App, Bounds, Context, Div, SharedString, Stateful, SystemNotification, - SystemNotificationAction, SystemNotificationResponse, Window, WindowBounds, WindowOptions, div, - prelude::*, px, rgb, size, -}; -use gpui_platform::application; - -const NOTIFICATION_TAG: &str = "gpui-system-notification-example"; - -struct SystemNotificationExample { - revision: usize, - status: SharedString, -} - -impl Render for SystemNotificationExample { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .flex() - .flex_col() - .size_full() - .gap_4() - .p_8() - .bg(rgb(0x18181b)) - .text_color(rgb(0xf4f4f5)) - .child(div().text_2xl().child("GPUI system notifications")) - .child(div().text_sm().text_color(rgb(0xa1a1aa)).child( - "Post repeatedly to replace the notification with the same tag. Click the \ - notification body or an action button to send a response back to GPUI.", - )) - .child( - div() - .flex() - .gap_3() - .child(button("show", "Show or replace").on_click(cx.listener( - |this, _, _, cx| { - this.revision += 1; - let revision = this.revision; - cx.show_system_notification(SystemNotification { - tag: NOTIFICATION_TAG.into(), - title: format!("Example notification {revision}").into(), - body: "This notification was posted by the GPUI example.".into(), - actions: vec![ - SystemNotificationAction { - id: "open".into(), - label: "Open".into(), - }, - SystemNotificationAction { - id: "snooze".into(), - label: "Snooze".into(), - }, - ], - }); - this.status = format!("Posted notification revision {revision}").into(); - cx.notify(); - }, - ))) - .child( - button("dismiss", "Dismiss").on_click(cx.listener(|this, _, _, cx| { - cx.dismiss_system_notification(NOTIFICATION_TAG); - this.status = "Dismissed the notification".into(); - cx.notify(); - })), - ), - ) - .child( - div() - .mt_2() - .p_4() - .rounded_md() - .bg(rgb(0x27272a)) - .child(self.status.clone()), - ) - .when(cfg!(target_os = "macos"), |this| { - this.child(div().mt_2().text_xs().text_color(rgb(0x71717a)).child( - "macOS only delivers notifications when this example runs from an app bundle.", - )) - }) - } -} - -fn button(id: &'static str, label: &'static str) -> Stateful
{ - div() - .id(id) - .px_4() - .py_2() - .rounded_md() - .bg(rgb(0x3f3f46)) - .hover(|style| style.bg(rgb(0x52525b))) - .active(|style| style.bg(rgb(0x71717a))) - .cursor_pointer() - .child(label) -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - cx.set_app_identity("dev.zed.gpui.system-notifications", "GPUI Notifications"); - - let view = cx.new(|_| SystemNotificationExample { - revision: 0, - status: "No notification posted yet".into(), - }); - cx.on_system_notification_response({ - let view = view.clone(); - move |response, cx| { - let SystemNotificationResponse { tag, action_id } = response; - view.update(cx, |this, cx| { - this.status = match action_id { - Some(action_id) => { - format!("Received action '{action_id}' for tag '{tag}'").into() - } - None => format!("Notification body clicked for tag '{tag}'").into(), - }; - cx.notify(); - }); - } - }); - - let bounds = Bounds::centered(None, size(px(560.), px(360.)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - titlebar: Some(gpui::TitlebarOptions { - title: Some("System Notifications Example".into()), - ..Default::default() - }), - ..Default::default() - }, - move |_, _| view, - ) - .expect("failed to open system notifications example window"); - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/tab_stop.rs b/crates/gpui_pre/examples/tab_stop.rs deleted file mode 100644 index cc7c34a..0000000 --- a/crates/gpui_pre/examples/tab_stop.rs +++ /dev/null @@ -1,220 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - App, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString, Stateful, Window, - WindowBounds, WindowOptions, actions, div, prelude::*, px, size, -}; -use gpui_platform::application; - -actions!(example, [Tab, TabPrev]); - -struct Example { - focus_handle: FocusHandle, - items: Vec, - message: SharedString, -} - -impl Example { - fn new(window: &mut Window, cx: &mut Context) -> Self { - let items = vec![ - cx.focus_handle().tab_index(1).tab_stop(true), - cx.focus_handle().tab_index(2).tab_stop(true), - cx.focus_handle().tab_index(3).tab_stop(true), - cx.focus_handle(), - cx.focus_handle().tab_index(2).tab_stop(true), - ]; - - let focus_handle = cx.focus_handle(); - window.focus(&focus_handle, cx); - - Self { - focus_handle, - items, - message: SharedString::from("Press `Tab`, `Shift-Tab` to switch focus."), - } - } - - fn on_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context) { - window.focus_next(cx); - self.message = SharedString::from("You have pressed `Tab`."); - } - - fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context) { - window.focus_prev(cx); - self.message = SharedString::from("You have pressed `Shift-Tab`."); - } -} - -impl Render for Example { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - fn tab_stop_style(this: T) -> T { - this.border_3().border_color(gpui::blue()) - } - - fn button(id: impl Into) -> Stateful
{ - div() - .id(id) - .h_10() - .flex_1() - .flex() - .justify_center() - .items_center() - .border_1() - .border_color(gpui::black()) - .bg(gpui::black()) - .text_color(gpui::white()) - .focus(tab_stop_style) - .shadow_sm() - } - - div() - .id("app") - .track_focus(&self.focus_handle) - .on_action(cx.listener(Self::on_tab)) - .on_action(cx.listener(Self::on_tab_prev)) - .size_full() - .flex() - .flex_col() - .p_4() - .gap_3() - .bg(gpui::white()) - .text_color(gpui::black()) - .child(self.message.clone()) - .children( - self.items - .clone() - .into_iter() - .enumerate() - .map(|(ix, item_handle)| { - div() - .id(("item", ix)) - .track_focus(&item_handle) - .h_10() - .w_full() - .flex() - .justify_center() - .items_center() - .border_1() - .border_color(gpui::black()) - .when( - item_handle.tab_stop && item_handle.is_focused(window), - tab_stop_style, - ) - .map(|this| match item_handle.tab_stop { - true => this - .hover(|this| this.bg(gpui::black().opacity(0.1))) - .child(format!("tab_index: {}", item_handle.tab_index)), - false => this.opacity(0.4).child("tab_stop: false"), - }) - }), - ) - .child( - div() - .flex() - .flex_row() - .gap_3() - .items_center() - .child( - button("el1") - .tab_index(4) - .child("Button 1") - .on_click(cx.listener(|this, _, _, cx| { - this.message = "You have clicked Button 1.".into(); - cx.notify(); - })), - ) - .child( - button("el2") - .tab_index(5) - .child("Button 2") - .on_click(cx.listener(|this, _, _, cx| { - this.message = "You have clicked Button 2.".into(); - cx.notify(); - })), - ), - ) - .child( - div() - .id("group-1") - .tab_index(6) - .tab_group() - .tab_stop(false) - .child( - button("group-1-button-1") - .tab_index(1) - .child("Tab index [6, 1]"), - ) - .child( - button("group-1-button-2") - .tab_index(2) - .child("Tab index [6, 2]"), - ) - .child( - button("group-1-button-3") - .tab_index(3) - .child("Tab index [6, 3]"), - ), - ) - .child( - div() - .id("group-2") - .tab_index(7) - .tab_group() - .tab_stop(false) - .child( - button("group-2-button-1") - .tab_index(1) - .child("Tab index [7, 1]"), - ) - .child( - button("group-2-button-2") - .tab_index(2) - .child("Tab index [7, 2]"), - ) - .child( - button("group-2-button-3") - .tab_index(3) - .child("Tab index [7, 3]"), - ), - ) - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - cx.bind_keys([ - KeyBinding::new("tab", Tab, None), - KeyBinding::new("shift-tab", TabPrev, None), - ]); - - let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |window, cx| cx.new(|cx| Example::new(window, cx)), - ) - .unwrap(); - - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/testing.rs b/crates/gpui_pre/examples/testing.rs deleted file mode 100644 index 76e3b77..0000000 --- a/crates/gpui_pre/examples/testing.rs +++ /dev/null @@ -1,559 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -//! Example demonstrating GPUI's testing infrastructure. -//! -//! When run normally, this displays an interactive counter window. -//! The tests below demonstrate various GPUI testing patterns. -//! -//! Run the app: cargo run -p gpui --example testing -//! Run tests: cargo test -p gpui --example testing --features test-support - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - App, Bounds, Context, FocusHandle, Focusable, Render, Task, Window, WindowBounds, - WindowOptions, actions, div, prelude::*, px, rgb, size, -}; -use gpui_platform::application; - -actions!(counter, [Increment, Decrement]); - -struct Counter { - count: i32, - focus_handle: FocusHandle, - _subscription: gpui::Subscription, -} - -/// Event emitted by Counter -struct CounterEvent; - -impl gpui::EventEmitter for Counter {} - -impl Counter { - fn new(cx: &mut Context) -> Self { - let subscription = cx.subscribe_self(|this: &mut Self, _event: &CounterEvent, _cx| { - this.count = 999; - }); - - Self { - count: 0, - focus_handle: cx.focus_handle(), - _subscription: subscription, - } - } - - fn increment(&mut self, _: &Increment, _window: &mut Window, cx: &mut Context) { - self.count += 1; - cx.notify(); - } - - fn decrement(&mut self, _: &Decrement, _window: &mut Window, cx: &mut Context) { - self.count -= 1; - cx.notify(); - } - - fn load(&self, cx: &mut Context) -> Task<()> { - cx.spawn(async move |this, cx| { - // Simulate loading data (e.g., from disk or network) - this.update(cx, |counter, _| { - counter.count = 100; - }) - .ok(); - }) - } - - fn reload(&self, cx: &mut Context) { - cx.spawn(async move |this, cx| { - // Simulate reloading data in the background - this.update(cx, |counter, _| { - counter.count += 50; - }) - .ok(); - }) - .detach(); - } -} - -impl Focusable for Counter { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for Counter { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .id("counter") - .key_context("Counter") - .on_action(cx.listener(Self::increment)) - .on_action(cx.listener(Self::decrement)) - .track_focus(&self.focus_handle) - .flex() - .flex_col() - .gap_4() - .bg(rgb(0x1e1e2e)) - .size_full() - .justify_center() - .items_center() - .child( - div() - .text_3xl() - .text_color(rgb(0xcdd6f4)) - .child(format!("{}", self.count)), - ) - .child( - div() - .flex() - .gap_2() - .child( - div() - .id("decrement") - .px_4() - .py_2() - .bg(rgb(0x313244)) - .hover(|s| s.bg(rgb(0x45475a))) - .rounded_md() - .cursor_pointer() - .text_color(rgb(0xcdd6f4)) - .on_click(cx.listener(|this, _, window, cx| { - this.decrement(&Decrement, window, cx) - })) - .child("−"), - ) - .child( - div() - .id("increment") - .px_4() - .py_2() - .bg(rgb(0x313244)) - .hover(|s| s.bg(rgb(0x45475a))) - .rounded_md() - .cursor_pointer() - .text_color(rgb(0xcdd6f4)) - .on_click(cx.listener(|this, _, window, cx| { - this.increment(&Increment, window, cx) - })) - .child("+"), - ), - ) - .child( - div() - .flex() - .gap_2() - .child( - div() - .id("load") - .px_4() - .py_2() - .bg(rgb(0x313244)) - .hover(|s| s.bg(rgb(0x45475a))) - .rounded_md() - .cursor_pointer() - .text_color(rgb(0xcdd6f4)) - .on_click(cx.listener(|this, _, _, cx| { - this.load(cx).detach(); - })) - .child("Load"), - ) - .child( - div() - .id("reload") - .px_4() - .py_2() - .bg(rgb(0x313244)) - .hover(|s| s.bg(rgb(0x45475a))) - .rounded_md() - .cursor_pointer() - .text_color(rgb(0xcdd6f4)) - .on_click(cx.listener(|this, _, _, cx| { - this.reload(cx); - })) - .child("Reload"), - ), - ) - .child( - div() - .text_sm() - .text_color(rgb(0x6c7086)) - .child("Press ↑/↓ or click buttons"), - ) - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - cx.bind_keys([ - gpui::KeyBinding::new("up", Increment, Some("Counter")), - gpui::KeyBinding::new("down", Decrement, Some("Counter")), - ]); - - let bounds = Bounds::centered(None, size(px(300.), px(200.)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |window, cx| { - let counter = cx.new(|cx| Counter::new(cx)); - counter.focus_handle(cx).focus(window, cx); - counter - }, - ) - .unwrap(); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::{TestAppContext, VisualTestContext}; - use rand::prelude::*; - - /// Here's a basic GPUI test. Just add the macro and take a TestAppContext as an argument! - /// - /// Note that synchronous side effects run immediately after your "update*" calls complete. - #[gpui::test] - fn basic_testing(cx: &mut TestAppContext) { - let counter = cx.new(|cx| Counter::new(cx)); - - counter.update(cx, |counter, _| { - counter.count = 42; - }); - - // Note that TestAppContext doesn't support `read(cx)` - let updated = counter.read_with(cx, |counter, _| counter.count); - assert_eq!(updated, 42); - - // Emit an event - the subscriber will run immediately after the update finishes - counter.update(cx, |_, cx| { - cx.emit(CounterEvent); - }); - - let count_after_update = counter.read_with(cx, |counter, _| counter.count); - assert_eq!( - count_after_update, 999, - "Side effects should run after update completes" - ); - } - - /// Tests which involve the window require you to construct a VisualTestContext. - /// Just like synchronous side effects, the window will be drawn after every "update*" - /// call, so you can test render-dependent behavior. - #[gpui::test] - fn test_counter_in_window(cx: &mut TestAppContext) { - let window = cx.update(|cx| { - cx.open_window(Default::default(), |_, cx| cx.new(|cx| Counter::new(cx))) - .unwrap() - }); - - let mut cx = VisualTestContext::from_window(window.into(), cx); - let counter = window.root(&mut cx).unwrap(); - - // Action dispatch depends on the element tree to resolve which action handler - // to call, and this works exactly as you'd expect in a test. - let focus_handle = counter.read_with(&cx, |counter, _| counter.focus_handle.clone()); - cx.update(|window, cx| { - focus_handle.dispatch_action(&Increment, window, cx); - }); - - let count_after = counter.read_with(&cx, |counter, _| counter.count); - assert_eq!( - count_after, 1, - "Action dispatched via focus handle should increment" - ); - } - - /// GPUI tests can also be async, simply add the async keyword before the test. - /// Note that the test executor is single thread, so async side effects (including - /// background tasks) won't run until you explicitly yield control. - #[gpui::test] - async fn test_async_operations(cx: &mut TestAppContext) { - let counter = cx.new(|cx| Counter::new(cx)); - - // Tasks can be awaited directly - counter.update(cx, |counter, cx| counter.load(cx)).await; - - let count = counter.read_with(cx, |counter, _| counter.count); - assert_eq!(count, 100, "Load task should have set count to 100"); - - // But side effects don't run until you yield control - counter.update(cx, |counter, cx| counter.reload(cx)); - - let count = counter.read_with(cx, |counter, _| counter.count); - assert_eq!(count, 100, "Detached reload task shouldn't have run yet"); - - // This runs all pending tasks - cx.run_until_parked(); - - let count = counter.read_with(cx, |counter, _| counter.count); - assert_eq!(count, 150, "Reload task should have run after parking"); - } - - /// Note that the test executor panics if you await a future that waits on - /// something outside GPUI's control, like a reading a file or network IO. - /// You should mock external systems where possible, as this feature can be used - /// to detect potential deadlocks in your async code. - /// - /// However, if you want to disable this check use `allow_parking()` - #[gpui::test] - async fn test_allow_parking(cx: &mut TestAppContext) { - // Allow the thread to park - cx.executor().allow_parking(); - - // Simulate an external system (like a file system) with an OS thread - let (tx, rx) = futures::channel::oneshot::channel(); - std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_millis(5)); - tx.send(42).ok(); - }); - - // Without allow_parking(), this await would panic because GPUI's - // scheduler runs out of tasks while waiting for the external thread. - let result = rx.await.unwrap(); - assert_eq!(result, 42); - } - - /// GPUI also provides support for property testing, via the iterations flag - #[gpui::test(iterations = 10)] - fn test_counter_random_operations(cx: &mut TestAppContext, mut rng: StdRng) { - let window = cx.update(|cx| { - cx.open_window(Default::default(), |_, cx| cx.new(|cx| Counter::new(cx))) - .unwrap() - }); - let mut cx = VisualTestContext::from_window(window.into(), cx); - - let counter = cx.new(|cx| Counter::new(cx)); - - // Perform random increments/decrements - let mut expected = 0i32; - for _ in 0..100 { - if rng.random_bool(0.5) { - expected += 1; - counter.update_in(&mut cx, |counter, window, cx| { - counter.increment(&Increment, window, cx) - }); - } else { - expected -= 1; - counter.update_in(&mut cx, |counter, window, cx| { - counter.decrement(&Decrement, window, cx) - }); - } - } - - let actual = counter.read_with(&cx, |counter, _| counter.count); - assert_eq!( - actual, expected, - "Counter should match expected after random ops" - ); - } - - /// Now, all of those tests are good, but GPUI also provides strong support for testing distributed systems. - /// Let's setup a mock network and enhance the counter to send messages over it. - mod distributed_systems { - use std::sync::{Arc, Mutex}; - - /// The state of the mock network. - struct MockNetworkState { - ordering: Vec, - a_to_b: Vec, - b_to_a: Vec, - } - - /// A mock network that delivers messages between two peers. - #[derive(Clone)] - struct MockNetwork { - state: Arc>, - } - - impl MockNetwork { - fn new() -> Self { - Self { - state: Arc::new(Mutex::new(MockNetworkState { - ordering: Vec::new(), - a_to_b: Vec::new(), - b_to_a: Vec::new(), - })), - } - } - - fn a_client(&self) -> NetworkClient { - NetworkClient { - network: self.clone(), - is_a: true, - } - } - - fn b_client(&self) -> NetworkClient { - NetworkClient { - network: self.clone(), - is_a: false, - } - } - } - - /// A client handle for sending/receiving messages over the mock network. - #[derive(Clone)] - struct NetworkClient { - network: MockNetwork, - is_a: bool, - } - - // See, networking is easy! - impl NetworkClient { - fn send(&self, value: i32) { - let mut network = self.network.state.lock().unwrap(); - network.ordering.push(value); - if self.is_a { - network.b_to_a.push(value); - } else { - network.a_to_b.push(value); - } - } - - fn receive_all(&self) -> Vec { - let mut network = self.network.state.lock().unwrap(); - if self.is_a { - network.a_to_b.drain(..).collect() - } else { - network.b_to_a.drain(..).collect() - } - } - } - - use gpui::Context; - - /// A networked counter that can send/receive over a mock network. - struct NetworkedCounter { - count: i32, - client: NetworkClient, - } - - impl NetworkedCounter { - fn new(client: NetworkClient) -> Self { - Self { count: 0, client } - } - - /// Increment the counter and broadcast the change. - fn increment(&mut self, delta: i32, cx: &mut Context) { - self.count += delta; - - cx.background_spawn({ - let client = self.client.clone(); - async move { - client.send(delta); - } - }) - .detach(); - } - - /// Process incoming increment requests. - fn sync(&mut self) { - for delta in self.client.receive_all() { - self.count += delta; - } - } - } - - use super::*; - - /// You can simulate distributed systems with multiple app contexts, simply by adding - /// additional parameters. - #[gpui::test] - fn test_app_sync(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { - let network = MockNetwork::new(); - - let a = cx_a.new(|_| NetworkedCounter::new(network.a_client())); - let b = cx_b.new(|_| NetworkedCounter::new(network.b_client())); - - // B increments locally and broadcasts the delta - b.update(cx_b, |b, cx| b.increment(42, cx)); - b.read_with(cx_b, |b, _| assert_eq!(b.count, 42)); // B's count is set immediately - a.read_with(cx_a, |a, _| assert_eq!(a.count, 0)); // A's count is in a side effect - - cx_b.run_until_parked(); // Send the delta from B - a.update(cx_a, |a, _| a.sync()); // Receive the delta at A - - b.read_with(cx_b, |b, _| assert_eq!(b.count, 42)); // Both counts now match - a.read_with(cx_a, |a, _| assert_eq!(a.count, 42)); - } - - /// Multiple apps can run concurrently, and to capture this each test app shares - /// a dispatcher. Whenever you call `run_until_parked`, the dispatcher will randomly - /// pick which app's tasks to run next. This allows you to test that your distributed code - /// is robust to different execution orderings. - #[gpui::test(iterations = 10)] - fn test_random_interleaving( - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - mut rng: StdRng, - ) { - let network = MockNetwork::new(); - - // Track execution order - let mut original_order = Vec::new(); - let a = cx_a.new(|_| NetworkedCounter::new(MockNetwork::a_client(&network))); - let b = cx_b.new(|_| NetworkedCounter::new(MockNetwork::b_client(&network))); - - let num_operations: usize = rng.random_range(3..8); - - for i in 0..num_operations { - let i = i as i32; - let which = rng.random_bool(0.5); - - original_order.push(i); - if which { - b.update(cx_b, |b, cx| b.increment(i, cx)); - } else { - a.update(cx_a, |a, cx| a.increment(i, cx)); - } - } - - // This will send all of the pending increment messages, from both a and b - cx_a.run_until_parked(); - - a.update(cx_a, |a, _| a.sync()); - b.update(cx_b, |b, _| b.sync()); - - let a_count = a.read_with(cx_a, |a, _| a.count); - let b_count = b.read_with(cx_b, |b, _| b.count); - - assert_eq!(a_count, b_count, "A and B should have the same count"); - - // Nicely format the execution order output. - // Run this test with `-- --nocapture` to see it! - let actual = network.state.lock().unwrap().ordering.clone(); - let spawned: Vec<_> = original_order.iter().map(|n| format!("{}", n)).collect(); - let ran: Vec<_> = actual.iter().map(|n| format!("{}", n)).collect(); - let diff: Vec<_> = original_order - .iter() - .zip(actual.iter()) - .map(|(o, a)| { - if o == a { - " ".to_string() - } else { - "^".to_string() - } - }) - .collect(); - println!("spawned: [{}]", spawned.join(", ")); - println!("ran: [{}]", ran.join(", ")); - println!(" [{}]", diff.join(", ")); - } - } -} diff --git a/crates/gpui_pre/examples/text.rs b/crates/gpui_pre/examples/text.rs deleted file mode 100644 index a1244eb..0000000 --- a/crates/gpui_pre/examples/text.rs +++ /dev/null @@ -1,415 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use std::{ - borrow::Cow, - ops::{Deref, DerefMut}, - sync::Arc, -}; - -use gpui::{ - AbsoluteLength, App, Context, DefiniteLength, ElementId, Global, Hsla, Menu, SharedString, - TextStyle, TitlebarOptions, Window, WindowBounds, WindowOptions, bounds, colors::DefaultColors, - div, point, prelude::*, px, relative, rgb, size, -}; -use gpui_platform::application; -use std::iter; - -#[derive(Clone, Debug)] -pub struct TextContext { - font_size: f32, - line_height: f32, - type_scale: f32, -} - -impl Default for TextContext { - fn default() -> Self { - TextContext { - font_size: 16.0, - line_height: 1.3, - type_scale: 1.33, - } - } -} - -impl TextContext { - pub fn get_global(cx: &App) -> &Arc { - &cx.global::().0 - } -} - -#[derive(Clone, Debug)] -pub struct GlobalTextContext(pub Arc); - -impl Deref for GlobalTextContext { - type Target = Arc; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for GlobalTextContext { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl Global for GlobalTextContext {} - -pub trait ActiveTextContext { - fn text_context(&self) -> &Arc; -} - -impl ActiveTextContext for App { - fn text_context(&self) -> &Arc { - &self.global::().0 - } -} - -#[derive(Clone, PartialEq)] -pub struct SpecimenTheme { - pub bg: Hsla, - pub fg: Hsla, -} - -impl Default for SpecimenTheme { - fn default() -> Self { - Self { - bg: gpui::white(), - fg: gpui::black(), - } - } -} - -impl SpecimenTheme { - pub fn invert(&self) -> Self { - Self { - bg: self.fg, - fg: self.bg, - } - } -} - -#[derive(Debug, Clone, PartialEq, IntoElement)] -struct Specimen { - id: ElementId, - scale: f32, - text_style: Option, - string: SharedString, - invert: bool, -} - -impl Specimen { - pub fn new(id: usize) -> Self { - let string = SharedString::new_static("The quick brown fox jumps over the lazy dog"); - let id_string = format!("specimen-{}", id); - let id = ElementId::Name(id_string.into()); - Self { - id, - scale: 1.0, - text_style: None, - string, - invert: false, - } - } - - pub fn invert(mut self) -> Self { - self.invert = !self.invert; - self - } - - pub fn scale(mut self, scale: f32) -> Self { - self.scale = scale; - self - } -} - -impl RenderOnce for Specimen { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let rem_size = window.rem_size(); - let scale = self.scale; - let global_style = cx.text_context(); - - let style_override = self.text_style; - - let mut font_size = global_style.font_size; - let mut line_height = global_style.line_height; - - if let Some(style_override) = style_override { - font_size = style_override.font_size.to_pixels(rem_size).into(); - line_height = match style_override.line_height { - DefiniteLength::Absolute(absolute_len) => match absolute_len { - AbsoluteLength::Rems(absolute_len) => absolute_len.to_pixels(rem_size).into(), - AbsoluteLength::Pixels(absolute_len) => absolute_len.into(), - }, - DefiniteLength::Fraction(value) => value, - }; - } - - let mut theme = SpecimenTheme::default(); - - if self.invert { - theme = theme.invert(); - } - - div() - .id(self.id) - .bg(theme.bg) - .text_color(theme.fg) - .text_size(px(font_size * scale)) - .line_height(relative(line_height)) - .p(px(10.0)) - .child(self.string) - } -} - -#[derive(Debug, Clone, PartialEq, IntoElement)] -struct CharacterGrid { - scale: f32, - invert: bool, - text_style: Option, -} - -impl CharacterGrid { - pub fn new() -> Self { - Self { - scale: 1.0, - invert: false, - text_style: None, - } - } - - pub fn scale(mut self, scale: f32) -> Self { - self.scale = scale; - self - } -} - -impl RenderOnce for CharacterGrid { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let mut theme = SpecimenTheme::default(); - - if self.invert { - theme = theme.invert(); - } - - let characters = vec![ - "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "A", "B", "C", "D", "E", "F", "G", - "H", "I", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", - "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "p", "q", - "r", "s", "t", "u", "v", "w", "x", "y", "z", "ẞ", "ſ", "ß", "ð", "Þ", "þ", "α", "β", - "Γ", "γ", "Δ", "δ", "η", "θ", "ι", "κ", "Λ", "λ", "μ", "ν", "ξ", "π", "τ", "υ", "φ", - "χ", "ψ", "∂", "а", "в", "Ж", "ж", "З", "з", "К", "к", "л", "м", "Н", "н", "Р", "р", - "У", "у", "ф", "ч", "ь", "ы", "Э", "э", "Я", "я", "ij", "öẋ", ".,", "⣝⣑", "~", "*", - "_", "^", "`", "'", "(", "{", "«", "#", "&", "@", "$", "¢", "%", "|", "?", "¶", "µ", - "❮", "<=", "!=", "==", "--", "++", "=>", "->", "🏀", "🎊", "😍", "❤️", "👍", "👎", - ]; - - let columns = 20; - let rows = characters.len().div_ceil(columns); - - let grid_rows = (0..rows).map(|row_idx| { - let start_idx = row_idx * columns; - let end_idx = (start_idx + columns).min(characters.len()); - - div() - .w_full() - .flex() - .flex_row() - .children((start_idx..end_idx).map(|i| { - div() - .text_center() - .size(px(62.)) - .bg(theme.bg) - .text_color(theme.fg) - .text_size(px(24.0)) - .line_height(relative(1.0)) - .child(characters[i]) - })) - .when(end_idx - start_idx < columns, |d| { - d.children( - iter::repeat_with(|| div().flex_1()).take(columns - (end_idx - start_idx)), - ) - }) - }); - - div().p_4().gap_2().flex().flex_col().children(grid_rows) - } -} - -struct TextExample { - next_id: usize, - font_family: SharedString, -} - -impl TextExample { - fn next_id(&mut self) -> usize { - self.next_id += 1; - self.next_id - } - - fn button( - text: &str, - cx: &mut Context, - on_click: impl Fn(&mut Self, &mut Context) + 'static, - ) -> impl IntoElement { - div() - .id(text.to_string()) - .flex_none() - .child(text.to_string()) - .bg(gpui::black()) - .text_color(gpui::white()) - .active(|this| this.opacity(0.8)) - .px_3() - .py_1() - .on_click(cx.listener(move |this, _, _, cx| on_click(this, cx))) - } -} - -const FONT_FAMILIES: [&str; 5] = [ - ".ZedMono", - ".SystemUIFont", - "Menlo", - "Monaco", - "Courier New", -]; - -impl Render for TextExample { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let tcx = cx.text_context(); - let colors = cx.default_colors().clone(); - - let type_scale = tcx.type_scale; - - let step_down_2 = 1.0 / (type_scale * type_scale); - let step_down_1 = 1.0 / type_scale; - let base = 1.0; - let step_up_1 = base * type_scale; - let step_up_2 = step_up_1 * type_scale; - let step_up_3 = step_up_2 * type_scale; - let step_up_4 = step_up_3 * type_scale; - let step_up_5 = step_up_4 * type_scale; - let step_up_6 = step_up_5 * type_scale; - - div() - .font_family(self.font_family.clone()) - .size_full() - .child( - div() - .bg(gpui::white()) - .border_b_1() - .border_color(gpui::black()) - .p_3() - .flex() - .child(Self::button(&self.font_family, cx, |this, cx| { - let new_family = FONT_FAMILIES - .iter() - .position(|f| *f == this.font_family.as_str()) - .map(|idx| FONT_FAMILIES[(idx + 1) % FONT_FAMILIES.len()]) - .unwrap_or(FONT_FAMILIES[0]); - - this.font_family = SharedString::new(new_family); - cx.notify(); - })), - ) - .child( - div() - .id("text-example") - .overflow_y_scroll() - .overflow_x_hidden() - .bg(rgb(0xffffff)) - .size_full() - .child(div().child(CharacterGrid::new().scale(base))) - .child( - div() - .child(Specimen::new(self.next_id()).scale(step_down_2)) - .child(Specimen::new(self.next_id()).scale(step_down_2).invert()) - .child(Specimen::new(self.next_id()).scale(step_down_1)) - .child(Specimen::new(self.next_id()).scale(step_down_1).invert()) - .child(Specimen::new(self.next_id()).scale(base)) - .child(Specimen::new(self.next_id()).scale(base).invert()) - .child(Specimen::new(self.next_id()).scale(step_up_1)) - .child(Specimen::new(self.next_id()).scale(step_up_1).invert()) - .child(Specimen::new(self.next_id()).scale(step_up_2)) - .child(Specimen::new(self.next_id()).scale(step_up_2).invert()) - .child(Specimen::new(self.next_id()).scale(step_up_3)) - .child(Specimen::new(self.next_id()).scale(step_up_3).invert()) - .child(Specimen::new(self.next_id()).scale(step_up_4)) - .child(Specimen::new(self.next_id()).scale(step_up_4).invert()) - .child(Specimen::new(self.next_id()).scale(step_up_5)) - .child(Specimen::new(self.next_id()).scale(step_up_5).invert()) - .child(Specimen::new(self.next_id()).scale(step_up_6)) - .child(Specimen::new(self.next_id()).scale(step_up_6).invert()), - ), - ) - .child(div().w(px(240.)).h_full().bg(colors.container)) - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - cx.set_menus(vec![Menu { - name: "GPUI Typography".into(), - disabled: false, - items: vec![], - }]); - - let fonts = [include_bytes!( - "../../../assets/fonts/lilex/Lilex-Regular.ttf" - )] - .iter() - .map(|b| Cow::Borrowed(&b[..])) - .collect(); - - _ = cx.text_system().add_fonts(fonts); - - cx.init_colors(); - cx.set_global(GlobalTextContext(Arc::new(TextContext::default()))); - - let window = cx - .open_window( - WindowOptions { - titlebar: Some(TitlebarOptions { - title: Some("GPUI Typography".into()), - ..Default::default() - }), - window_bounds: Some(WindowBounds::Windowed(bounds( - point(px(0.0), px(0.0)), - size(px(920.), px(720.)), - ))), - ..Default::default() - }, - |_window, cx| { - cx.new(|_cx| TextExample { - next_id: 0, - font_family: ".ZedMono".into(), - }) - }, - ) - .unwrap(); - - window - .update(cx, |_view, _window, cx| { - cx.activate(true); - }) - .unwrap(); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/text_layout.rs b/crates/gpui_pre/examples/text_layout.rs deleted file mode 100644 index 07f560a..0000000 --- a/crates/gpui_pre/examples/text_layout.rs +++ /dev/null @@ -1,117 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - App, Bounds, Context, FontStyle, FontWeight, StyledText, Window, WindowBounds, WindowOptions, - div, prelude::*, px, size, -}; -use gpui_platform::application; - -struct HelloWorld {} - -impl Render for HelloWorld { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .bg(gpui::white()) - .flex() - .flex_col() - .gap_2() - .p_4() - .size_full() - .child(div().child("Text left")) - .child(div().text_center().child("Text center")) - .child(div().text_right().child("Text right")) - .child(div().text_decoration_1().child("Text left (underline)")) - .child( - div() - .text_center() - .text_decoration_1() - .child("Text center (underline)"), - ) - .child( - div() - .text_right() - .text_decoration_1() - .child("Text right (underline)"), - ) - .child(div().line_through().child("Text left (line_through)")) - .child( - div() - .text_center() - .line_through() - .child("Text center (line_through)"), - ) - .child( - div() - .text_right() - .line_through() - .child("Text right (line_through)"), - ) - .child( - div() - .flex() - .gap_2() - .justify_between() - .child( - div() - .w(px(400.)) - .border_1() - .border_color(gpui::blue()) - .p_1() - .whitespace_nowrap() - .overflow_hidden() - .text_center() - .child("A long non-wrapping text align center"), - ) - .child( - div() - .w_32() - .border_1() - .border_color(gpui::blue()) - .p_1() - .whitespace_nowrap() - .overflow_hidden() - .text_right() - .child("100%"), - ), - ) - .child(div().flex().gap_2().justify_between().child( - StyledText::new("ABCD").with_highlights([ - (0..1, FontWeight::EXTRA_BOLD.into()), - (2..3, FontStyle::Italic.into()), - ]), - )) - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| HelloWorld {}), - ) - .unwrap(); - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/text_wrapper.rs b/crates/gpui_pre/examples/text_wrapper.rs deleted file mode 100644 index 51e58fc..0000000 --- a/crates/gpui_pre/examples/text_wrapper.rs +++ /dev/null @@ -1,144 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - App, Bounds, Context, TextOverflow, Window, WindowBounds, WindowOptions, div, prelude::*, px, - size, -}; -use gpui_platform::application; - -struct HelloWorld {} - -impl Render for HelloWorld { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let text = "The longest word 你好世界这段是中文,こんにちはこの段落は日本語です in any of the major \ - English language dictionaries is pneumonoultramicroscopicsilicovolcanoconiosis, a word that \ - refers to a lung disease contracted from the inhalation of very fine silica particles, \ - a url https://github.com/zed-industries/zed/pull/35724?query=foo&bar=2, \ - specifically from a volcano; medically, it is the same as silicosis."; - div() - .id("page") - .size_full() - .flex() - .flex_col() - .p_2() - .gap_2() - .bg(gpui::white()) - .child( - div() - .flex() - .flex_row() - .flex_shrink_0() - .gap_2() - .child( - div() - .flex() - .border_1() - .border_color(gpui::red()) - .text_ellipsis() - .child("longer text in flex 1"), - ) - .child( - div() - .flex() - .border_1() - .border_color(gpui::red()) - .text_ellipsis() - .child("short flex"), - ) - .child( - div() - .overflow_hidden() - .border_1() - .border_color(gpui::red()) - .text_ellipsis() - .w_full() - .child("A short text in normal div"), - ), - ) - .child( - div() - .flex_shrink_0() - .text_xl() - .truncate() - .border_1() - .border_color(gpui::blue()) - .child("ELLIPSIS: ".to_owned() + text), - ) - .child( - div() - .flex_shrink_0() - .text_xl() - .overflow_hidden() - .text_ellipsis() - .line_clamp(2) - .border_1() - .border_color(gpui::blue()) - .child("ELLIPSIS 2 lines: ".to_owned() + text), - ) - .child( - div() - .flex_shrink_0() - .text_xl() - .overflow_hidden() - .text_overflow(TextOverflow::Truncate("".into())) - .border_1() - .border_color(gpui::green()) - .child("TRUNCATE: ".to_owned() + text), - ) - .child( - div() - .flex_shrink_0() - .text_xl() - .overflow_hidden() - .text_overflow(TextOverflow::Truncate("".into())) - .line_clamp(3) - .border_1() - .border_color(gpui::green()) - .child("TRUNCATE 3 lines: ".to_owned() + text), - ) - .child( - div() - .flex_shrink_0() - .text_xl() - .whitespace_nowrap() - .overflow_hidden() - .border_1() - .border_color(gpui::black()) - .child("NOWRAP: ".to_owned() + text), - ) - .child(div().text_xl().w_full().child(text)) - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| HelloWorld {}), - ) - .unwrap(); - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/tree.rs b/crates/gpui_pre/examples/tree.rs deleted file mode 100644 index 891be47..0000000 --- a/crates/gpui_pre/examples/tree.rs +++ /dev/null @@ -1,64 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -//! Renders a div with deep children hierarchy. This example is useful to exemplify that Zed can -//! handle deep hierarchies (even though it cannot just yet!). -#[path = "example_support/fonts.rs"] -mod example_support; - -use std::sync::LazyLock; - -use gpui::{App, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px, size}; -use gpui_platform::application; - -struct Tree {} - -static DEPTH: LazyLock = LazyLock::new(|| { - std::env::var("GPUI_TREE_DEPTH") - .ok() - .and_then(|depth| depth.parse().ok()) - .unwrap_or_else(|| 50) -}); - -impl Render for Tree { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - let mut depth = *DEPTH; - static COLORS: [gpui::Hsla; 4] = [gpui::red(), gpui::blue(), gpui::green(), gpui::yellow()]; - let mut colors = COLORS.iter().cycle().copied(); - let mut next_div = || div().p_0p5().bg(colors.next().unwrap()); - let mut innermost_node = next_div(); - while depth > 0 { - innermost_node = next_div().child(innermost_node); - depth -= 1; - } - innermost_node - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| Tree {}), - ) - .unwrap(); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/uniform_list.rs b/crates/gpui_pre/examples/uniform_list.rs deleted file mode 100644 index 4a2c478..0000000 --- a/crates/gpui_pre/examples/uniform_list.rs +++ /dev/null @@ -1,71 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - App, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px, rgb, size, - uniform_list, -}; -use gpui_platform::application; - -struct UniformListExample {} - -impl Render for UniformListExample { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - div().size_full().bg(rgb(0xffffff)).child( - uniform_list( - "entries", - 50, - cx.processor(|_this, range, _window, _cx| { - let mut items = Vec::new(); - for ix in range { - let item = ix + 1; - - items.push( - div() - .id(ix) - .px_2() - .cursor_pointer() - .on_click(move |_event, _window, _cx| { - println!("clicked Item {item:?}"); - }) - .child(format!("Item {item}")), - ); - } - items - }), - ) - .h_full(), - ) - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| UniformListExample {}), - ) - .unwrap(); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/view_example/example_editor.rs b/crates/gpui_pre/examples/view_example/example_editor.rs deleted file mode 100644 index 2064d7c..0000000 --- a/crates/gpui_pre/examples/view_example/example_editor.rs +++ /dev/null @@ -1,549 +0,0 @@ -//! `Editor` — the workhorse entity. It owns the cursor, blink, focus, keyboard -//! handling, and the specialized text-shaping renderer. The *text itself* lives -//! in a shared `Entity` it's handed at construction, so the value is -//! readable/writable from outside while the editing machinery stays in here. -//! -//! This is the piece that proves the point: a text input is genuinely -//! complicated, and `View` lets all of that complexity live in one entity that -//! anything can embed. - -use std::ops::Range; -use std::time::Duration; - -use gpui::{ - App, Bounds, Context, ElementInputHandler, Entity, EntityInputHandler, FocusHandle, Focusable, - InteractiveElement, LayoutId, PaintQuad, Pixels, ShapedLine, SharedString, Subscription, Task, - TextRun, UTF16Selection, Window, fill, hsla, point, prelude::*, px, relative, size, -}; -use unicode_segmentation::*; - -use crate::{Backspace, Delete, End, Home, Left, Right}; - -pub struct Editor { - pub value: Entity, - pub focus_handle: FocusHandle, - pub cursor: usize, - pub cursor_visible: bool, - _blink_task: Task<()>, - _subscriptions: Vec, -} - -impl Editor { - /// An editor that owns its own string internally, seeded with `text`. - /// Nothing to allocate or wire up at the call site. - pub fn new(text: impl Into, window: &mut Window, cx: &mut Context) -> Self { - let value = cx.new(|_| text.into()); - Self::over(value, window, cx) - } - - /// An editor over a string *you* own, so the value is shared in and out. - pub fn over(value: Entity, window: &mut Window, cx: &mut Context) -> Self { - let focus_handle = cx.focus_handle(); - - let focus_sub = cx.on_focus(&focus_handle, window, |this, _window, cx| { - this.start_blink(cx); - }); - let blur_sub = cx.on_blur(&focus_handle, window, |this, _window, cx| { - this.stop_blink(cx); - }); - - // The value is shared: anything can write it while we hold a cursor into - // it. Observe it so external writes (a) clamp the cursor back onto a char - // boundary before the next IME round-trip can slice out of bounds, and - // (b) notify us, so an `editor.cached(..)` subtree re-renders — the cache - // is keyed on *our* notify, not the value's. - let value_sub = cx.observe(&value, |this, value, cx| { - let content = value.read(cx); - let mut cursor = this.cursor.min(content.len()); - while cursor > 0 && !content.is_char_boundary(cursor) { - cursor -= 1; - } - this.cursor = cursor; - cx.notify(); - }); - - Self { - value, - focus_handle, - cursor: 0, - cursor_visible: false, - _blink_task: Task::ready(()), - _subscriptions: vec![focus_sub, blur_sub, value_sub], - } - } - - /// The current text. Read this from anywhere to get the value out. - pub fn text(&self, cx: &App) -> String { - self.value.read(cx).clone() - } - - fn start_blink(&mut self, cx: &mut Context) { - self.cursor_visible = true; - self._blink_task = Self::spawn_blink_task(cx); - } - - fn stop_blink(&mut self, cx: &mut Context) { - self.cursor_visible = false; - self._blink_task = Task::ready(()); - cx.notify(); - } - - fn spawn_blink_task(cx: &mut Context) -> Task<()> { - cx.spawn(async move |this, cx| { - loop { - cx.background_executor() - .timer(Duration::from_millis(500)) - .await; - let result = this.update(cx, |editor, cx| { - editor.cursor_visible = !editor.cursor_visible; - cx.notify(); - }); - if result.is_err() { - break; - } - } - }) - } - - fn reset_blink(&mut self, cx: &mut Context) { - self.cursor_visible = true; - self._blink_task = Self::spawn_blink_task(cx); - } - - pub fn left(&mut self, _: &Left, _: &mut Window, cx: &mut Context) { - let content = self.text(cx); - if self.cursor > 0 { - self.cursor = previous_boundary(&content, self.cursor); - } - self.reset_blink(cx); - cx.notify(); - } - - pub fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context) { - let content = self.text(cx); - if self.cursor < content.len() { - self.cursor = next_boundary(&content, self.cursor); - } - self.reset_blink(cx); - cx.notify(); - } - - pub fn home(&mut self, _: &Home, _: &mut Window, cx: &mut Context) { - self.cursor = 0; - self.reset_blink(cx); - cx.notify(); - } - - pub fn end(&mut self, _: &End, _: &mut Window, cx: &mut Context) { - self.cursor = self.text(cx).len(); - self.reset_blink(cx); - cx.notify(); - } - - pub fn backspace(&mut self, _: &Backspace, _: &mut Window, cx: &mut Context) { - let content = self.text(cx); - if self.cursor > 0 { - let prev = previous_boundary(&content, self.cursor); - let cursor = self.cursor; - self.value.update(cx, |s, cx| { - s.drain(prev..cursor); - cx.notify(); - }); - self.cursor = prev; - } - self.reset_blink(cx); - cx.notify(); - } - - pub fn delete(&mut self, _: &Delete, _: &mut Window, cx: &mut Context) { - let content = self.text(cx); - if self.cursor < content.len() { - let next = next_boundary(&content, self.cursor); - let cursor = self.cursor; - self.value.update(cx, |s, cx| { - s.drain(cursor..next); - cx.notify(); - }); - } - self.reset_blink(cx); - cx.notify(); - } - - pub fn insert_newline(&mut self, cx: &mut Context) { - let cursor = self.cursor; - self.value.update(cx, |s, cx| { - s.insert(cursor, '\n'); - cx.notify(); - }); - self.cursor += 1; - self.reset_blink(cx); - cx.notify(); - } -} - -fn previous_boundary(content: &str, offset: usize) -> usize { - content - .grapheme_indices(true) - .rev() - .find_map(|(idx, _)| (idx < offset).then_some(idx)) - .unwrap_or(0) -} - -fn next_boundary(content: &str, offset: usize) -> usize { - content - .grapheme_indices(true) - .find_map(|(idx, _)| (idx > offset).then_some(idx)) - .unwrap_or(content.len()) -} - -fn offset_from_utf16(content: &str, offset: usize) -> usize { - let mut utf8_offset = 0; - let mut utf16_count = 0; - for ch in content.chars() { - if utf16_count >= offset { - break; - } - utf16_count += ch.len_utf16(); - utf8_offset += ch.len_utf8(); - } - utf8_offset -} - -fn offset_to_utf16(content: &str, offset: usize) -> usize { - let mut utf16_offset = 0; - let mut utf8_count = 0; - for ch in content.chars() { - if utf8_count >= offset { - break; - } - utf8_count += ch.len_utf8(); - utf16_offset += ch.len_utf16(); - } - utf16_offset -} - -fn range_to_utf16(content: &str, range: &Range) -> Range { - offset_to_utf16(content, range.start)..offset_to_utf16(content, range.end) -} - -fn range_from_utf16(content: &str, range_utf16: &Range) -> Range { - offset_from_utf16(content, range_utf16.start)..offset_from_utf16(content, range_utf16.end) -} - -impl Focusable for Editor { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl EntityInputHandler for Editor { - fn text_for_range( - &mut self, - range_utf16: Range, - actual_range: &mut Option>, - _window: &mut Window, - cx: &mut Context, - ) -> Option { - let content = self.text(cx); - let range = range_from_utf16(&content, &range_utf16); - actual_range.replace(range_to_utf16(&content, &range)); - Some(content[range].to_string()) - } - - fn selected_text_range( - &mut self, - _ignore_disabled_input: bool, - _window: &mut Window, - cx: &mut Context, - ) -> Option { - let content = self.text(cx); - let utf16_cursor = offset_to_utf16(&content, self.cursor); - Some(UTF16Selection { - range: utf16_cursor..utf16_cursor, - reversed: false, - }) - } - - fn marked_text_range( - &self, - _window: &mut Window, - _cx: &mut Context, - ) -> Option> { - None - } - - fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context) {} - - fn replace_text_in_range( - &mut self, - range_utf16: Option>, - new_text: &str, - _window: &mut Window, - cx: &mut Context, - ) { - let content = self.text(cx); - let range = range_utf16 - .as_ref() - .map(|r| range_from_utf16(&content, r)) - .unwrap_or(self.cursor..self.cursor); - - let new_content = content[..range.start].to_owned() + new_text + &content[range.end..]; - self.cursor = range.start + new_text.len(); - self.value.update(cx, |s, cx| { - *s = new_content; - cx.notify(); - }); - self.reset_blink(cx); - cx.notify(); - } - - fn replace_and_mark_text_in_range( - &mut self, - range_utf16: Option>, - new_text: &str, - _new_selected_range_utf16: Option>, - window: &mut Window, - cx: &mut Context, - ) { - self.replace_text_in_range(range_utf16, new_text, window, cx); - } - - fn bounds_for_range( - &mut self, - _range_utf16: Range, - _bounds: Bounds, - _window: &mut Window, - _cx: &mut Context, - ) -> Option> { - None - } - - fn character_index_for_point( - &mut self, - _point: gpui::Point, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - None - } -} - -impl gpui::Render for Editor { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - EditorText { - editor: cx.entity(), - } - } -} - -// --------------------------------------------------------------------------- -// EditorText — the specialized renderer: shapes the text and paints the cursor. -// --------------------------------------------------------------------------- - -struct EditorText { - editor: Entity, -} - -struct EditorTextPrepaint { - lines: Vec, - cursor: Option, -} - -impl IntoElement for EditorText { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -impl Element for EditorText { - type RequestLayoutState = (); - type PrepaintState = EditorTextPrepaint; - - fn id(&self) -> Option { - None - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _id: Option<&gpui::GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - let editor = self.editor.read(cx); - let content = editor.value.read(cx); - let line_count = content.split('\n').count().max(1); - let line_height = window.line_height(); - let mut style = gpui::Style::default(); - style.size.width = relative(1.).into(); - style.size.height = (line_height * line_count as f32).into(); - (window.request_layout(style, [], cx), ()) - } - - fn prepaint( - &mut self, - _id: Option<&gpui::GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState { - let editor = self.editor.read(cx); - let content = editor.value.read(cx).clone(); - let cursor_offset = editor.cursor; - let cursor_visible = editor.cursor_visible; - let is_focused = editor.focus_handle.is_focused(window); - - let style = window.text_style(); - let text_color = style.color; - let font_size = style.font_size.to_pixels(window.rem_size()); - let line_height = window.line_height(); - - let is_placeholder = content.is_empty(); - - let lines: Vec = if is_placeholder { - let placeholder: SharedString = "Type here...".into(); - let run = TextRun { - len: placeholder.len(), - font: style.font(), - color: hsla(0., 0., 0.5, 0.5), - background_color: None, - underline: None, - strikethrough: None, - }; - vec![ - window - .text_system() - .shape_line(placeholder, font_size, &[run], None), - ] - } else { - content - .split('\n') - .map(|line_str| { - let text: SharedString = SharedString::from(line_str.to_string()); - let run = TextRun { - len: text.len(), - font: style.font(), - color: text_color, - background_color: None, - underline: None, - strikethrough: None, - }; - window - .text_system() - .shape_line(text, font_size, &[run], None) - }) - .collect() - }; - - let cursor = if is_focused && cursor_visible && !is_placeholder { - let (cursor_line, offset_in_line) = cursor_line_and_offset(&content, cursor_offset); - let cursor_line = cursor_line.min(lines.len().saturating_sub(1)); - let cursor_x = lines[cursor_line].x_for_index(offset_in_line); - Some(fill( - Bounds::new( - point( - bounds.left() + cursor_x, - bounds.top() + line_height * cursor_line as f32, - ), - size(px(1.5), line_height), - ), - text_color, - )) - } else if is_focused && cursor_visible && is_placeholder { - Some(fill( - Bounds::new( - point(bounds.left(), bounds.top()), - size(px(1.5), line_height), - ), - text_color, - )) - } else { - None - }; - - EditorTextPrepaint { lines, cursor } - } - - fn paint( - &mut self, - _id: Option<&gpui::GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - prepaint: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - let focus_handle = self.editor.read(cx).focus_handle.clone(); - window.handle_input( - &focus_handle, - ElementInputHandler::new(bounds, self.editor.clone()), - cx, - ); - - let line_height = window.line_height(); - for (i, line) in prepaint.lines.iter().enumerate() { - let origin = point(bounds.left(), bounds.top() + line_height * i as f32); - line.paint(origin, line_height, gpui::TextAlign::Left, None, window, cx) - .unwrap(); - } - - if let Some(cursor) = prepaint.cursor.take() { - window.paint_quad(cursor); - } - } -} - -fn cursor_line_and_offset(content: &str, cursor: usize) -> (usize, usize) { - let mut line_index = 0; - let mut line_start = 0; - for (i, ch) in content.char_indices() { - if i >= cursor { - break; - } - if ch == '\n' { - line_index += 1; - line_start = i + 1; - } - } - (line_index, cursor - line_start) -} - -pub fn standard_actions(editor: Entity) -> impl FnOnce(E) -> E { - move |element| { - element - .on_action({ - let editor = editor.clone(); - move |a: &Left, window, cx| editor.update(cx, |e, cx| e.left(a, window, cx)) - }) - .on_action({ - let editor = editor.clone(); - move |a: &Right, window, cx| editor.update(cx, |e, cx| e.right(a, window, cx)) - }) - .on_action({ - let editor = editor.clone(); - move |a: &Home, window, cx| editor.update(cx, |e, cx| e.home(a, window, cx)) - }) - .on_action({ - let editor = editor.clone(); - move |a: &End, window, cx| editor.update(cx, |e, cx| e.end(a, window, cx)) - }) - .on_action({ - let editor = editor.clone(); - move |a: &Backspace, window, cx| { - editor.update(cx, |e, cx| e.backspace(a, window, cx)) - } - }) - .on_action(move |a: &Delete, window, cx| { - editor.update(cx, |e, cx| e.delete(a, window, cx)) - }) - } -} diff --git a/crates/gpui_pre/examples/view_example/example_input.rs b/crates/gpui_pre/examples/view_example/example_input.rs deleted file mode 100644 index 25d7401..0000000 --- a/crates/gpui_pre/examples/view_example/example_input.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! `Input` — a single-line text input. The shaping layer over `Editor`. -//! -//! Construct it two ways, depending on how much state you want to own: -//! * `Input::new(value: Entity)` — you hold just the string; the input -//! allocates the `Editor` internally via `use_state`. Value readable, cursor hidden. -//! * `Input::editor(editor: Entity)` — you hold the editor; cursor/selection -//! are now yours to read and drive too. -//! -//! Either way the chrome is identical. Because the string (or editor) is the -//! input's *identity*, the internal `use_state(Editor)` is collision-safe across -//! any number of inputs. - -use gpui::{ - App, BoxShadow, CursorStyle, Entity, EntityId, Hsla, IntoElement, Pixels, StyleRefinement, - Window, div, hsla, point, prelude::*, px, white, -}; - -use crate::example_editor::{Editor, standard_actions}; - -enum Source { - Value(Entity), - Editor(Entity), -} - -#[derive(IntoElement)] -pub struct Input { - source: Source, - width: Option, - color: Option, -} - -impl Input { - /// Backed by a bare string; the editor is allocated internally. - pub fn new(value: Entity) -> Self { - Self { - source: Source::Value(value), - width: None, - color: None, - } - } - - /// Backed by an editor you own (so you can read/drive its cursor). - pub fn editor(editor: Entity) -> Self { - Self { - source: Source::Editor(editor), - width: None, - color: None, - } - } - - pub fn width(mut self, width: Pixels) -> Self { - self.width = Some(width); - self - } - - pub fn color(mut self, color: Hsla) -> Self { - self.color = Some(color); - self - } -} - -impl gpui::View for Input { - fn entity_id(&self) -> Option { - Some(match &self.source { - Source::Value(value) => value.entity_id(), - Source::Editor(editor) => editor.entity_id(), - }) - } - - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - // Get the editor: use the one we were handed, or allocate it under our - // own (string-derived) identity so it persists and never collides. - let editor = match self.source { - Source::Value(value) => { - window.use_state(cx, move |window, cx| Editor::over(value, window, cx)) - } - Source::Editor(editor) => editor, - }; - - let focus_handle = editor.read(cx).focus_handle.clone(); - let is_focused = focus_handle.is_focused(window); - let text_color = self.color.unwrap_or(hsla(0., 0., 0.1, 1.)); - let box_width = self.width.unwrap_or(px(300.)); - - let border = if is_focused { - hsla(220. / 360., 0.8, 0.5, 1.) - } else { - hsla(0., 0., 0.75, 1.) - }; - - div() - .id("input") - .key_context("TextInput") - .track_focus(&focus_handle) - .cursor(CursorStyle::IBeam) - .map(standard_actions(editor.clone())) - .w(box_width) - .h(px(36.)) - .px(px(8.)) - .bg(white()) - .border_1() - .border_color(border) - .when(is_focused, |this| { - this.shadow(vec![BoxShadow { - color: hsla(220. / 360., 0.8, 0.5, 0.3), - offset: point(px(0.), px(0.)), - blur_radius: px(4.), - spread_radius: px(1.), - inset: false, - }]) - }) - .rounded(px(4.)) - .overflow_hidden() - .flex() - .items_center() - .line_height(px(20.)) - .text_size(px(14.)) - .text_color(text_color) - .child(editor.cached(StyleRefinement::default().size_full())) - } -} diff --git a/crates/gpui_pre/examples/view_example/example_tests.rs b/crates/gpui_pre/examples/view_example/example_tests.rs deleted file mode 100644 index a3edae8..0000000 --- a/crates/gpui_pre/examples/view_example/example_tests.rs +++ /dev/null @@ -1,131 +0,0 @@ -//! Tests for the input composition. Require the `test-support` feature: -//! -//! ```sh -//! cargo test -p gpui --example view_example --features test-support -//! ``` - -#[cfg(test)] -mod tests { - use gpui::{Context, Entity, KeyBinding, TestAppContext, Window, prelude::*}; - - use crate::example_editor::Editor; - use crate::example_input::Input; - use crate::{Backspace, Delete, End, Home, Left, Right}; - - /// Two inputs, each backed by an editor we own (so the test can focus and - /// read them). Proves data flows through the shared `String` and that - /// sibling inputs stay isolated. - struct Harness { - a: Entity, - b: Entity, - } - - impl Render for Harness { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - gpui::div() - .child(Input::editor(self.a.clone())) - .child(Input::editor(self.b.clone())) - } - } - - fn bind_keys(cx: &mut TestAppContext) { - cx.update(|cx| { - cx.bind_keys([ - KeyBinding::new("backspace", Backspace, None), - KeyBinding::new("delete", Delete, None), - KeyBinding::new("left", Left, None), - KeyBinding::new("right", Right, None), - KeyBinding::new("home", Home, None), - KeyBinding::new("end", End, None), - ]); - }); - } - - fn setup( - cx: &mut TestAppContext, - ) -> ( - Entity, - Entity, - Entity, - &mut gpui::VisualTestContext, - ) { - bind_keys(cx); - - let (harness, cx) = cx.add_window_view(|window, cx| { - let a_value = cx.new(|_| String::new()); - let b_value = cx.new(|_| String::new()); - let a = cx.new(|cx| Editor::over(a_value, window, cx)); - let b = cx.new(|cx| Editor::over(b_value, window, cx)); - Harness { a, b } - }); - - let a = cx.read_entity(&harness, |h, _| h.a.clone()); - let b = cx.read_entity(&harness, |h, _| h.b.clone()); - let a_value = cx.read_entity(&a, |e, _| e.value.clone()); - let b_value = cx.read_entity(&b, |e, _| e.value.clone()); - - // Focus the first input's editor. - cx.update(|window, cx| { - let focus_handle = a.read(cx).focus_handle.clone(); - window.focus(&focus_handle, cx); - }); - - (a, a_value, b_value, cx) - } - - #[gpui::test] - fn typing_updates_the_shared_string(cx: &mut TestAppContext) { - let (editor, a_value, _b_value, cx) = setup(cx); - - cx.simulate_input("hello"); - - cx.read_entity(&a_value, |value, _| assert_eq!(value, "hello")); - cx.read_entity(&editor, |editor, _| assert_eq!(editor.cursor, 5)); - } - - #[gpui::test] - fn sibling_inputs_are_isolated(cx: &mut TestAppContext) { - let (_editor, a_value, b_value, cx) = setup(cx); - - cx.simulate_input("x"); - - cx.read_entity(&a_value, |value, _| assert_eq!(value, "x")); - cx.read_entity(&b_value, |value, _| { - assert_eq!(value, "", "typing in input A must not touch input B") - }); - } - - #[gpui::test] - fn external_writes_clamp_the_cursor(cx: &mut TestAppContext) { - let (editor, a_value, _b_value, cx) = setup(cx); - - cx.simulate_input("hello"); - cx.read_entity(&editor, |editor, _| assert_eq!(editor.cursor, 5)); - - // Write the shared value from outside the editor. The old cursor (5) - // now points into the middle of a multi-byte character; the editor's - // observation must clamp it back onto a boundary. - cx.update(|_, cx| { - a_value.update(cx, |value, cx| { - *value = "日本".to_string(); - cx.notify(); - }) - }); - - cx.read_entity(&a_value, |value, _| assert_eq!(value, "日本")); - cx.read_entity(&editor, |editor, _| { - assert_eq!(editor.cursor, 3, "cursor must clamp to a char boundary"); - }); - } - - #[gpui::test] - fn arrows_move_the_cursor(cx: &mut TestAppContext) { - let (editor, _a_value, _b_value, cx) = setup(cx); - - cx.simulate_input("abc"); - cx.read_entity(&editor, |editor, _| assert_eq!(editor.cursor, 3)); - - cx.simulate_keystrokes("left left"); - cx.read_entity(&editor, |editor, _| assert_eq!(editor.cursor, 1)); - } -} diff --git a/crates/gpui_pre/examples/view_example/example_text_area.rs b/crates/gpui_pre/examples/view_example/example_text_area.rs deleted file mode 100644 index 07640b9..0000000 --- a/crates/gpui_pre/examples/view_example/example_text_area.rs +++ /dev/null @@ -1,118 +0,0 @@ -//! `TextArea` — a multi-line text box. Same `Editor` workhorse, taller chrome, -//! and `Enter` inserts a newline instead of being ignored. Constructible from a -//! string or an editor, exactly like [`Input`](crate::example_input::Input). - -use gpui::{ - App, BoxShadow, CursorStyle, Entity, EntityId, Hsla, IntoElement, StyleRefinement, Window, div, - hsla, point, prelude::*, px, white, -}; - -use crate::Enter; -use crate::example_editor::{Editor, standard_actions}; - -enum Source { - Value(Entity), - Editor(Entity), -} - -#[derive(IntoElement)] -pub struct TextArea { - source: Source, - rows: usize, - color: Option, -} - -impl TextArea { - pub fn new(value: Entity, rows: usize) -> Self { - Self { - source: Source::Value(value), - rows, - color: None, - } - } - - pub fn editor(editor: Entity, rows: usize) -> Self { - Self { - source: Source::Editor(editor), - rows, - color: None, - } - } - - pub fn color(mut self, color: Hsla) -> Self { - self.color = Some(color); - self - } -} - -impl gpui::View for TextArea { - fn entity_id(&self) -> Option { - Some(match &self.source { - Source::Value(value) => value.entity_id(), - Source::Editor(editor) => editor.entity_id(), - }) - } - - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let editor = match self.source { - Source::Value(value) => { - window.use_state(cx, move |window, cx| Editor::over(value, window, cx)) - } - Source::Editor(editor) => editor, - }; - - let focus_handle = editor.read(cx).focus_handle.clone(); - let is_focused = focus_handle.is_focused(window); - let text_color = self.color.unwrap_or(hsla(0., 0., 0.1, 1.)); - let row_height = px(20.); - let box_height = row_height * self.rows as f32 + px(16.); - - let border = if is_focused { - hsla(220. / 360., 0.8, 0.5, 1.) - } else { - hsla(0., 0., 0.75, 1.) - }; - - div() - .id("text-area") - .key_context("TextInput") - .track_focus(&focus_handle) - .cursor(CursorStyle::IBeam) - .map(standard_actions(editor.clone())) - // Enter is the one binding that differs from a single-line input. - .on_action({ - let editor = editor.clone(); - move |_: &Enter, _window, cx| editor.update(cx, |e, cx| e.insert_newline(cx)) - }) - .w(px(400.)) - .h(box_height) - .p(px(8.)) - .bg(white()) - .border_1() - .border_color(border) - .when(is_focused, |this| { - this.shadow(vec![BoxShadow { - color: hsla(220. / 360., 0.8, 0.5, 0.3), - offset: point(px(0.), px(0.)), - blur_radius: px(4.), - spread_radius: px(1.), - inset: false, - }]) - }) - .rounded(px(4.)) - .overflow_hidden() - .line_height(row_height) - .text_size(px(14.)) - .text_color(text_color) - // The cache style is computed from the `rows` prop: change `rows` and - // the editor's cached bounds change, busting its cache and re-laying - // out the text. (`Input` just uses `size_full()` — nothing to vary.) - .child( - editor.cached( - StyleRefinement::default() - .w_full() - .h(row_height * self.rows as f32), - ), - ) - } -} diff --git a/crates/gpui_pre/examples/view_example/view_example_main.rs b/crates/gpui_pre/examples/view_example/view_example_main.rs deleted file mode 100644 index d2e4991..0000000 --- a/crates/gpui_pre/examples/view_example/view_example_main.rs +++ /dev/null @@ -1,179 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -//! View example — composing a text input from the `View` primitives. -//! -//! The whole point: a text input is deceptively complicated, and `View` makes it -//! easy to compose one. Three pieces, each shown in its own section: -//! -//! * `Editor` — the workhorse entity: cursor, blink, focus, keyboard, and a -//! specialized text renderer. All the hard parts live here. -//! * `String` — the data plane. `editor.text(cx)` / `value.read(cx)` get it out. -//! * `Input` / `TextArea` — the shaping layer. Each takes a `String` (and grows -//! the editor internally) OR an `Editor` (so you can read the cursor). -//! -//! Run: `cargo run -p gpui --example view_example` - -#[path = "../example_support/fonts.rs"] -mod example_support; - -mod example_editor; -mod example_input; -mod example_text_area; - -#[cfg(test)] -mod example_tests; - -use example_editor::Editor; -use example_input::Input; -use example_text_area::TextArea; - -use gpui::{ - App, Bounds, Context, Div, Entity, IntoElement, KeyBinding, Render, SharedString, Window, - WindowBounds, WindowOptions, actions, div, hsla, prelude::*, px, rgb, size, -}; -use gpui_platform::application; - -actions!( - view_example, - [Backspace, Delete, Left, Right, Home, End, Enter, Quit] -); - -/// A tiny stateless view that reads an editor's cursor and is composed *beside* -/// the thing editing it — two views over one entity, zero wiring. -#[derive(IntoElement)] -struct CursorReadout { - editor: Entity, -} - -impl CursorReadout { - fn new(editor: Entity) -> Self { - Self { editor } - } -} - -impl gpui::RenderOnce for CursorReadout { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let cursor = self.editor.read(cx).cursor; - div() - .text_sm() - .text_color(hsla(0., 0., 0.45, 1.)) - .child(SharedString::from(format!("cursor @ {cursor}"))) - } -} - -struct ViewExample; - -impl ViewExample { - fn new() -> Self { - Self - } -} - -impl Render for ViewExample { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - // The data plane: plain strings, allocated at the top by the hook. - let name = window.use_state(cx, |_, _| String::new()); - let email = window.use_state(cx, |_, _| String::from("me@example.com")); - let bio = window.use_state(cx, |_, _| String::new()); - // Editors that own their own string internally — no extra wiring up top. - let notes = window.use_state(cx, |window, cx| Editor::new("multi\nline", window, cx)); - let owned = window.use_state(cx, |window, cx| Editor::new("editable", window, cx)); - - div() - .flex() - .flex_col() - .size_full() - .bg(rgb(0xf0f0f0)) - .p(px(24.)) - .gap(px(24.)) - .child( - section("Inputs — from a String (cursor stays internal)") - .child(Input::new(name).width(px(320.))) - .child( - Input::new(email) - .width(px(320.)) - .color(hsla(0., 0., 0.3, 1.)), - ), - ) - .child( - section("Input — from an Editor (read its cursor beside it)").child( - div() - .flex() - .items_center() - .gap(px(12.)) - .child(Input::editor(owned.clone()).width(px(320.))) - .child(CursorReadout::new(owned)), - ), - ) - .child( - section("Text areas — from a String, or from an Editor") - .child(TextArea::new(bio, 3)) - .child( - div() - .flex() - .items_start() - .gap(px(12.)) - .child(TextArea::editor(notes.clone(), 3).color(hsla( - 250. / 360., - 0.7, - 0.4, - 1., - ))) - .child(CursorReadout::new(notes)), - ), - ) - } -} - -/// A labeled vertical section. -fn section(title: &str) -> Div { - div().flex().flex_col().gap(px(8.)).child( - div() - .text_sm() - .text_color(hsla(0., 0., 0.3, 1.)) - .child(SharedString::from(title.to_string())), - ) -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let bounds = Bounds::centered(None, size(px(560.0), px(480.0)), cx); - cx.bind_keys([ - KeyBinding::new("backspace", Backspace, None), - KeyBinding::new("delete", Delete, None), - KeyBinding::new("left", Left, None), - KeyBinding::new("right", Right, None), - KeyBinding::new("home", Home, None), - KeyBinding::new("end", End, None), - KeyBinding::new("enter", Enter, None), - KeyBinding::new("cmd-q", Quit, None), - ]); - - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| ViewExample::new()), - ) - .unwrap(); - - cx.on_action(|_: &Quit, cx| cx.quit()); - cx.activate(true); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/window.rs b/crates/gpui_pre/examples/window.rs deleted file mode 100644 index 51dbe72..0000000 --- a/crates/gpui_pre/examples/window.rs +++ /dev/null @@ -1,355 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - App, Bounds, Context, KeyBinding, PromptButton, PromptLevel, Window, WindowBounds, WindowKind, - WindowOptions, actions, div, prelude::*, px, rgb, size, -}; -use gpui_platform::application; - -struct SubWindow { - custom_titlebar: bool, - is_dialog: bool, -} - -fn button(text: &str, on_click: impl Fn(&mut Window, &mut App) + 'static) -> impl IntoElement { - div() - .id(text.to_string()) - .flex_none() - .px_2() - .bg(rgb(0xf7f7f7)) - .active(|this| this.opacity(0.85)) - .border_1() - .border_color(rgb(0xe0e0e0)) - .rounded_sm() - .cursor_pointer() - .child(text.to_string()) - .on_click(move |_, window, cx| on_click(window, cx)) -} - -impl Render for SubWindow { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let window_bounds = - WindowBounds::Windowed(Bounds::centered(None, size(px(250.0), px(200.0)), cx)); - - div() - .flex() - .flex_col() - .bg(rgb(0xffffff)) - .size_full() - .gap_2() - .when(self.custom_titlebar, |cx| { - cx.child( - div() - .flex() - .h(px(32.)) - .px_4() - .bg(gpui::blue()) - .text_color(gpui::white()) - .w_full() - .child( - div() - .flex() - .items_center() - .justify_center() - .size_full() - .child("Custom Titlebar"), - ), - ) - }) - .child( - div() - .p_8() - .flex() - .flex_col() - .gap_2() - .child("SubWindow") - .when(self.is_dialog, |div| { - div.child(button("Open Nested Dialog", move |_, cx| { - cx.open_window( - WindowOptions { - window_bounds: Some(window_bounds), - kind: WindowKind::Dialog, - ..Default::default() - }, - |_, cx| { - cx.new(|_| SubWindow { - custom_titlebar: false, - is_dialog: true, - }) - }, - ) - .unwrap(); - })) - }) - .child(button("Close", |window, _| { - window.remove_window(); - })), - ) - } -} - -struct WindowDemo {} - -impl Render for WindowDemo { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let window_bounds = - WindowBounds::Windowed(Bounds::centered(None, size(px(300.0), px(300.0)), cx)); - - div() - .p_4() - .flex() - .flex_wrap() - .bg(rgb(0xffffff)) - .size_full() - .justify_center() - .content_center() - .gap_2() - .child(button("Normal", move |_, cx| { - cx.open_window( - WindowOptions { - window_bounds: Some(window_bounds), - ..Default::default() - }, - |_, cx| { - cx.new(|_| SubWindow { - custom_titlebar: false, - is_dialog: false, - }) - }, - ) - .unwrap(); - })) - .child(button("Popup", move |_, cx| { - cx.open_window( - WindowOptions { - window_bounds: Some(window_bounds), - kind: WindowKind::PopUp, - ..Default::default() - }, - |_, cx| { - cx.new(|_| SubWindow { - custom_titlebar: false, - is_dialog: false, - }) - }, - ) - .unwrap(); - })) - .child(button("Floating", move |_, cx| { - cx.open_window( - WindowOptions { - window_bounds: Some(window_bounds), - kind: WindowKind::Floating, - ..Default::default() - }, - |_, cx| { - cx.new(|_| SubWindow { - custom_titlebar: false, - is_dialog: false, - }) - }, - ) - .unwrap(); - })) - .child(button("Dialog", move |_, cx| { - cx.open_window( - WindowOptions { - window_bounds: Some(window_bounds), - kind: WindowKind::Dialog, - ..Default::default() - }, - |_, cx| { - cx.new(|_| SubWindow { - custom_titlebar: false, - is_dialog: true, - }) - }, - ) - .unwrap(); - })) - .child(button("Custom Titlebar", move |_, cx| { - cx.open_window( - WindowOptions { - titlebar: None, - window_bounds: Some(window_bounds), - ..Default::default() - }, - |_, cx| { - cx.new(|_| SubWindow { - custom_titlebar: true, - is_dialog: false, - }) - }, - ) - .unwrap(); - })) - .child(button("Invisible", move |_, cx| { - cx.open_window( - WindowOptions { - show: false, - window_bounds: Some(window_bounds), - ..Default::default() - }, - |_, cx| { - cx.new(|_| SubWindow { - custom_titlebar: false, - is_dialog: false, - }) - }, - ) - .unwrap(); - })) - .child(button("Unmovable", move |_, cx| { - cx.open_window( - WindowOptions { - is_movable: false, - titlebar: None, - window_bounds: Some(window_bounds), - ..Default::default() - }, - |_, cx| { - cx.new(|_| SubWindow { - custom_titlebar: false, - is_dialog: false, - }) - }, - ) - .unwrap(); - })) - .child(button("Unresizable", move |_, cx| { - cx.open_window( - WindowOptions { - is_resizable: false, - window_bounds: Some(window_bounds), - ..Default::default() - }, - |_, cx| { - cx.new(|_| SubWindow { - custom_titlebar: false, - is_dialog: false, - }) - }, - ) - .unwrap(); - })) - .child(button("Unminimizable", move |_, cx| { - cx.open_window( - WindowOptions { - is_minimizable: false, - window_bounds: Some(window_bounds), - ..Default::default() - }, - |_, cx| { - cx.new(|_| SubWindow { - custom_titlebar: false, - is_dialog: false, - }) - }, - ) - .unwrap(); - })) - .child(button("Hide Application", |window, cx| { - cx.hide(); - - // Restore the application after 3 seconds - window - .spawn(cx, async move |cx| { - cx.background_executor() - .timer(std::time::Duration::from_secs(3)) - .await; - cx.update(|_, cx| { - cx.activate(false); - }) - }) - .detach(); - })) - .child(button("Resize", |window, _| { - let content_size = window.bounds().size; - window.resize(size(content_size.height, content_size.width)); - })) - .child(button("Prompt", |window, cx| { - let answer = window.prompt( - PromptLevel::Info, - "Are you sure?", - None, - &["OK", "Cancel"], - cx, - ); - - cx.spawn(async move |_| { - if answer.await.unwrap() == 0 { - println!("You have clicked Ok"); - } else { - println!("You have clicked Cancel"); - } - }) - .detach(); - })) - .child(button("Prompt (non-English)", |window, cx| { - let answer = window.prompt( - PromptLevel::Info, - "Are you sure?", - None, - &[PromptButton::ok("确定"), PromptButton::cancel("取消")], - cx, - ); - - cx.spawn(async move |_| { - if answer.await.unwrap() == 0 { - println!("You have clicked Ok"); - } else { - println!("You have clicked Cancel"); - } - }) - .detach(); - })) - } -} - -actions!(window, [Quit]); - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx); - - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |window, cx| { - cx.new(|cx| { - cx.observe_window_bounds(window, move |_, window, _| { - println!("Window bounds changed: {:?}", window.bounds()); - }) - .detach(); - - WindowDemo {} - }) - }, - ) - .unwrap(); - - cx.activate(true); - cx.on_action(|_: &Quit, cx| cx.quit()); - cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/window_movable.rs b/crates/gpui_pre/examples/window_movable.rs deleted file mode 100644 index b5fb260..0000000 --- a/crates/gpui_pre/examples/window_movable.rs +++ /dev/null @@ -1,131 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - App, Bounds, Context, FocusHandle, Window, WindowBounds, WindowOptions, div, prelude::*, px, - rgb, size, -}; -use gpui::{SharedString, TitlebarOptions}; -use gpui_platform::application; - -struct ExampleWindow { - label: SharedString, - focus_handle: FocusHandle, -} - -impl Render for ExampleWindow { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .track_focus(&self.focus_handle) - .flex() - .flex_col() - .gap_3() - .bg(rgb(0x2e2e2e)) - .size_full() - .justify_center() - .items_center() - .p_8() - .text_lg() - .text_color(rgb(0xffffff)) - .child(self.label.clone()) - .child( - div() - .text_sm() - .text_color(rgb(0xb0b0b0)) - .child("Try to drag the titlebar, and check the Window menu."), - ) - } -} - -fn open_test_window( - cx: &mut App, - bounds: Bounds, - label: &str, - is_movable: bool, - appears_transparent: bool, - app_owns_titlebar_drag: bool, -) { - let label = SharedString::from(format!( - "{label}\nis_movable: {is_movable}\n\ - appears_transparent: {appears_transparent}\n\ - app_owns_titlebar_drag: {app_owns_titlebar_drag}" - )); - - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - is_movable, - app_owns_titlebar_drag, - titlebar: Some(TitlebarOptions { - title: Some(label.clone()), - appears_transparent, - ..Default::default() - }), - ..Default::default() - }, - |window, cx| { - cx.new(|cx| { - let focus_handle = cx.focus_handle(); - focus_handle.focus(window, cx); - ExampleWindow { - label, - focus_handle, - } - }) - }, - ) - .unwrap(); -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let window_size = size(px(420.), px(280.0)); - let base = Bounds::centered(None, window_size, cx); - - // (label, is_movable, appears_transparent, app_owns_titlebar_drag, col, row) - let windows = [ - ("Native titlebar, movable", true, false, false, 0.0, 0.0), - ( - "Native titlebar, NOT movable", - false, - false, - false, - 1.0, - 0.0, - ), - ("Custom titlebar, movable", true, true, false, 0.0, 1.0), - ("Custom titlebar, NOT movable", false, true, false, 1.0, 1.0), - ]; - - for (label, is_movable, appears_transparent, app_owns_titlebar_drag, col, row) in windows { - let mut bounds = base; - bounds.origin.x += window_size.width * col; - bounds.origin.y += window_size.height * row; - open_test_window( - cx, - bounds, - label, - is_movable, - appears_transparent, - app_owns_titlebar_drag, - ); - } - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/window_positioning.rs b/crates/gpui_pre/examples/window_positioning.rs deleted file mode 100644 index 22c9b35..0000000 --- a/crates/gpui_pre/examples/window_positioning.rs +++ /dev/null @@ -1,240 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - App, Bounds, Context, DisplayId, Hsla, Pixels, SharedString, Size, Window, - WindowBackgroundAppearance, WindowBounds, WindowKind, WindowOptions, div, point, prelude::*, - px, rgb, -}; -use gpui_platform::application; - -struct WindowContent { - text: SharedString, - bounds: Bounds, - bg: Hsla, -} - -impl Render for WindowContent { - fn render(&mut self, window: &mut Window, _: &mut Context) -> impl IntoElement { - let window_bounds = window.bounds(); - - div() - .flex() - .flex_col() - .bg(self.bg) - .size_full() - .items_center() - .text_color(rgb(0xffffff)) - .child(self.text.clone()) - .child( - div() - .flex() - .flex_col() - .text_sm() - .items_center() - .size_full() - .child(format!( - "origin: {}, {} size: {}, {}", - self.bounds.origin.x, - self.bounds.origin.y, - self.bounds.size.width, - self.bounds.size.height - )) - .child(format!( - "cx.bounds() origin: {}, {} size {}, {}", - window_bounds.origin.x, - window_bounds.origin.y, - window_bounds.size.width, - window_bounds.size.height - )), - ) - } -} - -fn build_window_options(display_id: DisplayId, bounds: Bounds) -> WindowOptions { - WindowOptions { - // Set the bounds of the window in screen coordinates - window_bounds: Some(WindowBounds::Windowed(bounds)), - // Specify the display_id to ensure the window is created on the correct screen - display_id: Some(display_id), - titlebar: None, - window_background: WindowBackgroundAppearance::Transparent, - focus: false, - show: true, - kind: WindowKind::PopUp, - is_movable: false, - app_id: None, - window_min_size: None, - window_decorations: None, - tabbing_identifier: None, - ..Default::default() - } -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - // Create several new windows, positioned in the top right corner of each screen - let size = Size { - width: px(350.), - height: px(75.), - }; - let margin_offset = px(150.); - - for screen in cx.displays() { - let bounds = Bounds { - origin: point(margin_offset, margin_offset), - size, - }; - - cx.open_window(build_window_options(screen.id(), bounds), |_, cx| { - cx.new(|_| WindowContent { - text: format!("Top Left {:?}", screen.id()).into(), - bg: gpui::red(), - bounds, - }) - }) - .unwrap(); - - let bounds = Bounds { - origin: screen.bounds().top_right() - - point(size.width + margin_offset, -margin_offset), - size, - }; - - cx.open_window(build_window_options(screen.id(), bounds), |_, cx| { - cx.new(|_| WindowContent { - text: format!("Top Right {:?}", screen.id()).into(), - bg: gpui::red(), - bounds, - }) - }) - .unwrap(); - - let bounds = Bounds { - origin: screen.bounds().bottom_left() - - point(-margin_offset, size.height + margin_offset), - size, - }; - - cx.open_window(build_window_options(screen.id(), bounds), |_, cx| { - cx.new(|_| WindowContent { - text: format!("Bottom Left {:?}", screen.id()).into(), - bg: gpui::blue(), - bounds, - }) - }) - .unwrap(); - - let bounds = Bounds { - origin: screen.bounds().bottom_right() - - point(size.width + margin_offset, size.height + margin_offset), - size, - }; - - cx.open_window(build_window_options(screen.id(), bounds), |_, cx| { - cx.new(|_| WindowContent { - text: format!("Bottom Right {:?}", screen.id()).into(), - bg: gpui::blue(), - bounds, - }) - }) - .unwrap(); - - let bounds = Bounds { - origin: point(screen.bounds().center().x - size.center().x, margin_offset), - size, - }; - - cx.open_window(build_window_options(screen.id(), bounds), |_, cx| { - cx.new(|_| WindowContent { - text: format!("Top Center {:?}", screen.id()).into(), - bg: gpui::black(), - bounds, - }) - }) - .unwrap(); - - let bounds = Bounds { - origin: point(margin_offset, screen.bounds().center().y - size.center().y), - size, - }; - - cx.open_window(build_window_options(screen.id(), bounds), |_, cx| { - cx.new(|_| WindowContent { - text: format!("Left Center {:?}", screen.id()).into(), - bg: gpui::black(), - bounds, - }) - }) - .unwrap(); - - let bounds = Bounds { - origin: point( - screen.bounds().center().x - size.center().x, - screen.bounds().center().y - size.center().y, - ), - size, - }; - - cx.open_window(build_window_options(screen.id(), bounds), |_, cx| { - cx.new(|_| WindowContent { - text: format!("Center {:?}", screen.id()).into(), - bg: gpui::black(), - bounds, - }) - }) - .unwrap(); - - let bounds = Bounds { - origin: point( - screen.bounds().size.width - size.width - margin_offset, - screen.bounds().center().y - size.center().y, - ), - size, - }; - - cx.open_window(build_window_options(screen.id(), bounds), |_, cx| { - cx.new(|_| WindowContent { - text: format!("Right Center {:?}", screen.id()).into(), - bg: gpui::black(), - bounds, - }) - }) - .unwrap(); - - let bounds = Bounds { - origin: point( - screen.bounds().center().x - size.center().x, - screen.bounds().size.height - size.height - margin_offset, - ), - size, - }; - - cx.open_window(build_window_options(screen.id(), bounds), |_, cx| { - cx.new(|_| WindowContent { - text: format!("Bottom Center {:?}", screen.id()).into(), - bg: gpui::black(), - bounds, - }) - }) - .unwrap(); - } - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/examples/window_shadow.rs b/crates/gpui_pre/examples/window_shadow.rs deleted file mode 100644 index fba5e91..0000000 --- a/crates/gpui_pre/examples/window_shadow.rs +++ /dev/null @@ -1,252 +0,0 @@ -#![cfg_attr(target_family = "wasm", no_main)] - -#[path = "example_support/fonts.rs"] -mod example_support; - -use gpui::{ - App, Bounds, Context, CursorStyle, Decorations, HitboxBehavior, Hsla, MouseButton, Pixels, - Point, ResizeEdge, Size, Window, WindowBackgroundAppearance, WindowBounds, WindowDecorations, - WindowOptions, black, canvas, div, green, point, prelude::*, px, rgb, size, transparent_black, - white, -}; -use gpui_platform::application; - -struct WindowShadow {} - -// Things to do: -// 1. We need a way of calculating which edge or corner the mouse is on, -// and then dispatch on that -// 2. We need to improve the shadow rendering significantly -// 3. We need to implement the techniques in here in Zed - -impl Render for WindowShadow { - fn render(&mut self, window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let decorations = window.window_decorations(); - let rounding = px(10.0); - let shadow_size = px(10.0); - let border_size = px(1.0); - let grey = rgb(0x808080); - window.set_client_inset(shadow_size); - - div() - .id("window-backdrop") - .bg(transparent_black()) - .map(|div| match decorations { - Decorations::Server => div, - Decorations::Client { tiling, .. } => div - .bg(gpui::transparent_black()) - .child( - canvas( - |_bounds, window, _cx| { - window.insert_hitbox( - Bounds::new( - point(px(0.0), px(0.0)), - window.window_bounds().get_bounds().size, - ), - HitboxBehavior::Normal, - ) - }, - move |_bounds, hitbox, window, _cx| { - let mouse = window.mouse_position(); - let size = window.window_bounds().get_bounds().size; - let Some(edge) = resize_edge(mouse, shadow_size, size) else { - return; - }; - window.set_cursor_style( - match edge { - ResizeEdge::Top | ResizeEdge::Bottom => { - CursorStyle::ResizeUpDown - } - ResizeEdge::Left | ResizeEdge::Right => { - CursorStyle::ResizeLeftRight - } - ResizeEdge::TopLeft | ResizeEdge::BottomRight => { - CursorStyle::ResizeUpLeftDownRight - } - ResizeEdge::TopRight | ResizeEdge::BottomLeft => { - CursorStyle::ResizeUpRightDownLeft - } - }, - &hitbox, - ); - }, - ) - .size_full() - .absolute(), - ) - .when(!(tiling.top || tiling.right), |div| { - div.rounded_tr(rounding) - }) - .when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding)) - .when(!tiling.top, |div| div.pt(shadow_size)) - .when(!tiling.bottom, |div| div.pb(shadow_size)) - .when(!tiling.left, |div| div.pl(shadow_size)) - .when(!tiling.right, |div| div.pr(shadow_size)) - .on_mouse_move(|_e, window, _cx| window.refresh()) - .on_mouse_down(MouseButton::Left, move |e, window, _cx| { - let size = window.window_bounds().get_bounds().size; - let pos = e.position; - - match resize_edge(pos, shadow_size, size) { - Some(edge) => window.start_window_resize(edge), - None => window.start_window_move(), - }; - }), - }) - .size_full() - .child( - div() - .cursor(CursorStyle::Arrow) - .map(|div| match decorations { - Decorations::Server => div, - Decorations::Client { tiling } => div - .border_color(grey) - .when(!(tiling.top || tiling.right), |div| { - div.rounded_tr(rounding) - }) - .when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding)) - .when(!tiling.top, |div| div.border_t(border_size)) - .when(!tiling.bottom, |div| div.border_b(border_size)) - .when(!tiling.left, |div| div.border_l(border_size)) - .when(!tiling.right, |div| div.border_r(border_size)) - .when(!tiling.is_tiled(), |div| { - div.shadow(vec![ - gpui::BoxShadow::new( - px(0.), - px(0.), - Hsla { - h: 0., - s: 0., - l: 0., - a: 0.4, - }, - ) - .blur_radius(shadow_size / 2.), - ]) - }), - }) - .on_mouse_move(|_e, _, cx| { - cx.stop_propagation(); - }) - .bg(gpui::rgb(0xCCCCFF)) - .size_full() - .flex() - .flex_col() - .justify_around() - .child( - div().w_full().flex().flex_row().justify_around().child( - div() - .flex() - .bg(white()) - .size(px(300.0)) - .justify_center() - .items_center() - .shadow_lg() - .border_1() - .border_color(rgb(0x0000ff)) - .text_xl() - .text_color(rgb(0xffffff)) - .child( - div() - .id("hello") - .w(px(200.0)) - .h(px(100.0)) - .bg(green()) - .shadow(vec![ - gpui::BoxShadow::new( - px(0.), - px(0.), - Hsla { - h: 0., - s: 0., - l: 0., - a: 1.0, - }, - ) - .blur_radius(px(20.0)), - ]) - .map(|div| match decorations { - Decorations::Server => div, - Decorations::Client { .. } => div - .on_mouse_down( - MouseButton::Left, - |_e, window, _| { - window.start_window_move(); - }, - ) - .on_click(|e, window, _| { - if e.is_right_click() { - window.show_window_menu(e.position()); - } - }) - .text_color(black()) - .child("this is the custom titlebar"), - }), - ), - ), - ), - ) - } -} - -fn resize_edge(pos: Point, shadow_size: Pixels, size: Size) -> Option { - let edge = if pos.y < shadow_size && pos.x < shadow_size { - ResizeEdge::TopLeft - } else if pos.y < shadow_size && pos.x > size.width - shadow_size { - ResizeEdge::TopRight - } else if pos.y < shadow_size { - ResizeEdge::Top - } else if pos.y > size.height - shadow_size && pos.x < shadow_size { - ResizeEdge::BottomLeft - } else if pos.y > size.height - shadow_size && pos.x > size.width - shadow_size { - ResizeEdge::BottomRight - } else if pos.y > size.height - shadow_size { - ResizeEdge::Bottom - } else if pos.x < shadow_size { - ResizeEdge::Left - } else if pos.x > size.width - shadow_size { - ResizeEdge::Right - } else { - return None; - }; - Some(edge) -} - -fn run_example() { - application().run(|cx: &mut App| { - if !example_support::load_fonts(cx) { - return; - } - let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - window_background: WindowBackgroundAppearance::Opaque, - window_decorations: Some(WindowDecorations::Client), - ..Default::default() - }, - |window, cx| { - cx.new(|cx| { - cx.observe_window_appearance(window, |_, window, _| { - window.refresh(); - }) - .detach(); - WindowShadow {} - }) - }, - ) - .unwrap(); - }); -} - -#[cfg(not(target_family = "wasm"))] -fn main() { - run_example(); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(start)] -pub fn start() { - gpui_platform::web_init(); - run_example(); -} diff --git a/crates/gpui_pre/resources/windows/gpui.manifest.xml b/crates/gpui_pre/resources/windows/gpui.manifest.xml deleted file mode 100644 index d11e1a4..0000000 --- a/crates/gpui_pre/resources/windows/gpui.manifest.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - true/pm - PerMonitorV2 - SegmentHeap - - - - - - - - diff --git a/crates/gpui_pre/resources/windows/gpui.rc b/crates/gpui_pre/resources/windows/gpui.rc deleted file mode 100644 index a6f3787..0000000 --- a/crates/gpui_pre/resources/windows/gpui.rc +++ /dev/null @@ -1,2 +0,0 @@ -#define RT_MANIFEST 24 -1 RT_MANIFEST "resources/windows/gpui.manifest.xml" \ No newline at end of file diff --git a/crates/gpui_pre/src/_accessibility.rs b/crates/gpui_pre/src/_accessibility.rs deleted file mode 100644 index af14b63..0000000 --- a/crates/gpui_pre/src/_accessibility.rs +++ /dev/null @@ -1,295 +0,0 @@ -//! # Accessibility in GPUI -//! -//! "Accessibility" refers to the ability of your application to be used by all -//! users, regardless of disability status. There are many aspects, all important, including: -//! - Ensuring sufficient text contrast. -//! - Providing a mechanism to disable animations. -//! - Providing a mechanism to increase text sizes. -//! - etc. -//! -//! This guide is focused on **programmatic accessibility**. This allows -//! assistive technology, such as screen readers or Braille displays, to inspect -//! and interact with your app. Docs for contributors working on accessibility -//! support can be found in the `a11y` module's doc comment. -//! -//! GPUI integrates with [AccessKit] to provide programmatic accessibility -//! features (referred to as simply "accessibility" for the rest of this guide). -//! -//! A minimal example can be found in the `examples/a11y` directory. -//! -//! ## Background -//! -//! Accessibility support is based on two key capabilities: -//! - Exposing information about the current UI state to assistive technology. -//! - Responding to actions requested by assistive technology. -//! -//! For example, a screen reader might want to announce to the user that a new -//! button has appeared. The user may then want to use a voice control program -//! to press that button. -//! -//! ### IDs in GPUI - [`ElementId`] and [`GlobalElementId`] -//! -//! In GPUI, each [`Element`] can have an [`id`][Element::id]: -//! ```rust -//! # use gpui::*; -//! let div_with_id = div().id("my-id").child(text!("hello")); -//! -//! // IDs are optional -//! let div_without_id = div().child(text!("hello")); -//! ``` -//! -//! [`Element`]s with IDs are also assigned a [`GlobalElementId`]. This global -//! ID is formed by composing all the non-`None` IDs of its ancestors. For -//! example: -//! ```rust -//! # use gpui::*; -//! let inner = div().id("inner-id"); -//! let middle = div().child(inner); // no ID -//! let outer = div().id("outer-id").child(middle); -//! ``` -//! In this example, `inner`s global ID is (roughly speaking) `["outer-id", -//! "inner-id"]`. -//! -//! Since `middle` doesn't have an ID itself, it has no global ID. -//! -//! [`GlobalElementId`]s should be unique per-frame. Duplicate global IDs in the -//! same frame will likely cause bugs. -//! -//! ### IDs and accessibility -//! -//! When GPUI renders a frame, it walks your UI tree, and finds nodes with -//! global IDs, and informs assistive technology about this node. -//! -//! In order for nodes to be reported, they must also have a non-`None` -//! [`role`][Element::a11y_role]. This is used to inform assistive technology -//! what *sort* of node it is (button, label, table, etc.). You can use -//! [`div().id(...).role()`][StatefulInteractiveElement::role] to set the role. -//! -//! Nodes with the same global ID *across frames* are considered to be "the -//! same" node. For example: -//! ```rust -//! # use gpui::*; -//! // The UI in frame 1 -//! let frame_1 = div() -//! .id("parent") -//! .role(Role::Button) -//! .child( -//! div() -//! .id("id-1") -//! .role(Role::Label) -//! .child(text!("hello")) -//! ); -//! -//! // The UI on the next frame -//! let frame_2 = div() -//! .id("parent") -//! .role(Role::Button) -//! .child( -//! div() -//! .id("id-2") // <- different ID -//! .role(Role::Label) -//! .child(text!("hello")) -//! ); -//! ``` -//! Logically, the UI has not changed. But the screen reader has no way of -//! knowing that both child [`div`]s are "the same". So assistive technology -//! will interpret this as one node being removed, and another node being added. -//! This can be very disorienting for users, since announcements typically only -//! happen when something has *meaningfully* changed. -//! -//! In other words, by controlling the ID of an element, you can control whether -//! a change to a UI element is considered meaningful. You can also control -//! whether elements are reported to assistive technology *at all* by setting -//! the [`role`][Element::a11y_role], since nodes with no role are not reported. -//! -//! #### IDs and text -//! -//! Special care must be taken when dealing with text. -//! -//! GPUI provides the [`text!`] macro, which wraps strings in the [`Text`] type, -//! but automatically derives an ID. Usually, this is what you want. However, -//! the way it generates its ID is subtle and perhaps surprising. -//! -//! The ID of an invocation of the [`text!`] macro is derived from the -//! **location in the source code of that invocation**. For example: -//! -//! ```rust -//! # use gpui::*; -//! let a = text!("a"); -//! let b = text!("b"); -//! -//! // Different source locations, different IDs -//! assert_ne!(a.id(), b.id()); -//! -//! // However: -//! -//! fn make_text(s: &str) -> Text { text!(s) } -//! -//! let a = make_text("a"); -//! let b = make_text("b"); -//! -//! // Both `a` and `b` are produced by the same `text!` invocation, so the IDs -//! // are the same -//! assert_eq!(a.id(), b.id()); -//! ``` -//! This can produce surprising behaviour. For example, this footgun: -//! ```rust -//! # use gpui::*; -//! let todos = vec!["eat lunch", "drink water", "go to gym"]; -//! let todo_divs = todos.into_iter().map(|todo| { -//! text!(todo) -//! }); -//! -//! div() -//! .id("todo-list") -//! .role(Role::Document) -//! .children(todo_divs); // ERROR: multiple nodes with the same global ID -//! ``` -//! -//! Here, when we map the iterator, since we have only written [`text!`] once, -//! there is only one ID. And since they have the same ancestors and the same -//! ID, they will have the same global ID. In release builds, this will mean -//! some nodes get silently dropped! -//! -//! To fix this, you can set an ID: -//! ```rust -//! # use gpui::*; -//! let todos = vec!["eat lunch", "drink water", "go to gym"]; -//! let todo_divs = todos.into_iter().enumerate().map(|(index, todo)| { -//! text!(todo).with_id(index) // OR `text(id = index, todo)` -//! }); -//! -//! div() -//! .id("todo-list") -//! .role(Role::Document) -//! .children(todo_divs); -//! ``` -//! Another possible solution is to wrap the [`text!`] in another node that -//! *does* have a unique global ID. For example: -//! ```rust -//! # use gpui::*; -//! let todos = vec!["eat lunch", "drink water", "go to gym"]; -//! let todo_divs = todos.into_iter().enumerate().map(|(index, todo)| { -//! div().id(index).child(text!(todo)) -//! }); -//! -//! div() -//! .id("todo-list") -//! .role(Role::Document) -//! .children(todo_divs); -//! ``` -//! Since the AccessKit [`NodeId`][accesskit::NodeId] is derived from the global -//! ID, and the global ID takes into account the IDs of all ancestors, this -//! works too. -//! -//! Occasionally, you will need to create a [`Text`] element with *no* ID. You -//! can achieve this with [`Text::new_inaccessible`]. If you are creating a -//! custom UI component (e.g. a button), you may want this so that you can set a -//! label property on a parent [`div`] without duplicating the text in the -//! accessibility tree. -//! -//! ### Handling actions -//! -//! Assistive technology can dispatch actions to the UI. While many users of -//! assistive technology use traditional input devices (e.g. a keyboard), some -//! use more specialized systems. For example, users with limited mobility may -//! use voice control to interact with your app. -//! -//! When a user dispatches an action, it is dispatched *to a specific node*. It -//! is your responsibility to tell the UI elements how they should respond when -//! a request comes in. -//! -//! Note, these actions are **totally unrelated** to GPUI's [`Action`] trait. -//! AccessKit exposes [`accesskit::Action`]. In GPUI, this is re-exported as -//! [`AccessibleAction`]. -//! -//! To respond to an accessible action, use -//! [`div().on_a11y_action()`][InteractiveElement::on_a11y_action]: -//! ```rust,ignore -//! div() -//! .id("my-slider") -//! .role(Role::Slider) -//! .on_a11y_action(AccessibleAction::Increment, |_extra, _window, _cx| { -//! position += 1; -//! cx.notify(); -//! }) -//! .child(my_cool_slider()); -//! ``` -//! -//! Note that some common actions are automatically registered. For example, -//! [`.on_click()`][StatefulInteractiveElement::on_click] adds an -//! [`AccessibleAction::Click`] handler that calls the click handler. -//! -//! ## Synthetic children -//! -//! Sometimes, a custom [`Element`] may want to appear as if it is really made -//! of multiple nodes. For example, a totally hypothetical custom text editor -//! element may want to have [`Role::TextInput`], while presenting children -//! consisting of [`Role::TextRun`]s. -//! -//! This is possible using [`Element::a11y_synthetic_children`]. For example: -//! ```rust,ignore -//! # use gpui::*; -//! impl Element for MyCustomTextField { -//! -//! // ... -//! -//! fn a11y_role(&self) -> Option { -//! Some(Role::TextInput) -//! } -//! -//! fn a11y_synthetic_children( -//! &mut self, -//! _prepaint: &mut Self::PrepaintState, -//! builder: &mut A11ySubtreeBuilder, -//! ) { -//! // Create the synthetic child node -//! let mut run = accesskit::Node::new(Role::TextRun); -//! run.set_value(self.text.clone()); -//! run.set_character_lengths( -//! self.text.chars().map(|c| c.len_utf8() as u8).collect::>(), -//! ); -//! -//! // Insert it as a child of `MyCustomTextField` -//! let run_id = builder.synthetic_node_id(0); -//! builder.push_child(run_id, run); -//! -//! // You can also mutate the parent (i.e. the `MyCustomTextField`) -//! let caret = accesskit::TextPosition { -//! node: run_id, -//! character_index: self.cursor, -//! }; -//! builder.parent_node().set_text_selection(accesskit::TextSelection { -//! anchor: caret, -//! focus: caret, -//! }); -//! } -//! } -//! ``` -//! -//! Notably, synthetic children are added *after* an element is -//! [prepainted][Element::prepaint], so prepaint state can be used (for example, -//! to determine what is visible on screen). -//! -//! ## Further reading -//! -//! Designing high-quality accessible interfaces can be challenging, in the same -//! way that designing high-quality traditional interfaces can be. The -//! following pages have useful information: -//! -//! - [AccessKit]: The cross-platform accessibility toolkit GPUI uses -//! internally. -//! - [MDN WAI-ARIA basics][mdn-aria]: Introduction to roles, properties, and -//! states. -//! - [ARIA Authoring Practices Guide][apg]: W3C patterns for accessible -//! widgets. -//! -//! Note that, while GPUI mimics web APIs, it doesn't necessarily behave -//! *exactly* as a web browser would with the same attributes. -//! -//! [AccessKit]: https://accesskit.dev/ -//! [mdn-aria]: https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Accessibility/WAI-ARIA_basics -//! [apg]: https://www.w3.org/WAI/ARIA/apg/ - -#[cfg(doc)] -use crate::*; // so I don't have to qualify every type :) diff --git a/crates/gpui_pre/src/_ownership_and_data_flow.rs b/crates/gpui_pre/src/_ownership_and_data_flow.rs deleted file mode 100644 index 68699cc..0000000 --- a/crates/gpui_pre/src/_ownership_and_data_flow.rs +++ /dev/null @@ -1,140 +0,0 @@ -//! In GPUI, every model or view in the application is actually owned by a single top-level object called the `App`. When a new entity or view is created (referred to collectively as _entities_), the application is given ownership of their state to enable their participation in a variety of app services and interaction with other entities. -//! -//! To illustrate, consider the trivial app below. We start the app by calling `run` with a callback, which is passed a reference to the `App` that owns all the state for the application. This `App` is our gateway to all application-level services, such as opening windows, presenting dialogs, etc. It also has an `insert_entity` method, which is called below to create an entity and give ownership of it to the application. -//! -//! ```no_run -//! # use gpui::{App, AppContext, Application, Entity}; -//! # struct Counter { -//! # count: usize, -//! # } -//! gpui_platform::application().run(|cx: &mut App| { -//! let _counter: Entity = cx.new(|_cx| Counter { count: 0 }); -//! // ... -//! }); -//! ``` -//! -//! The call to `new_entity` returns an _entity handle_, which carries a type parameter based on the type of object it references. By itself, this `Entity` handle doesn't provide access to the entity's state. It's merely an inert identifier plus a compile-time type tag, and it maintains a reference counted pointer to the underlying `Counter` object that is owned by the app. -//! -//! Much like an `Rc` from the Rust standard library, this reference count is incremented when the handle is cloned and decremented when it is dropped to enable shared ownership over the underlying model, but unlike an `Rc` it only provides access to the model's state when a reference to an `App` is available. The handle doesn't truly _own_ the state, but it can be used to access the state from its true owner, the `App`. Stripping away some of the setup code for brevity: -//! -//! ```no_run -//! # use gpui::{App, AppContext, Application, Context, Entity}; -//! # struct Counter { -//! # count: usize, -//! # } -//! gpui_platform::application().run(|cx: &mut App| { -//! let counter: Entity = cx.new(|_cx| Counter { count: 0 }); -//! // Call `update` to access the model's state. -//! counter.update(cx, |counter: &mut Counter, _cx: &mut Context| { -//! counter.count += 1; -//! }); -//! }); -//! ``` -//! -//! To update the counter, we call `update` on the handle, passing the context reference and a callback. The callback is yielded a mutable reference to the counter, which can be used to manipulate state. -//! -//! The callback is also provided a second `Context` reference. This reference is similar to the `App` reference provided to the `run` callback. A `Context` is actually a wrapper around the `App`, including some additional data to indicate which particular entity it is tied to; in this case the counter. -//! -//! In addition to the application-level services provided by `App`, a `Context` provides access to entity-level services. For example, it can be used it to inform observers of this entity that its state has changed. Let's add that to our example, by calling `cx.notify()`. -//! -//! ```no_run -//! # use gpui::{App, AppContext, Application, Entity}; -//! # struct Counter { -//! # count: usize, -//! # } -//! gpui_platform::application().run(|cx: &mut App| { -//! let counter: Entity = cx.new(|_cx| Counter { count: 0 }); -//! counter.update(cx, |counter, cx| { -//! counter.count += 1; -//! cx.notify(); // Notify observers -//! }); -//! }); -//! ``` -//! -//! Next, these notifications need to be observed and reacted to. Before updating the counter, we'll construct a second counter that observes it. Whenever the first counter changes, twice its count is assigned to the second counter. Note how `observe` is called on the `Context` belonging to our second counter to arrange for it to be notified whenever the first counter notifies. The call to `observe` returns a `Subscription`, which is `detach`ed to preserve this behavior for as long as both counters exist. We could also store this subscription and drop it at a time of our choosing to cancel this behavior. -//! -//! The `observe` callback is passed a mutable reference to the observer and a _handle_ to the observed counter, whose state we access with the `read` method. -//! -//! ```no_run -//! # use gpui::{App, AppContext, Application, Entity, prelude::*}; -//! # struct Counter { -//! # count: usize, -//! # } -//! gpui_platform::application().run(|cx: &mut App| { -//! let first_counter: Entity = cx.new(|_cx| Counter { count: 0 }); -//! -//! let second_counter = cx.new(|cx: &mut Context| { -//! // Note we can set up the callback before the Counter is even created! -//! cx.observe( -//! &first_counter, -//! |second: &mut Counter, first: Entity, cx| { -//! second.count = first.read(cx).count * 2; -//! }, -//! ) -//! .detach(); -//! -//! Counter { count: 0 } -//! }); -//! -//! first_counter.update(cx, |counter, cx| { -//! counter.count += 1; -//! cx.notify(); -//! }); -//! -//! assert_eq!(second_counter.read(cx).count, 2); -//! }); -//! ``` -//! -//! After updating the first counter, it can be noted that the observing counter's state is maintained according to our subscription. -//! -//! In addition to `observe` and `notify`, which indicate that an entity's state has changed, GPUI also offers `subscribe` and `emit`, which enables entities to emit typed events. To opt into this system, the emitting object must implement the `EventEmitter` trait. -//! -//! Let's introduce a new event type called `CounterChangeEvent`, then indicate that `Counter` can emit this type of event: -//! -//! ```no_run -//! use gpui::EventEmitter; -//! # struct Counter { -//! # count: usize, -//! # } -//! struct CounterChangeEvent { -//! increment: usize, -//! } -//! -//! impl EventEmitter for Counter {} -//! ``` -//! -//! Next, the example should be updated, replacing the observation with a subscription. Whenever the counter is incremented, a `Change` event is emitted to indicate the magnitude of the increase. -//! -//! ```no_run -//! # use gpui::{App, AppContext, Application, Context, Entity, EventEmitter}; -//! # struct Counter { -//! # count: usize, -//! # } -//! # struct CounterChangeEvent { -//! # increment: usize, -//! # } -//! # impl EventEmitter for Counter {} -//! gpui_platform::application().run(|cx: &mut App| { -//! let first_counter: Entity = cx.new(|_cx| Counter { count: 0 }); -//! -//! let second_counter = cx.new(|cx: &mut Context| { -//! // Note we can set up the callback before the Counter is even created! -//! cx.subscribe(&first_counter, |second: &mut Counter, _first: Entity, event, _cx| { -//! second.count += event.increment * 2; -//! }) -//! .detach(); -//! -//! Counter { -//! count: first_counter.read(cx).count * 2, -//! } -//! }); -//! -//! first_counter.update(cx, |first, cx| { -//! first.count += 2; -//! cx.emit(CounterChangeEvent { increment: 2 }); -//! cx.notify(); -//! }); -//! -//! assert_eq!(second_counter.read(cx).count, 4); -//! }); -//! ``` diff --git a/crates/gpui_pre/src/action.rs b/crates/gpui_pre/src/action.rs deleted file mode 100644 index 2d2473d..0000000 --- a/crates/gpui_pre/src/action.rs +++ /dev/null @@ -1,459 +0,0 @@ -// Modified for gpui-pre (snapshot of zed@5b055fa): the `actions!` derive paths are crate-relative. -use anyhow::{Context as _, Result}; -use collections::{HashMap, TypeIdHashMap}; -pub use gpui_macros::Action; -pub use no_action::{NoAction, Unbind, is_no_action, is_unbind}; -use serde_json::json; -use std::{ - any::{Any, TypeId}, - fmt::Display, -}; - -/// Defines and registers unit structs that can be used as actions. For more complex data types, derive `Action`. -/// -/// For example: -/// -/// ``` -/// use gpui::actions; -/// actions!(editor, [MoveUp, MoveDown, MoveLeft, MoveRight, Newline]); -/// ``` -/// -/// This will create actions with names like `editor::MoveUp`, `editor::MoveDown`, etc. -/// -/// The namespace argument `editor` can also be omitted, though it is required for Zed actions. -#[macro_export] -macro_rules! actions { - ($namespace:path, [ $( $(#[$attr:meta])* $name:ident),* $(,)? ]) => { - $( - #[derive(::std::clone::Clone, ::std::cmp::PartialEq, ::std::default::Default, ::std::fmt::Debug, $crate::Action)] - #[action(namespace = $namespace)] - $(#[$attr])* - pub struct $name; - )* - }; - ([ $( $(#[$attr:meta])* $name:ident),* $(,)? ]) => { - $( - #[derive(::std::clone::Clone, ::std::cmp::PartialEq, ::std::default::Default, ::std::fmt::Debug, $crate::Action)] - $(#[$attr])* - pub struct $name; - )* - }; -} - -/// Actions are used to implement keyboard-driven UI. When you declare an action, you can bind keys -/// to the action in the keymap and listeners for that action in the element tree. -/// -/// To declare a list of simple actions, you can use the actions! macro, which defines a simple unit -/// struct action for each listed action name in the given namespace. -/// -/// ``` -/// use gpui::actions; -/// actions!(editor, [MoveUp, MoveDown, MoveLeft, MoveRight, Newline]); -/// ``` -/// -/// Registering the actions with the same name will result in a panic during `App` creation. -/// -/// # Derive Macro -/// -/// More complex data types can also be actions, by using the derive macro for `Action`: -/// -/// ``` -/// use gpui::Action; -/// #[derive(Clone, PartialEq, serde::Deserialize, schemars::JsonSchema, Action)] -/// #[action(namespace = editor)] -/// pub struct SelectNext { -/// pub replace_newest: bool, -/// } -/// ``` -/// -/// The derive macro for `Action` requires that the type implement `Clone` and `PartialEq`. It also -/// requires `serde::Deserialize` and `schemars::JsonSchema` unless `#[action(no_json)]` is -/// specified. In Zed these trait impls are used to load keymaps from JSON. -/// -/// Multiple arguments separated by commas may be specified in `#[action(...)]`: -/// -/// - `namespace = some_namespace` sets the namespace. In Zed this is required. -/// -/// - `name = "ActionName"` overrides the action's name. This must not contain `::`. -/// -/// - `no_json` causes the `build` method to always error and `action_json_schema` to return `None`, -/// and allows actions not implement `serde::Serialize` and `schemars::JsonSchema`. -/// -/// - `no_register` skips registering the action. This is useful for implementing the `Action` trait -/// while not supporting invocation by name or JSON deserialization. -/// -/// - `deprecated_aliases = ["editor::SomeAction"]` specifies deprecated old names for the action. -/// These action names should *not* correspond to any actions that are registered. These old names -/// can then still be used to refer to invoke this action. In Zed, the keymap JSON schema will -/// accept these old names and provide warnings. -/// -/// - `deprecated = "Message about why this action is deprecation"` specifies a deprecation message. -/// In Zed, the keymap JSON schema will cause this to be displayed as a warning. -/// -/// # Manual Implementation -/// -/// If you want to control the behavior of the action trait manually, you can use the lower-level -/// `#[register_action]` macro, which only generates the code needed to register your action before -/// `main`. -/// -/// ``` -/// use gpui::{SharedString, register_action}; -/// #[derive(Clone, PartialEq, Eq, serde::Deserialize, schemars::JsonSchema)] -/// pub struct Paste { -/// pub content: SharedString, -/// } -/// -/// impl gpui::Action for Paste { -/// # fn boxed_clone(&self) -> Box { unimplemented!()} -/// # fn partial_eq(&self, other: &dyn gpui::Action) -> bool { unimplemented!() } -/// # fn name(&self) -> &'static str { "Paste" } -/// # fn name_for_type() -> &'static str { "Paste" } -/// # fn build(value: serde_json::Value) -> anyhow::Result> { -/// # unimplemented!() -/// # } -/// } -/// -/// register_action!(Paste); -/// ``` -pub trait Action: Any + Send { - /// Clone the action into a new box - fn boxed_clone(&self) -> Box; - - /// Do a partial equality check on this action and the other - fn partial_eq(&self, action: &dyn Action) -> bool; - - /// Get the name of this action, for displaying in UI - fn name(&self) -> &'static str; - - /// Get the name of this action type (static) - fn name_for_type() -> &'static str - where - Self: Sized; - - /// Build this action from a JSON value. This is used to construct actions from the keymap. - /// A value of `{}` will be passed for actions that don't have any parameters. - fn build(value: serde_json::Value) -> Result> - where - Self: Sized; - - /// Optional JSON schema for the action's input data. - fn action_json_schema(_: &mut schemars::SchemaGenerator) -> Option - where - Self: Sized, - { - None - } - - /// A list of alternate, deprecated names for this action. These names can still be used to - /// invoke the action. In Zed, the keymap JSON schema will accept these old names and provide - /// warnings. - fn deprecated_aliases() -> &'static [&'static str] - where - Self: Sized, - { - &[] - } - - /// Returns the deprecation message for this action, if any. In Zed, the keymap JSON schema will - /// cause this to be displayed as a warning. - fn deprecation_message() -> Option<&'static str> - where - Self: Sized, - { - None - } - - /// The documentation for this action, if any. When using the derive macro for actions - /// this will be automatically generated from the doc comments on the action struct. - fn documentation() -> Option<&'static str> - where - Self: Sized, - { - None - } -} - -impl std::fmt::Debug for dyn Action { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("dyn Action") - .field("name", &self.name()) - .finish() - } -} - -impl dyn Action { - /// Type-erase Action type. - pub fn as_any(&self) -> &dyn Any { - self as &dyn Any - } -} - -/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use -/// markdown to display it. -#[derive(Debug)] -pub enum ActionBuildError { - /// Indicates that an action with this name has not been registered. - NotFound { - /// Name of the action that was not found. - name: String, - }, - /// Indicates that an error occurred while building the action, typically a JSON deserialization - /// error. - BuildError { - /// Name of the action that was attempting to be built. - name: String, - /// Error that occurred while building the action. - error: anyhow::Error, - }, -} - -impl std::error::Error for ActionBuildError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - ActionBuildError::NotFound { .. } => None, - ActionBuildError::BuildError { error, .. } => error.source(), - } - } -} - -impl Display for ActionBuildError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ActionBuildError::NotFound { name } => { - write!(f, "Didn't find an action named \"{name}\"") - } - ActionBuildError::BuildError { name, error } => { - write!(f, "Error while building action \"{name}\": {error}") - } - } - } -} - -type ActionBuilder = fn(json: serde_json::Value) -> anyhow::Result>; - -pub(crate) struct ActionRegistry { - by_name: HashMap<&'static str, ActionData>, - names_by_type_id: TypeIdHashMap<&'static str>, - all_names: Vec<&'static str>, // So we can return a static slice. - deprecated_aliases: HashMap<&'static str, &'static str>, // deprecated name -> preferred name - deprecation_messages: HashMap<&'static str, &'static str>, // action name -> deprecation message - documentation: HashMap<&'static str, &'static str>, // action name -> documentation -} - -impl Default for ActionRegistry { - fn default() -> Self { - let mut this = ActionRegistry { - by_name: Default::default(), - names_by_type_id: Default::default(), - documentation: Default::default(), - all_names: Default::default(), - deprecated_aliases: Default::default(), - deprecation_messages: Default::default(), - }; - - this.load_actions(); - - this - } -} - -struct ActionData { - pub build: ActionBuilder, - pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option, -} - -/// This type must be public so that our macros can build it in other crates. -/// But this is an implementation detail and should not be used directly. -#[doc(hidden)] -pub struct MacroActionBuilder(pub fn() -> MacroActionData); - -/// This type must be public so that our macros can build it in other crates. -/// But this is an implementation detail and should not be used directly. -#[doc(hidden)] -pub struct MacroActionData { - pub name: &'static str, - pub type_id: TypeId, - pub build: ActionBuilder, - pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option, - pub deprecated_aliases: &'static [&'static str], - pub deprecation_message: Option<&'static str>, - pub documentation: Option<&'static str>, -} - -inventory::collect!(MacroActionBuilder); - -impl ActionRegistry { - /// Load all registered actions into the registry. - pub(crate) fn load_actions(&mut self) { - for builder in inventory::iter:: { - let action = builder.0(); - self.insert_action(action); - } - } - - fn insert_action(&mut self, action: MacroActionData) { - let name = action.name; - if self.by_name.contains_key(name) { - panic!( - "Action with name `{name}` already registered \ - (might be registered in `#[action(deprecated_aliases = [...])]`." - ); - } - self.by_name.insert( - name, - ActionData { - build: action.build, - json_schema: action.json_schema, - }, - ); - for &alias in action.deprecated_aliases { - if self.by_name.contains_key(alias) { - panic!( - "Action with name `{alias}` already registered. \ - `{alias}` is specified in `#[action(deprecated_aliases = [...])]` for action `{name}`." - ); - } - self.by_name.insert( - alias, - ActionData { - build: action.build, - json_schema: action.json_schema, - }, - ); - self.deprecated_aliases.insert(alias, name); - self.all_names.push(alias); - } - self.names_by_type_id.insert(action.type_id, name); - self.all_names.push(name); - if let Some(deprecation_msg) = action.deprecation_message { - self.deprecation_messages.insert(name, deprecation_msg); - } - if let Some(documentation) = action.documentation { - self.documentation.insert(name, documentation); - } - } - - /// Construct an action based on its name and optional JSON parameters sourced from the keymap. - pub fn build_action_type(&self, type_id: &TypeId) -> Result> { - let name = self - .names_by_type_id - .get(type_id) - .with_context(|| format!("no action type registered for {type_id:?}"))?; - - Ok(self.build_action(name, None)?) - } - - #[cfg(feature = "profiler")] - pub(crate) fn try_resolve_action(&self, type_id: &TypeId) -> Option<&'static str> { - self.names_by_type_id.get(type_id).copied() - } - - /// Construct an action based on its name and optional JSON parameters sourced from the keymap. - pub fn build_action( - &self, - name: &str, - params: Option, - ) -> std::result::Result, ActionBuildError> { - let build_action = self - .by_name - .get(name) - .ok_or_else(|| ActionBuildError::NotFound { - name: name.to_owned(), - })? - .build; - (build_action)(params.unwrap_or_else(|| json!({}))).map_err(|e| { - ActionBuildError::BuildError { - name: name.to_owned(), - error: e, - } - }) - } - - pub fn all_action_names(&self) -> &[&'static str] { - self.all_names.as_slice() - } - - pub fn action_schemas( - &self, - generator: &mut schemars::SchemaGenerator, - ) -> Vec<(&'static str, Option)> { - // Use the order from all_names so that the resulting schema has sensible order. - self.all_names - .iter() - .map(|name| { - let action_data = self - .by_name - .get(name) - .expect("All actions in all_names should be registered"); - (*name, (action_data.json_schema)(generator)) - }) - .collect::>() - } - - pub fn action_schema_by_name( - &self, - name: &str, - generator: &mut schemars::SchemaGenerator, - ) -> Option> { - self.by_name - .get(name) - .map(|action_data| (action_data.json_schema)(generator)) - } - - pub fn deprecated_aliases(&self) -> &HashMap<&'static str, &'static str> { - &self.deprecated_aliases - } - - pub fn deprecation_messages(&self) -> &HashMap<&'static str, &'static str> { - &self.deprecation_messages - } - - pub fn documentation(&self) -> &HashMap<&'static str, &'static str> { - &self.documentation - } -} - -/// Generate a list of all the registered actions. -/// Useful for transforming the list of available actions into a -/// format suited for static analysis such as in validating keymaps, or -/// generating documentation. -pub fn generate_list_of_all_registered_actions() -> impl Iterator { - inventory::iter:: - .into_iter() - .map(|builder| builder.0()) -} - -mod no_action { - use crate as gpui; - use schemars::JsonSchema; - use serde::Deserialize; - - actions!( - zed, - [ - /// Action with special handling which unbinds the keybinding this is associated with, - /// if it is the highest precedence match. - NoAction - ] - ); - - /// Action with special handling which unbinds later bindings for the same keystrokes when they - /// dispatch the named action, regardless of that action's context. - /// - /// In keymap JSON this is written as: - /// - /// `["zed::Unbind", "editor::NewLine"]` - #[derive(Clone, Debug, PartialEq, Deserialize, JsonSchema, gpui::Action)] - #[action(namespace = zed)] - pub struct Unbind(pub gpui::SharedString); - - /// Returns whether or not this action represents a removed key binding. - pub fn is_no_action(action: &dyn gpui::Action) -> bool { - action.as_any().is::() - } - - /// Returns whether or not this action represents an unbind marker. - pub fn is_unbind(action: &dyn gpui::Action) -> bool { - action.as_any().is::() - } -} diff --git a/crates/gpui_pre/src/app.rs b/crates/gpui_pre/src/app.rs deleted file mode 100644 index 9811fc3..0000000 --- a/crates/gpui_pre/src/app.rs +++ /dev/null @@ -1,3192 +0,0 @@ -use scheduler::Instant; -use std::{ - any::{TypeId, type_name}, - cell::{BorrowMutError, Cell, Ref, RefCell, RefMut}, - ffi::OsString, - marker::PhantomData, - mem, - ops::{Deref, DerefMut}, - path::{Path, PathBuf}, - rc::{Rc, Weak}, - sync::{Arc, atomic::Ordering::SeqCst}, - time::Duration, -}; - -use anyhow::{Context as _, Result, anyhow}; -use derive_more::{Deref, DerefMut}; -use futures::{ - Future, FutureExt, - channel::oneshot, - future::{LocalBoxFuture, Shared}, -}; -use itertools::Itertools; -use parking_lot::RwLock; -use slotmap::SlotMap; - -pub use async_context::*; -#[cfg(feature = "bench-support")] -pub use bench_context::{BenchAppContext, BenchReport, BenchWindowContext, bench_platform}; -use collections::{FxHashMap, FxHashSet, HashMap, TypeIdHashMap, TypeIdHashSet, VecDeque}; -pub use context::*; -pub use entity_map::*; -use gpui_util::{ResultExt, debug_panic}; -#[cfg(any(test, feature = "test-support"))] -pub use headless_app_context::*; -use http_client::{HttpClient, Url}; -use smallvec::SmallVec; -#[cfg(any(test, feature = "test-support"))] -pub use test_app::*; -#[cfg(any(test, feature = "test-support"))] -pub use test_context::*; -#[cfg(all(target_os = "macos", any(test, feature = "test-support")))] -pub use visual_test_context::*; - -#[cfg(any(feature = "inspector", debug_assertions))] -use crate::InspectorElementRegistry; -use crate::{ - Action, ActionBuildError, ActionRegistry, Any, AnyView, AnyWindowHandle, AppContext, Arena, - ArenaBox, Asset, AssetSource, BackgroundExecutor, Bounds, ClipboardItem, ClipboardReadError, - CursorStyle, DispatchPhase, DisplayId, EventEmitter, ExternalDragPayload, FocusHandle, - FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext, Keymap, Keystroke, LayoutId, - Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform, PlatformDisplay, - PlatformKeyboardLayout, PlatformKeyboardMapper, Point, Priority, PromptBuilder, PromptButton, - PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, - ScreenCaptureSource, SharedString, SubscriberSet, Subscription, SvgRenderer, - SystemNotification, SystemNotificationResponse, Task, TextRenderingMode, TextSystem, - ThermalState, Window, WindowAppearance, WindowButtonLayout, WindowHandle, WindowId, - WindowInvalidator, - colors::{Colors, GlobalColors}, - hash, init_app_menus, -}; - -mod async_context; -#[cfg(feature = "bench-support")] -mod bench_context; -mod context; -mod entity_map; -#[cfg(any(test, feature = "test-support"))] -mod headless_app_context; -#[cfg(any(test, feature = "test-support"))] -mod test_app; -#[cfg(any(test, feature = "test-support"))] -mod test_context; -#[cfg(all(target_os = "macos", any(test, feature = "test-support")))] -mod visual_test_context; - -/// The duration for which native applications wait for futures returned from -/// [Context::on_app_quit] before fully quitting. -pub const SHUTDOWN_TIMEOUT: Duration = Duration::from_millis(200); - -/// Temporary(?) wrapper around [`RefCell`] to help us debug any double borrows. -/// Strongly consider removing after stabilization. -#[doc(hidden)] -pub struct AppCell { - app: RefCell, -} - -impl AppCell { - #[doc(hidden)] - #[track_caller] - pub fn borrow(&self) -> AppRef<'_> { - if option_env!("TRACK_THREAD_BORROWS").is_some() { - let thread_id = std::thread::current().id(); - eprintln!("borrowed {thread_id:?}"); - } - AppRef(self.app.borrow()) - } - - #[doc(hidden)] - #[track_caller] - pub fn borrow_mut(&self) -> AppRefMut<'_> { - if option_env!("TRACK_THREAD_BORROWS").is_some() { - let thread_id = std::thread::current().id(); - eprintln!("borrowed {thread_id:?}"); - } - AppRefMut(self.app.borrow_mut()) - } - - #[doc(hidden)] - #[track_caller] - pub fn try_borrow_mut(&self) -> Result, BorrowMutError> { - if option_env!("TRACK_THREAD_BORROWS").is_some() { - let thread_id = std::thread::current().id(); - eprintln!("borrowed {thread_id:?}"); - } - Ok(AppRefMut(self.app.try_borrow_mut()?)) - } -} - -#[doc(hidden)] -#[derive(Deref, DerefMut)] -pub struct AppRef<'a>(Ref<'a, App>); - -impl Drop for AppRef<'_> { - fn drop(&mut self) { - if option_env!("TRACK_THREAD_BORROWS").is_some() { - let thread_id = std::thread::current().id(); - eprintln!("dropped borrow from {thread_id:?}"); - } - } -} - -#[doc(hidden)] -#[derive(Deref, DerefMut)] -pub struct AppRefMut<'a>(RefMut<'a, App>); - -impl Drop for AppRefMut<'_> { - fn drop(&mut self) { - if option_env!("TRACK_THREAD_BORROWS").is_some() { - let thread_id = std::thread::current().id(); - eprintln!("dropped {thread_id:?}"); - } - } -} - -/// A reference to a GPUI application, typically constructed in the `main` function of your app. -/// You won't interact with this type much outside of initial configuration and startup. -pub struct Application(Rc); - -/// A strong handle to an [`Application`] started with [`Application::run_embedded`]. -/// -/// Dropping this handle releases the app, so an embedder must hold it for as long as the -/// app should run. While held, it is the embedder's entry point back into GPUI each time -/// the external run loop gives it control. -pub struct ApplicationHandle { - app: Rc, -} - -impl ApplicationHandle { - /// Invoke `f` with the app context. Must not be called re-entrantly from code that - /// is already inside an update; the app state is a `RefCell` and will panic on a - /// double borrow. - pub fn update(&self, f: impl FnOnce(&mut App) -> R) -> R { - let cx = &mut *self.app.borrow_mut(); - f(cx) - } - - /// An [`AsyncApp`] for use across await points. It holds the app weakly; keeping the - /// app alive remains this handle's job. - pub fn to_async(&self) -> AsyncApp { - self.update(|cx| cx.to_async()) - } -} - -/// Represents an application before it is fully launched. Once your app is -/// configured, you'll start the app with `App::run`. -impl Application { - /// Builds an app with a caller-provided platform implementation. - pub fn with_platform(platform: Rc) -> Self { - Self(App::new_app( - platform, - Arc::new(()), - Arc::new(NullHttpClient), - )) - } - - /// Builds an app with accessibility (AccessKit) integration forcibly - /// disabled. - /// - /// In this mode, accessibility APIs (e.g. - /// [`div().role()`][crate::StatefulInteractiveElement::role]) silently - /// no-op. - /// - /// See the [accessibility guide](crate::_accessibility) for an overview of - /// the features this disables. - pub fn new_inaccessible(platform: Rc) -> Self { - let this = Self::with_platform(platform); - this.0.borrow_mut().accessibility_force_disabled = true; - this - } - - /// Assigns the source of assets for the application. - pub fn with_assets(self, asset_source: impl AssetSource) -> Self { - let mut context_lock = self.0.borrow_mut(); - let asset_source = Arc::new(asset_source); - context_lock.asset_source = asset_source.clone(); - context_lock.svg_renderer = SvgRenderer::new(asset_source); - drop(context_lock); - self - } - - /// Configures arguments to pass when restarting the application. - pub fn with_restart_arguments(self, arguments: Vec) -> Self { - self.0.borrow_mut().restart_arguments = arguments; - self - } - - /// Sets the HTTP client for the application. - pub fn with_http_client(self, http_client: Arc) -> Self { - let mut context_lock = self.0.borrow_mut(); - context_lock.http_client = http_client; - drop(context_lock); - self - } - - /// Configures when the application should automatically quit. - /// By default, [`QuitMode::Default`] is used. - pub fn with_quit_mode(self, mode: QuitMode) -> Self { - self.0.borrow_mut().quit_mode = mode; - self - } - - /// Start the application. The provided callback will be called once the - /// app is fully launched. - pub fn run(self, on_finish_launching: F) - where - F: 'static + FnOnce(&mut App), - { - let this = self.0.clone(); - let platform = self.0.borrow().platform.clone(); - platform.run(Box::new(move || { - let cx = &mut *this.borrow_mut(); - on_finish_launching(cx); - })); - } - - /// Start the application for an embedder that drives the run loop itself. - /// - /// On ordinary platforms `Platform::run` blocks for the lifetime of the app, and the - /// app state is kept alive by [`Application::run`]'s stack frame. Embedded platforms — - /// where the run loop belongs to someone else, e.g. GPUI compiled into a Wasm guest, - /// or a GPUI view hosted inside a foreign native application — implement - /// `Platform::run` to invoke the launch callback and return immediately. This method - /// supports that shape: it returns an [`ApplicationHandle`] that keeps the app alive - /// and lets the embedder re-enter it whenever the external run loop yields control. - pub fn run_embedded(self, on_finish_launching: F) -> ApplicationHandle - where - F: 'static + FnOnce(&mut App), - { - let this = self.0.clone(); - let platform = self.0.borrow().platform.clone(); - platform.run(Box::new(move || { - let cx = &mut *this.borrow_mut(); - on_finish_launching(cx); - })); - ApplicationHandle { app: self.0 } - } - - /// Register a handler to be invoked when the platform instructs the application - /// to open one or more URLs. - pub fn on_open_urls(&self, mut callback: F) -> &Self - where - F: 'static + FnMut(Vec), - { - self.0.borrow().platform.on_open_urls(Box::new(callback)); - self - } - - /// Invokes a handler when an already-running application is launched. - /// On macOS, this can occur when the application icon is double-clicked or the app is launched via the dock. - pub fn on_reopen(&self, mut callback: F) -> &Self - where - F: 'static + FnMut(&mut App), - { - let this = Rc::downgrade(&self.0); - self.0.borrow_mut().platform.on_reopen(Box::new(move || { - if let Some(app) = this.upgrade() { - callback(&mut app.borrow_mut()); - } - })); - self - } - - /// Returns a handle to the [`BackgroundExecutor`] associated with this app, which can be used to spawn futures in the background. - pub fn background_executor(&self) -> BackgroundExecutor { - self.0.borrow().background_executor.clone() - } - - /// Returns a handle to the [`ForegroundExecutor`] associated with this app, which can be used to spawn futures in the foreground. - pub fn foreground_executor(&self) -> ForegroundExecutor { - self.0.borrow().foreground_executor.clone() - } - - /// Returns a reference to the [`TextSystem`] associated with this app. - pub fn text_system(&self) -> Arc { - self.0.borrow().text_system.clone() - } - - /// Returns the file URL of the executable with the specified name in the application bundle - pub fn path_for_auxiliary_executable(&self, name: &str) -> Result { - self.0.borrow().path_for_auxiliary_executable(name) - } -} - -type Handler = Box bool + 'static>; -type Listener = Box bool + 'static>; -pub(crate) type KeystrokeObserver = - Box bool + 'static>; -type QuitHandler = Box LocalBoxFuture<'static, ()> + 'static>; -type WindowClosedHandler = Box; -type ReleaseListener = Box; -type NewEntityListener = Box, &mut App) + 'static>; - -/// Defines when the application should automatically quit. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum QuitMode { - /// Use [`QuitMode::Explicit`] on macOS and [`QuitMode::LastWindowClosed`] on other platforms. - #[default] - Default, - /// Quit automatically when the last window is closed. - LastWindowClosed, - /// Quit only when requested via [`App::quit`]. - Explicit, -} - -/// Controls when GPUI hides the mouse cursor in response to keyboard input. -/// -/// Restoration on mouse motion is handled by the platform layer; this enum -/// only describes the policy for *triggering* a hide. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -pub enum CursorHideMode { - /// Never hide the cursor automatically. - Never, - /// Hide on character-producing key presses (typing). - OnTyping, - /// Hide on character-producing key presses, *and* when a key binding - /// resolves to an action that consumes the keystroke. - #[default] - OnTypingAndAction, -} - -#[doc(hidden)] -#[derive(Clone, PartialEq, Eq)] -pub struct SystemWindowTab { - pub id: WindowId, - pub title: SharedString, - pub handle: AnyWindowHandle, - pub last_active_at: Instant, -} - -impl SystemWindowTab { - /// Create a new instance of the window tab. - pub fn new(title: SharedString, handle: AnyWindowHandle) -> Self { - Self { - id: handle.id, - title, - handle, - last_active_at: Instant::now(), - } - } -} - -/// A controller for managing window tabs. -#[derive(Default)] -pub struct SystemWindowTabController { - visible: Option, - tab_groups: FxHashMap>, -} - -impl Global for SystemWindowTabController {} - -impl SystemWindowTabController { - /// Create a new instance of the window tab controller. - pub fn new() -> Self { - Self { - visible: None, - tab_groups: FxHashMap::default(), - } - } - - /// Initialize the global window tab controller. - pub fn init(cx: &mut App) { - cx.set_global(SystemWindowTabController::new()); - } - - /// Get all tab groups. - pub fn tab_groups(&self) -> &FxHashMap> { - &self.tab_groups - } - - /// Get the next tab group window handle. - pub fn get_next_tab_group_window(cx: &mut App, id: WindowId) -> Option<&AnyWindowHandle> { - let controller = cx.global::(); - let current_group = controller - .tab_groups - .iter() - .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group)); - - let current_group = current_group?; - // TODO: `.keys()` returns arbitrary order, what does "next" mean? - let mut group_ids: Vec<_> = controller.tab_groups.keys().collect(); - let idx = group_ids.iter().position(|g| *g == current_group)?; - let next_idx = (idx + 1) % group_ids.len(); - - controller - .tab_groups - .get(group_ids[next_idx]) - .and_then(|tabs| { - tabs.iter() - .max_by_key(|tab| tab.last_active_at) - .or_else(|| tabs.first()) - .map(|tab| &tab.handle) - }) - } - - /// Get the previous tab group window handle. - pub fn get_prev_tab_group_window(cx: &mut App, id: WindowId) -> Option<&AnyWindowHandle> { - let controller = cx.global::(); - let current_group = controller - .tab_groups - .iter() - .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group)); - - let current_group = current_group?; - // TODO: `.keys()` returns arbitrary order, what does "previous" mean? - let mut group_ids: Vec<_> = controller.tab_groups.keys().collect(); - let idx = group_ids.iter().position(|g| *g == current_group)?; - let prev_idx = if idx == 0 { - group_ids.len() - 1 - } else { - idx - 1 - }; - - controller - .tab_groups - .get(group_ids[prev_idx]) - .and_then(|tabs| { - tabs.iter() - .max_by_key(|tab| tab.last_active_at) - .or_else(|| tabs.first()) - .map(|tab| &tab.handle) - }) - } - - /// Get all tabs in the same window. - pub fn tabs(&self, id: WindowId) -> Option<&Vec> { - self.tab_groups - .values() - .find(|tabs| tabs.iter().any(|tab| tab.id == id)) - } - - /// Initialize the visibility of the system window tab controller. - pub fn init_visible(cx: &mut App, visible: bool) { - let mut controller = cx.global_mut::(); - if controller.visible.is_none() { - controller.visible = Some(visible); - } - } - - /// Get the visibility of the system window tab controller. - pub fn is_visible(&self) -> bool { - self.visible.unwrap_or(false) - } - - /// Set the visibility of the system window tab controller. - pub fn set_visible(cx: &mut App, visible: bool) { - let mut controller = cx.global_mut::(); - controller.visible = Some(visible); - } - - /// Update the last active of a window. - pub fn update_last_active(cx: &mut App, id: WindowId) { - let mut controller = cx.global_mut::(); - for windows in controller.tab_groups.values_mut() { - for tab in windows.iter_mut() { - if tab.id == id { - tab.last_active_at = Instant::now(); - } - } - } - } - - /// Update the position of a tab within its group. - pub fn update_tab_position(cx: &mut App, id: WindowId, ix: usize) { - let mut controller = cx.global_mut::(); - for (_, windows) in controller.tab_groups.iter_mut() { - if let Some(current_pos) = windows.iter().position(|tab| tab.id == id) { - if ix < windows.len() && current_pos != ix { - let window_tab = windows.remove(current_pos); - windows.insert(ix, window_tab); - } - break; - } - } - } - - /// Update the title of a tab. - pub fn update_tab_title(cx: &mut App, id: WindowId, title: SharedString) { - let controller = cx.global::(); - let tab = controller - .tab_groups - .values() - .flat_map(|windows| windows.iter()) - .find(|tab| tab.id == id); - - if tab.map_or(true, |t| t.title == title) { - return; - } - - let mut controller = cx.global_mut::(); - for windows in controller.tab_groups.values_mut() { - for tab in windows.iter_mut() { - if tab.id == id { - tab.title = title; - return; - } - } - } - } - - /// Insert a tab into a tab group. - pub fn add_tab(cx: &mut App, id: WindowId, tabs: Vec) { - let mut controller = cx.global_mut::(); - let Some(tab) = tabs.iter().find(|tab| tab.id == id).cloned() else { - return; - }; - - let mut expected_tab_ids: Vec<_> = tabs - .iter() - .filter(|tab| tab.id != id) - .map(|tab| tab.id) - .sorted() - .collect(); - - let mut tab_group_id = None; - for (group_id, group_tabs) in &controller.tab_groups { - let tab_ids: Vec<_> = group_tabs.iter().map(|tab| tab.id).sorted().collect(); - if tab_ids == expected_tab_ids { - tab_group_id = Some(*group_id); - break; - } - } - - if let Some(tab_group_id) = tab_group_id { - if let Some(tabs) = controller.tab_groups.get_mut(&tab_group_id) { - tabs.push(tab); - } - } else { - let new_group_id = controller.tab_groups.len(); - controller.tab_groups.insert(new_group_id, tabs); - } - } - - /// Remove a tab from a tab group. - pub fn remove_tab(cx: &mut App, id: WindowId) -> Option { - let mut controller = cx.global_mut::(); - let mut removed_tab = None; - - controller.tab_groups.retain(|_, tabs| { - if let Some(pos) = tabs.iter().position(|tab| tab.id == id) { - removed_tab = Some(tabs.remove(pos)); - } - !tabs.is_empty() - }); - - removed_tab - } - - /// Move a tab to a new tab group. - pub fn move_tab_to_new_window(cx: &mut App, id: WindowId) { - let mut removed_tab = Self::remove_tab(cx, id); - let mut controller = cx.global_mut::(); - - if let Some(tab) = removed_tab { - let new_group_id = controller.tab_groups.keys().max().map_or(0, |k| k + 1); - controller.tab_groups.insert(new_group_id, vec![tab]); - } - } - - /// Merge all tab groups into a single group. - pub fn merge_all_windows(cx: &mut App, id: WindowId) { - let mut controller = cx.global_mut::(); - let Some(initial_tabs) = controller.tabs(id) else { - return; - }; - - let initial_tabs_len = initial_tabs.len(); - let mut all_tabs = initial_tabs.clone(); - - for (_, mut tabs) in controller.tab_groups.drain() { - tabs.retain(|tab| !all_tabs[..initial_tabs_len].contains(tab)); - all_tabs.extend(tabs); - } - - controller.tab_groups.insert(0, all_tabs); - } - - /// Selects the next tab in the tab group in the trailing direction. - pub fn select_next_tab(cx: &mut App, id: WindowId) { - let mut controller = cx.global_mut::(); - let Some(tabs) = controller.tabs(id) else { - return; - }; - - let current_index = tabs.iter().position(|tab| tab.id == id).unwrap(); - let next_index = (current_index + 1) % tabs.len(); - - let _ = &tabs[next_index].handle.update(cx, |_, window, _| { - window.activate_window(); - }); - } - - /// Selects the previous tab in the tab group in the leading direction. - pub fn select_previous_tab(cx: &mut App, id: WindowId) { - let mut controller = cx.global_mut::(); - let Some(tabs) = controller.tabs(id) else { - return; - }; - - let current_index = tabs.iter().position(|tab| tab.id == id).unwrap(); - let previous_index = if current_index == 0 { - tabs.len() - 1 - } else { - current_index - 1 - }; - - let _ = &tabs[previous_index].handle.update(cx, |_, window, _| { - window.activate_window(); - }); - } -} - -pub(crate) enum GpuiMode { - #[cfg(any(test, feature = "test-support"))] - Test { - skip_drawing: bool, - }, - Production, -} - -impl GpuiMode { - #[cfg(any(test, feature = "test-support"))] - pub fn test() -> Self { - GpuiMode::Test { - skip_drawing: false, - } - } - - #[inline] - pub(crate) fn skip_drawing(&self) -> bool { - match self { - #[cfg(any(test, feature = "test-support"))] - GpuiMode::Test { skip_drawing } => *skip_drawing, - GpuiMode::Production => false, - } - } -} - -struct PlatformOwnedDrag { - source_window: WindowId, - state: PlatformOwnedDragState, -} - -enum PlatformOwnedDragState { - Suspended(AnyDrag), - // A source-window drop consumes `active_drag` before AppKit ends the dragging session, so this - // marker can outlive the active drag and is cleaned up by `FileDropEvent::Ended`. - RestoredInSourceWindow, -} - -/// Contains the state of the full application, and passed as a reference to a variety of callbacks. -/// Other [Context] derefs to this type. -/// You need a reference to an `App` to access the state of a [Entity]. -pub struct App { - pub(crate) this: Weak, - pub(crate) platform: Rc, - text_system: Arc, - - pub(crate) actions: Rc, - pub(crate) active_drag: Option, - platform_owned_drag: Option, - pub(crate) background_executor: BackgroundExecutor, - pub(crate) foreground_executor: ForegroundExecutor, - #[cfg(feature = "profiler")] - foreground_journal: crate::profiler::journal::ForegroundJournal, - pub(crate) entities: EntityMap, - pub(crate) new_entity_observers: SubscriberSet, - pub(crate) windows: SlotMap>>, - pub(crate) window_handles: FxHashMap, - pub(crate) focus_handles: Arc, - pub(crate) keymap: Rc>, - pub(crate) keyboard_layout: Box, - pub(crate) keyboard_mapper: Rc, - pub(crate) global_action_listeners: - TypeIdHashMap>>, - pending_effects: VecDeque, - - pub(crate) observers: SubscriberSet, - pub(crate) event_listeners: SubscriberSet, - pub(crate) keystroke_observers: SubscriberSet<(), KeystrokeObserver>, - pub(crate) keystroke_interceptors: SubscriberSet<(), KeystrokeObserver>, - pub(crate) keyboard_layout_observers: SubscriberSet<(), Handler>, - pub(crate) thermal_state_observers: SubscriberSet<(), Handler>, - pub(crate) system_wake_observers: SubscriberSet<(), Handler>, - pub(crate) release_listeners: SubscriberSet, - pub(crate) global_observers: SubscriberSet, - pub(crate) quit_observers: SubscriberSet<(), QuitHandler>, - pub(crate) restart_observers: SubscriberSet<(), Handler>, - pub(crate) window_closed_observers: SubscriberSet<(), WindowClosedHandler>, - - /// Per-App element arena. This isolates element allocations between different - /// App instances (important for tests where multiple Apps run concurrently). - pub(crate) element_arena: RefCell, - /// Per-App event arena. - pub(crate) event_arena: Arena, - - // Drop globals last. We need to ensure all tasks owned by entities and - // callbacks are marked cancelled at this point as this will also shutdown - // the tokio runtime. As any task attempting to spawn a blocking tokio task, - // might panic. - pub(crate) globals_by_type: TypeIdHashMap>, - - // assets - pub(crate) loading_assets: FxHashMap<(TypeId, u64), Box>, - asset_source: Arc, - pub(crate) svg_renderer: SvgRenderer, - http_client: Arc, - - // below is plain data, the drop order is insignificant here - pub(crate) pending_notifications: FxHashSet, - pub(crate) pending_global_notifications: TypeIdHashSet, - pub(crate) restart_path: Option, - pub(crate) restart_arguments: Vec, - pub(crate) layout_id_buffer: Vec, // We recycle this memory across layout requests. - pub(crate) propagate_event: bool, - pub(crate) prompt_builder: Option, - pub(crate) window_invalidators_by_entity: - FxHashMap>, - pub(crate) tracked_entities: FxHashMap>, - pub(crate) current_window_by_entity: FxHashMap, - #[cfg(any(feature = "inspector", debug_assertions))] - pub(crate) inspector_renderer: Option, - #[cfg(any(feature = "inspector", debug_assertions))] - pub(crate) inspector_element_registry: InspectorElementRegistry, - #[cfg(any(test, feature = "test-support", debug_assertions))] - pub(crate) name: Option<&'static str>, - pub(crate) text_rendering_mode: Rc>, - - pub(crate) window_update_stack: Vec, - pub(crate) mode: GpuiMode, - pub(crate) cursor_hide_mode: CursorHideMode, - pub(crate) reduce_motion: bool, - /// Origin of the shared clock that phase-locks synced repeating animations. - pub(crate) synced_animation_epoch: Instant, - /// Whether the app was created by [`Application::new_inaccessible`]. No - /// accesskit APIs will be called when this flag is set. - pub(crate) accessibility_force_disabled: bool, - flushing_effects: bool, - pending_updates: usize, - quit_mode: QuitMode, - quitting: bool, - - // We need to ensure the leak detector drops last, after all tasks, callbacks and things have been dropped. - // Otherwise it may report false positives. - #[cfg(any(test, feature = "leak-detection"))] - _ref_counts: Arc>, -} - -impl App { - #[allow(clippy::new_ret_no_self)] - pub(crate) fn new_app( - platform: Rc, - asset_source: Arc, - http_client: Arc, - ) -> Rc { - let background_executor = platform.background_executor(); - let foreground_executor = platform.foreground_executor(); - assert!( - background_executor.is_main_thread(), - "must construct App on main thread" - ); - #[cfg(feature = "profiler")] - let foreground_journal = crate::profiler::journal::install_foreground_journal(); - let synced_animation_epoch = background_executor.now(); - - let text_system = Arc::new(TextSystem::new(platform.text_system())); - let entities = EntityMap::new(); - let keyboard_layout = platform.keyboard_layout(); - let keyboard_mapper = platform.keyboard_mapper(); - - #[cfg(any(test, feature = "leak-detection"))] - let _ref_counts = entities.ref_counts_drop_handle(); - - let app = Rc::new_cyclic(|this| AppCell { - app: RefCell::new(App { - this: this.clone(), - platform: platform.clone(), - text_system, - text_rendering_mode: Rc::new(Cell::new(TextRenderingMode::default())), - mode: GpuiMode::Production, - actions: Rc::new(ActionRegistry::default()), - flushing_effects: false, - pending_updates: 0, - active_drag: None, - platform_owned_drag: None, - background_executor, - foreground_executor, - #[cfg(feature = "profiler")] - foreground_journal, - svg_renderer: SvgRenderer::new(asset_source.clone()), - loading_assets: Default::default(), - asset_source, - http_client, - globals_by_type: Default::default(), - entities, - new_entity_observers: SubscriberSet::new(), - windows: SlotMap::with_key(), - window_update_stack: Vec::new(), - window_handles: FxHashMap::default(), - focus_handles: Arc::new(RwLock::new(SlotMap::with_key())), - keymap: Rc::new(RefCell::new(Keymap::default())), - keyboard_layout, - keyboard_mapper, - global_action_listeners: Default::default(), - pending_effects: VecDeque::new(), - pending_notifications: FxHashSet::default(), - pending_global_notifications: Default::default(), - observers: SubscriberSet::new(), - tracked_entities: FxHashMap::default(), - window_invalidators_by_entity: FxHashMap::default(), - current_window_by_entity: FxHashMap::default(), - event_listeners: SubscriberSet::new(), - release_listeners: SubscriberSet::new(), - keystroke_observers: SubscriberSet::new(), - keystroke_interceptors: SubscriberSet::new(), - keyboard_layout_observers: SubscriberSet::new(), - thermal_state_observers: SubscriberSet::new(), - system_wake_observers: SubscriberSet::new(), - global_observers: SubscriberSet::new(), - quit_observers: SubscriberSet::new(), - restart_observers: SubscriberSet::new(), - restart_path: None, - restart_arguments: Vec::new(), - window_closed_observers: SubscriberSet::new(), - layout_id_buffer: Default::default(), - propagate_event: true, - prompt_builder: Some(PromptBuilder::Default), - #[cfg(any(feature = "inspector", debug_assertions))] - inspector_renderer: None, - #[cfg(any(feature = "inspector", debug_assertions))] - inspector_element_registry: InspectorElementRegistry::default(), - quit_mode: QuitMode::default(), - quitting: false, - cursor_hide_mode: CursorHideMode::default(), - reduce_motion: false, - synced_animation_epoch, - accessibility_force_disabled: false, - - #[cfg(any(test, feature = "test-support", debug_assertions))] - name: None, - element_arena: RefCell::new(Arena::new(1024 * 1024)), - event_arena: Arena::new(1024 * 1024), - - #[cfg(any(test, feature = "leak-detection"))] - _ref_counts, - }), - }); - - init_app_menus(platform.as_ref(), &app.borrow()); - SystemWindowTabController::init(&mut app.borrow_mut()); - - platform.on_keyboard_layout_change(Box::new({ - let app = Rc::downgrade(&app); - move || { - if let Some(app) = app.upgrade() { - let cx = &mut app.borrow_mut(); - cx.keyboard_layout = cx.platform.keyboard_layout(); - cx.keyboard_mapper = cx.platform.keyboard_mapper(); - cx.keyboard_layout_observers - .clone() - .retain(&(), move |callback| (callback)(cx)); - } - } - })); - - platform.on_thermal_state_change(Box::new({ - let app = Rc::downgrade(&app); - move || { - if let Some(app) = app.upgrade() { - let cx = &mut app.borrow_mut(); - cx.thermal_state_observers - .clone() - .retain(&(), move |callback| (callback)(cx)); - } - } - })); - - platform.on_system_wake(Box::new({ - let app = Rc::downgrade(&app); - move || { - if let Some(app) = app.upgrade() { - let cx = &mut app.borrow_mut(); - cx.system_wake_observers - .clone() - .retain(&(), move |callback| (callback)(cx)); - } - } - })); - - platform.on_quit(Box::new({ - let cx = Rc::downgrade(&app); - move || { - let Some(cx) = cx.upgrade() else { - return true; - }; - match cx.try_borrow_mut() { - Ok(mut cx) => { - cx.shutdown(); - true - } - Err(_) => { - // Quit was requested while the AppCell was borrowed, so we can't shut down synchronously. - // The platform decides how to proceed. - false - } - } - } - })); - - app - } - - #[doc(hidden)] - pub fn ref_counts_drop_handle(&self) -> impl Sized + use<> { - self.entities.ref_counts_drop_handle() - } - - /// Captures a snapshot of all entities that currently have alive handles. - /// - /// The returned [`LeakDetectorSnapshot`] can later be passed to - /// [`assert_no_new_leaks`](Self::assert_no_new_leaks) to verify that no - /// entities created after the snapshot are still alive. - #[cfg(any(test, feature = "leak-detection"))] - pub fn leak_detector_snapshot(&self) -> LeakDetectorSnapshot { - self.entities.leak_detector_snapshot() - } - - /// Asserts that no entities created after `snapshot` still have alive handles. - /// - /// Entities that were already tracked at the time of the snapshot are ignored, - /// even if they still have handles. Only *new* entities (those whose - /// `EntityId` was not present in the snapshot) are considered leaks. - /// - /// # Panics - /// - /// Panics if any new entity handles exist. The panic message lists every - /// leaked entity with its type name, and includes allocation-site backtraces - /// when `LEAK_BACKTRACE` is set. - #[cfg(any(test, feature = "leak-detection"))] - pub fn assert_no_new_leaks(&self, snapshot: &LeakDetectorSnapshot) { - self.entities.assert_no_new_leaks(snapshot) - } - - /// Quit the application gracefully. - /// - /// Native applications give handlers registered with [`Context::on_app_quit`] - /// [`SHUTDOWN_TIMEOUT`] to complete. WebAssembly runs them asynchronously as best-effort cleanup - /// because its event-loop thread cannot block. - pub fn shutdown(&mut self) { - let mut futures = Vec::new(); - - for observer in self.quit_observers.remove(&()) { - futures.push(observer(self)); - } - - self.windows.clear(); - self.window_handles.clear(); - self.flush_effects(); - self.quitting = true; - - let futures = futures::future::join_all(futures); - #[cfg(not(target_family = "wasm"))] - if self - .foreground_executor - .block_with_timeout(SHUTDOWN_TIMEOUT, futures) - .is_err() - { - log::error!("timed out waiting on app_will_quit"); - } - #[cfg(target_family = "wasm")] - self.foreground_executor.spawn(futures).detach(); - - self.quitting = false; - } - - /// Get the id of the current keyboard layout - pub fn keyboard_layout(&self) -> &dyn PlatformKeyboardLayout { - self.keyboard_layout.as_ref() - } - - /// Get the current keyboard mapper. - pub fn keyboard_mapper(&self) -> &Rc { - &self.keyboard_mapper - } - - /// Invokes a handler when the current keyboard layout changes - pub fn on_keyboard_layout_change(&self, mut callback: F) -> Subscription - where - F: 'static + FnMut(&mut App), - { - let (subscription, activate) = self.keyboard_layout_observers.insert( - (), - Box::new(move |cx| { - callback(cx); - true - }), - ); - activate(); - subscription - } - - /// Gracefully quit the application via the platform's standard routine. - pub fn quit(&self) { - self.platform.quit(); - } - - /// Returns the current policy for hiding the cursor in response to - /// keyboard input. - pub fn cursor_hide_mode(&self) -> CursorHideMode { - self.cursor_hide_mode - } - - /// Sets the policy controlling when GPUI hides the cursor in response - /// to keyboard input. - pub fn set_cursor_hide_mode(&mut self, mode: CursorHideMode) { - self.cursor_hide_mode = mode; - } - - /// Returns whether the cursor is currently visible according to the - /// platform. This will report `false` after a keyboard input has hidden - /// the cursor and the user has not yet moved the mouse to restore it. - /// - /// See [`App::set_cursor_hide_mode`]. - pub fn is_cursor_visible(&self) -> bool { - self.platform.is_cursor_visible() - } - - /// Returns whether non-essential animations (e.g. loading spinners) should - /// be rendered in a static state instead of animating. - pub fn reduce_motion(&self) -> bool { - self.reduce_motion - } - - /// Sets whether non-essential animations (e.g. loading spinners) should be - /// rendered in a static state instead of animating. - pub fn set_reduce_motion(&mut self, reduce_motion: bool) { - if self.reduce_motion != reduce_motion { - self.reduce_motion = reduce_motion; - self.refresh_windows(); - } - } - - /// Schedules all windows in the application to be redrawn. This can be called - /// multiple times in an update cycle and still result in a single redraw. - pub fn refresh_windows(&mut self) { - self.pending_effects.push_back(Effect::RefreshWindows); - } - - pub(crate) fn update(&mut self, update: impl FnOnce(&mut Self) -> R) -> R { - self.start_update(); - let result = update(self); - self.finish_update(); - result - } - - pub(crate) fn start_update(&mut self) { - self.pending_updates += 1; - } - - pub(crate) fn finish_update(&mut self) { - if !self.flushing_effects && self.pending_updates == 1 { - self.flushing_effects = true; - self.flush_effects(); - self.flushing_effects = false; - } - self.pending_updates -= 1; - } - - /// Arrange a callback to be invoked when the given entity calls `notify` on its respective context. - pub fn observe( - &mut self, - entity: &Entity, - mut on_notify: impl FnMut(Entity, &mut App) + 'static, - ) -> Subscription - where - W: 'static, - { - self.observe_internal(entity, move |e, cx| { - on_notify(e, cx); - true - }) - } - - pub(crate) fn detect_accessed_entities( - &mut self, - callback: impl FnOnce(&mut App) -> R, - ) -> (R, FxHashSet) { - let accessed_entities_start = self.entities.accessed_entities.get_mut().clone(); - let result = callback(self); - let entities_accessed_in_callback = self - .entities - .accessed_entities - .get_mut() - .difference(&accessed_entities_start) - .copied() - .collect::>(); - (result, entities_accessed_in_callback) - } - - pub(crate) fn record_entities_accessed( - &mut self, - window_handle: AnyWindowHandle, - invalidator: WindowInvalidator, - entities: &FxHashSet, - ) { - let mut tracked_entities = - std::mem::take(self.tracked_entities.entry(window_handle.id).or_default()); - for entity in tracked_entities.iter() { - self.window_invalidators_by_entity - .entry(*entity) - .and_modify(|windows| { - windows.remove(&window_handle.id); - }); - } - for entity in entities.iter() { - self.window_invalidators_by_entity - .entry(*entity) - .or_default() - .insert(window_handle.id, invalidator.clone()); - self.current_window_by_entity - .insert(*entity, window_handle.id); - } - tracked_entities.clear(); - tracked_entities.extend(entities.iter().copied()); - self.tracked_entities - .insert(window_handle.id, tracked_entities); - } - - pub(crate) fn new_observer(&mut self, key: EntityId, value: Handler) -> Subscription { - let (subscription, activate) = self.observers.insert(key, value); - self.defer(move |_| activate()); - subscription - } - - pub(crate) fn observe_internal( - &mut self, - entity: &Entity, - mut on_notify: impl FnMut(Entity, &mut App) -> bool + 'static, - ) -> Subscription - where - W: 'static, - { - let entity_id = entity.entity_id(); - let handle = entity.downgrade(); - self.new_observer( - entity_id, - Box::new(move |cx| { - if let Some(entity) = handle.upgrade() { - on_notify(entity, cx) - } else { - false - } - }), - ) - } - - /// Arrange for the given callback to be invoked whenever the given entity emits an event of a given type. - /// The callback is provided a handle to the emitting entity and a reference to the emitted event. - pub fn subscribe( - &mut self, - entity: &Entity, - mut on_event: impl FnMut(Entity, &Event, &mut App) + 'static, - ) -> Subscription - where - T: 'static + EventEmitter, - Event: 'static, - { - self.subscribe_internal(entity, move |entity, event, cx| { - on_event(entity, event, cx); - true - }) - } - - pub(crate) fn new_subscription( - &mut self, - key: EntityId, - value: (TypeId, Listener), - ) -> Subscription { - let (subscription, activate) = self.event_listeners.insert(key, value); - self.defer(move |_| activate()); - subscription - } - pub(crate) fn subscribe_internal( - &mut self, - entity: &Entity, - mut on_event: impl FnMut(Entity, &Evt, &mut App) -> bool + 'static, - ) -> Subscription - where - T: 'static + EventEmitter, - Evt: 'static, - { - let entity_id = entity.entity_id(); - let handle = entity.downgrade(); - self.new_subscription( - entity_id, - ( - TypeId::of::(), - Box::new(move |event, cx| { - let event: &Evt = event.downcast_ref().expect("invalid event type"); - if let Some(entity) = handle.upgrade() { - on_event(entity, event, cx) - } else { - false - } - }), - ), - ) - } - - /// Returns handles to all open windows in the application. - /// Each handle could be downcast to a handle typed for the root view of that window. - /// To find all windows of a given type, you could filter on - pub fn windows(&self) -> Vec { - self.windows - .keys() - .flat_map(|window_id| self.window_handles.get(&window_id).copied()) - .collect() - } - - /// Returns the window handles ordered by their appearance on screen, front to back. - /// - /// The first window in the returned list is the active/topmost window of the application. - /// - /// This method returns None if the platform doesn't implement the method yet. - pub fn window_stack(&self) -> Option> { - self.platform.window_stack() - } - - /// Returns a handle to the window that is currently focused at the platform level, if one exists. - pub fn active_window(&self) -> Option { - self.platform.active_window() - } - - /// Opens a new window with the given option and the root view returned by the given function. - /// The function is invoked with a `Window`, which can be used to interact with window-specific - /// functionality. - pub fn open_window( - &mut self, - options: crate::WindowOptions, - build_root_view: impl FnOnce(&mut Window, &mut App) -> Entity, - ) -> anyhow::Result> { - self.update(|cx| { - let id = cx.windows.insert(None); - let handle = WindowHandle::new(id); - match Window::new(handle.into(), options, cx) { - Ok(mut window) => { - cx.window_update_stack.push(id); - let root_view = build_root_view(&mut window, cx); - cx.window_update_stack.pop(); - window.root.replace(root_view.into()); - window.defer(cx, |window: &mut Window, cx| window.appearance_changed(cx)); - - // allow a window to draw at least once before returning - // this didn't cause any issues on non windows platforms as it seems we always won the race to on_request_frame - // on windows we quite frequently lose the race and return a window that has never rendered, which leads to a crash - // where DispatchTree::root_node_id asserts on empty nodes - let clear = window.draw(cx); - clear.clear(cx); - - cx.window_handles.insert(id, window.handle); - cx.windows.get_mut(id).unwrap().replace(Box::new(window)); - Ok(handle) - } - Err(e) => { - cx.windows.remove(id); - Err(e) - } - } - }) - } - - /// Instructs the platform to activate the application by bringing it to the foreground. - pub fn activate(&self, ignoring_other_apps: bool) { - self.platform.activate(ignoring_other_apps); - } - - /// Hide the application at the platform level. - pub fn hide(&self) { - self.platform.hide(); - } - - /// Hide other applications at the platform level. - pub fn hide_other_apps(&self) { - self.platform.hide_other_apps(); - } - - /// Unhide other applications at the platform level. - pub fn unhide_other_apps(&self) { - self.platform.unhide_other_apps(); - } - - /// Returns the list of currently active displays. - pub fn displays(&self) -> Vec> { - self.platform.displays() - } - - /// Returns the primary display that will be used for new windows. - pub fn primary_display(&self) -> Option> { - self.platform.primary_display() - } - - /// Returns whether `screen_capture_sources` may work. - pub fn is_screen_capture_supported(&self) -> bool { - self.platform.is_screen_capture_supported() - } - - /// Returns a list of available screen capture sources. - pub fn screen_capture_sources( - &self, - ) -> oneshot::Receiver>>> { - self.platform.screen_capture_sources() - } - - /// Returns the display with the given ID, if one exists. - pub fn find_display(&self, id: DisplayId) -> Option> { - self.displays() - .iter() - .find(|display| display.id() == id) - .cloned() - } - - /// Returns the current thermal state of the system. - pub fn thermal_state(&self) -> ThermalState { - self.platform.thermal_state() - } - - /// Invokes a handler when the thermal state changes - pub fn on_thermal_state_change(&self, mut callback: F) -> Subscription - where - F: 'static + FnMut(&mut App), - { - let (subscription, activate) = self.thermal_state_observers.insert( - (), - Box::new(move |cx| { - callback(cx); - true - }), - ); - activate(); - subscription - } - - /// Invokes a handler when the system wakes from sleep. - pub fn on_system_wake(&self, mut callback: F) -> Subscription - where - F: 'static + FnMut(&mut App), - { - let (subscription, activate) = self.system_wake_observers.insert( - (), - Box::new(move |cx| { - callback(cx); - true - }), - ); - activate(); - subscription - } - - /// Returns the appearance of the application's windows. - pub fn window_appearance(&self) -> WindowAppearance { - self.platform.window_appearance() - } - - /// Overrides the appearance (light/dark) applied to the app's windows, independent of - /// the OS-wide setting. Pass `None` to clear the override and follow the system again. - /// The current value is reported by [`App::window_appearance`]. - /// - /// On macOS this sets the underlying `NSApplication.appearance`, which controls the - /// native window chrome (the window border and titlebar) of every window. Use this - /// when the app uses a dark theme while the system is in light mode (or vice versa) - /// so the window edges render to match the theme. While an appearance is forced, - /// windows stop tracking system light/dark changes; pass `None` to resume following - /// the system. On other platforms this is a no-op. - pub fn set_window_appearance(&self, appearance: Option) { - self.platform.set_window_appearance(appearance); - } - - /// Returns the window button layout configuration when supported. - pub fn button_layout(&self) -> Option { - self.platform.button_layout() - } - - /// Reads data from the platform clipboard. - pub fn read_from_clipboard(&self) -> Option { - self.platform.read_from_clipboard() - } - - /// Reads data from the platform clipboard, resolving once the contents - /// are available. - /// - /// Prefer this over [`App::read_from_clipboard`] in code that can await: - /// on platforms where clipboard access is asynchronous and - /// permission-gated (e.g. web), the synchronous read always returns - /// `None` while this method performs a real read. - pub fn read_from_clipboard_async( - &self, - ) -> Task, ClipboardReadError>> { - self.platform.read_from_clipboard_async() - } - - /// Sets the text rendering mode for the application. - pub fn set_text_rendering_mode(&mut self, mode: TextRenderingMode) { - self.text_rendering_mode.set(mode); - } - - /// Returns the current text rendering mode for the application. - pub fn text_rendering_mode(&self) -> TextRenderingMode { - self.text_rendering_mode.get() - } - - /// Writes data to the platform clipboard. - pub fn write_to_clipboard(&self, item: ClipboardItem) { - self.platform.write_to_clipboard(item) - } - - /// Reads data from the primary selection buffer. - /// Only available on Linux. - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - pub fn read_from_primary(&self) -> Option { - self.platform.read_from_primary() - } - - /// Writes data to the primary selection buffer. - /// Only available on Linux. - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - pub fn write_to_primary(&self, item: ClipboardItem) { - self.platform.write_to_primary(item) - } - - /// Reads data from macOS's "Find" pasteboard. - /// - /// Used to share the current search string between apps. - /// - /// https://developer.apple.com/documentation/appkit/nspasteboard/name-swift.struct/find - #[cfg(target_os = "macos")] - pub fn read_from_find_pasteboard(&self) -> Option { - self.platform.read_from_find_pasteboard() - } - - /// Writes data to macOS's "Find" pasteboard. - /// - /// Used to share the current search string between apps. - /// - /// https://developer.apple.com/documentation/appkit/nspasteboard/name-swift.struct/find - #[cfg(target_os = "macos")] - pub fn write_to_find_pasteboard(&self, item: ClipboardItem) { - self.platform.write_to_find_pasteboard(item) - } - - /// Writes credentials to the platform keychain. - pub fn write_credentials( - &self, - url: &str, - username: &str, - password: &[u8], - ) -> Task> { - self.platform.write_credentials(url, username, password) - } - - /// Reads credentials from the platform keychain. - pub fn read_credentials(&self, url: &str) -> Task)>>> { - self.platform.read_credentials(url) - } - - /// Deletes credentials from the platform keychain. - pub fn delete_credentials(&self, url: &str) -> Task> { - self.platform.delete_credentials(url) - } - - /// Directs the platform's default browser to open the given URL. - pub fn open_url(&self, url: &str) { - self.platform.open_url(url); - } - - /// Registers the given URL scheme (e.g. `zed` for `zed://` urls) to be - /// opened by the current app. - /// - /// On some platforms (e.g. macOS) you may be able to register URL schemes - /// as part of app distribution, but this method exists to let you register - /// schemes at runtime. - pub fn register_url_scheme(&self, scheme: &str) -> Task> { - self.platform.register_url_scheme(scheme) - } - - /// Sets the application's process-wide identity and user-visible name. - /// - /// The identifier is used for platform identity mechanisms such as the - /// Windows AppUserModelID. The name is used wherever the operating system - /// presents the application to the user. Call this once, early in startup, - /// before opening windows or posting notifications. - pub fn set_app_identity(&self, identifier: &str, name: &str) { - self.platform.set_app_identity(identifier, name); - } - - /// Posts a notification to the operating system's notification center. - /// - /// Posting a notification whose [`SystemNotification::tag`] matches an - /// earlier one replaces that notification where the platform supports it. - /// No-op on platforms without notification support, or when delivery is - /// unavailable (e.g. authorization was denied). - pub fn show_system_notification(&self, notification: SystemNotification) { - self.platform.show_system_notification(notification); - } - - /// Removes the delivered or pending notification with this tag. - /// - /// Best-effort: some platforms cannot retract a notification once shown, - /// in which case it ages out of the notification center on its own. - pub fn dismiss_system_notification(&self, tag: &str) { - self.platform.dismiss_system_notification(tag); - } - - /// Registers the handler invoked when the user activates a system - /// notification, either by clicking its body or one of its action - /// buttons. Subsequent registrations replace the handler. - pub fn on_system_notification_response(&self, mut callback: F) - where - F: 'static + FnMut(SystemNotificationResponse, &mut App), - { - let this = self.this.clone(); - self.platform - .on_system_notification_response(Box::new(move |response| { - if let Some(app) = this.upgrade() { - callback(response, &mut app.borrow_mut()); - } - })); - } - - /// Returns the full pathname of the current app bundle. - /// - /// Returns an error if the app is not being run from a bundle. - pub fn app_path(&self) -> Result { - self.platform.app_path() - } - - /// On Linux, returns the name of the compositor in use. - /// - /// Returns an empty string on other platforms. - pub fn compositor_name(&self) -> &'static str { - self.platform.compositor_name() - } - - /// Returns the file URL of the executable with the specified name in the application bundle - pub fn path_for_auxiliary_executable(&self, name: &str) -> Result { - self.platform.path_for_auxiliary_executable(name) - } - - /// Displays a platform modal for selecting paths. - /// - /// When one or more paths are selected, they'll be relayed asynchronously via the returned oneshot channel. - /// If cancelled, a `None` will be relayed instead. - /// May return an error on Linux if the file picker couldn't be opened. - pub fn prompt_for_paths( - &self, - options: PathPromptOptions, - ) -> oneshot::Receiver>>> { - self.platform.prompt_for_paths(options) - } - - /// Displays a platform modal for selecting a new path where a file can be saved. - /// - /// The provided directory will be used to set the initial location. - /// When a path is selected, it is relayed asynchronously via the returned oneshot channel. - /// If cancelled, a `None` will be relayed instead. - /// May return an error on Linux if the file picker couldn't be opened. - pub fn prompt_for_new_path( - &self, - directory: &Path, - suggested_name: Option<&str>, - ) -> oneshot::Receiver>> { - self.platform.prompt_for_new_path(directory, suggested_name) - } - - /// Reveals the specified path at the platform level, such as in Finder on macOS. - pub fn reveal_path(&self, path: &Path) { - self.platform.reveal_path(path) - } - - /// Opens the specified path with the system's default application. - pub fn open_with_system(&self, path: &Path) { - self.platform.open_with_system(path) - } - - /// Returns whether the user has configured scrollbars to auto-hide at the platform level. - pub fn should_auto_hide_scrollbars(&self) -> bool { - self.platform.should_auto_hide_scrollbars() - } - - /// Restarts the application. - pub fn restart(&mut self) { - self.restart_observers - .clone() - .retain(&(), |observer| observer(self)); - self.platform.restart( - self.restart_path.take(), - std::mem::take(&mut self.restart_arguments), - ) - } - - /// Sets the path to use when restarting the application. - pub fn set_restart_path(&mut self, path: PathBuf) { - self.restart_path = Some(path); - } - - /// Returns the HTTP client for the application. - pub fn http_client(&self) -> Arc { - self.http_client.clone() - } - - /// Sets the HTTP client for the application. - pub fn set_http_client(&mut self, new_client: Arc) { - self.http_client = new_client; - } - - /// Configures when the application should automatically quit. - /// By default, [`QuitMode::Default`] is used. - pub fn set_quit_mode(&mut self, mode: QuitMode) { - self.quit_mode = mode; - } - - /// Returns the SVG renderer used by the application. - pub fn svg_renderer(&self) -> SvgRenderer { - self.svg_renderer.clone() - } - - pub(crate) fn push_effect(&mut self, effect: Effect) { - match &effect { - Effect::Notify { emitter } => { - if !self.pending_notifications.insert(*emitter) { - return; - } - } - Effect::NotifyGlobalObservers { global_type } => { - if !self.pending_global_notifications.insert(*global_type) { - return; - } - } - _ => {} - }; - - self.pending_effects.push_back(effect); - } - - /// Called at the end of [`App::update`] to complete any side effects - /// such as notifying observers, emitting events, etc. Effects can themselves - /// cause effects, so we continue looping until all effects are processed. - fn flush_effects(&mut self) { - loop { - self.release_dropped_entities(); - self.release_dropped_focus_handles(); - if let Some(effect) = self.pending_effects.pop_front() { - match effect { - Effect::Notify { emitter } => { - self.apply_notify_effect(emitter); - } - - Effect::Emit { - emitter, - event_type, - event, - } => self.apply_emit_effect(emitter, event_type, &*event), - - Effect::RefreshWindows => { - self.apply_refresh_effect(); - } - - Effect::NotifyGlobalObservers { global_type } => { - self.apply_notify_global_observers_effect(global_type); - } - - Effect::Defer { callback } => { - self.apply_defer_effect(callback); - } - Effect::EntityCreated { - entity, - tid, - window, - } => { - self.apply_entity_created_effect(entity, tid, window); - } - } - } else { - #[cfg(any(test, feature = "test-support", feature = "bench-support"))] - for window in self - .windows - .values() - .filter_map(|window| { - let window = window.as_deref()?; - window.invalidator.is_dirty().then_some(window.handle) - }) - .collect::>() - { - self.update_window(window, |_, window, cx| window.draw(cx).clear(cx)) - .unwrap(); - } - - if self.pending_effects.is_empty() { - for window in self.windows.values().filter_map(|window| window.as_deref()) { - if window.invalidator.is_dirty() - || window.needs_present.get() - || !window.next_frame_callbacks.borrow().is_empty() - { - window.platform_window.schedule_frame(); - } - } - - self.event_arena.clear(); - break; - } - } - } - } - - /// Repeatedly called during `flush_effects` to release any entities whose - /// reference count has become zero. We invoke any release observers before dropping - /// each entity. - fn release_dropped_entities(&mut self) { - loop { - let dropped = self.entities.take_dropped(); - if dropped.is_empty() { - break; - } - - for (entity_id, mut entity) in dropped { - self.observers.remove(&entity_id); - self.event_listeners.remove(&entity_id); - self.window_invalidators_by_entity.remove(&entity_id); - self.current_window_by_entity.remove(&entity_id); - for release_callback in self.release_listeners.remove(&entity_id) { - release_callback(entity.as_mut(), self); - } - } - } - } - - /// Repeatedly called during `flush_effects` to handle a focused handle being dropped. - fn release_dropped_focus_handles(&mut self) { - self.focus_handles - .clone() - .write() - .retain(|handle_id, focus| { - if focus.ref_count.load(SeqCst) == 0 { - for window_handle in self.windows() { - window_handle - .update(self, |_, window, cx| { - if window.focus == Some(handle_id) { - window.blur(cx); - } - }) - .unwrap(); - } - false - } else { - true - } - }); - } - - fn apply_notify_effect(&mut self, emitter: EntityId) { - self.pending_notifications.remove(&emitter); - - self.observers - .clone() - .retain(&emitter, |handler| handler(self)); - } - - fn apply_emit_effect(&mut self, emitter: EntityId, event_type: TypeId, event: &dyn Any) { - self.event_listeners - .clone() - .retain(&emitter, |(stored_type, handler)| { - if *stored_type == event_type { - handler(event, self) - } else { - true - } - }); - } - - fn apply_refresh_effect(&mut self) { - for window in self.windows.values_mut() { - if let Some(window) = window.as_deref_mut() { - window.refreshing = true; - window.invalidator.set_dirty(true); - } - } - } - - fn apply_notify_global_observers_effect(&mut self, type_id: TypeId) { - self.pending_global_notifications.remove(&type_id); - self.global_observers - .clone() - .retain(&type_id, |observer| observer(self)); - } - - fn apply_defer_effect(&mut self, callback: Box) { - callback(self); - } - - fn apply_entity_created_effect( - &mut self, - entity: AnyEntity, - tid: TypeId, - window: Option, - ) { - // Seed the entity's current window from its creation context so - // `with_window` resolves correctly before the entity has ever been - // rendered. - if let Some(id) = window { - self.current_window_by_entity.insert(entity.entity_id(), id); - } - - self.new_entity_observers.clone().retain(&tid, |observer| { - if let Some(id) = window { - self.update_window_id(id, { - let entity = entity.clone(); - |_, window, cx| (observer)(entity, &mut Some(window), cx) - }) - .expect("All windows should be off the stack when flushing effects"); - } else { - (observer)(entity.clone(), &mut None, self) - } - true - }); - } - - /// Run `f` against the entity's *current* window — the most recently - /// rendered window that referenced the entity, or its creation window if - /// it has yet to be rendered. Returns `None` if the entity has no - /// current window, or if that window has been closed, or if it is - /// already on the update stack. - pub fn with_window( - &mut self, - entity_id: EntityId, - f: impl FnOnce(&mut Window, &mut App) -> R, - ) -> Option { - let window_id = *self.current_window_by_entity.get(&entity_id)?; - self.update_window_id(window_id, |_, window, cx| f(window, cx)) - .ok() - } - - fn ensure_window(&mut self, entity_id: EntityId, window: WindowId) { - self.current_window_by_entity - .entry(entity_id) - .or_insert(window); - } - - pub(crate) fn update_window_id(&mut self, id: WindowId, update: F) -> Result - where - F: FnOnce(AnyView, &mut Window, &mut App) -> T, - { - self.update(|cx| { - let mut window = cx.windows.get_mut(id)?.take()?; - - let root_view = window.root.clone().unwrap(); - - cx.window_update_stack.push(window.handle.id); - let result = update(root_view, &mut window, cx); - fn trail(id: WindowId, window: Box, cx: &mut App) -> Option<()> { - cx.window_update_stack.pop(); - - if window.removed { - cx.end_platform_drag(id); - cx.window_handles.remove(&id); - cx.windows.remove(id); - if let Some(tracked) = cx.tracked_entities.remove(&id) { - for entity_id in tracked { - if let Some(windows) = - cx.window_invalidators_by_entity.get_mut(&entity_id) - { - windows.remove(&id); - } - if cx.current_window_by_entity.get(&entity_id) == Some(&id) { - cx.current_window_by_entity.remove(&entity_id); - } - } - } - - cx.window_closed_observers.clone().retain(&(), |callback| { - callback(cx, id); - true - }); - - let quit_on_empty = match cx.quit_mode { - QuitMode::Explicit => false, - QuitMode::LastWindowClosed => true, - QuitMode::Default => cfg!(not(target_os = "macos")), - }; - - if quit_on_empty && cx.windows.is_empty() { - cx.quit(); - } - } else { - cx.windows.get_mut(id)?.replace(window); - } - Some(()) - } - trail(id, window, cx)?; - - Some(result) - }) - .context("window not found") - } - - /// Creates an `AsyncApp`, which can be cloned and has a static lifetime - /// so it can be held across `await` points. - pub fn to_async(&self) -> AsyncApp { - AsyncApp { - app: self.this.clone(), - background_executor: self.background_executor.clone(), - foreground_executor: self.foreground_executor.clone(), - } - } - - /// Obtains a reference to the executor, which can be used to spawn futures. - pub fn background_executor(&self) -> &BackgroundExecutor { - &self.background_executor - } - - /// Obtains a reference to the executor, which can be used to spawn futures. - pub fn foreground_executor(&self) -> &ForegroundExecutor { - if self.quitting { - panic!("Can't spawn on main thread after on_app_quit") - }; - &self.foreground_executor - } - - /// Returns the foreground work journal for this app's foreground thread. - /// Apps constructed on the same thread share the stream. - #[cfg(feature = "profiler")] - pub fn foreground_journal(&self) -> crate::profiler::journal::ForegroundJournal { - self.foreground_journal.clone() - } - - /// Spawns the future returned by the given function on the main thread. The closure will be invoked - /// with [AsyncApp], which allows the application state to be accessed across await points. - #[track_caller] - pub fn spawn(&self, f: AsyncFn) -> Task - where - AsyncFn: AsyncFnOnce(&mut AsyncApp) -> R + 'static, - R: 'static, - { - if self.quitting { - debug_panic!("Can't spawn on main thread after on_app_quit") - }; - - let mut cx = self.to_async(); - - self.foreground_executor - .spawn(async move { f(&mut cx).await }.boxed_local()) - } - - /// Spawns the future returned by the given function on the main thread with - /// the given priority. The closure will be invoked with [AsyncApp], which - /// allows the application state to be accessed across await points. - pub fn spawn_with_priority(&self, priority: Priority, f: AsyncFn) -> Task - where - AsyncFn: AsyncFnOnce(&mut AsyncApp) -> R + 'static, - R: 'static, - { - if self.quitting { - debug_panic!("Can't spawn on main thread after on_app_quit") - }; - - let mut cx = self.to_async(); - - self.foreground_executor - .spawn_with_priority(priority, async move { f(&mut cx).await }.boxed_local()) - } - - /// Schedules the given function to be run at the end of the current effect cycle, allowing entities - /// that are currently on the stack to be returned to the app. - pub fn defer(&mut self, f: impl FnOnce(&mut App) + 'static) { - self.push_effect(Effect::Defer { - callback: Box::new(f), - }); - } - - /// Accessor for the application's asset source, which is provided when constructing the `App`. - pub fn asset_source(&self) -> &Arc { - &self.asset_source - } - - /// Accessor for the text system. - pub fn text_system(&self) -> &Arc { - &self.text_system - } - - /// Check whether a global of the given type has been assigned. - pub fn has_global(&self) -> bool { - self.globals_by_type.contains_key(&TypeId::of::()) - } - - /// Access the global of the given type. Panics if a global for that type has not been assigned. - #[track_caller] - pub fn global(&self) -> &G { - self.globals_by_type - .get(&TypeId::of::()) - .map(|any_state| any_state.downcast_ref::().unwrap()) - .unwrap_or_else(|| panic!("no state of type {} exists", type_name::())) - } - - /// Access the global of the given type if a value has been assigned. - pub fn try_global(&self) -> Option<&G> { - self.globals_by_type - .get(&TypeId::of::()) - .map(|any_state| any_state.downcast_ref::().unwrap()) - } - - /// Access the global of the given type mutably. Panics if a global for that type has not been assigned. - #[track_caller] - pub fn global_mut(&mut self) -> &mut G { - let global_type = TypeId::of::(); - self.push_effect(Effect::NotifyGlobalObservers { global_type }); - self.globals_by_type - .get_mut(&global_type) - .and_then(|any_state| any_state.downcast_mut::()) - .unwrap_or_else(|| panic!("no state of type {} exists", type_name::())) - } - - /// Access the global of the given type mutably. A default value is assigned if a global of this type has not - /// yet been assigned. - pub fn default_global(&mut self) -> &mut G { - let global_type = TypeId::of::(); - self.push_effect(Effect::NotifyGlobalObservers { global_type }); - self.globals_by_type - .entry(global_type) - .or_insert_with(|| Box::::default()) - .downcast_mut::() - .unwrap() - } - - /// Sets the value of the global of the given type. - pub fn set_global(&mut self, global: G) { - let global_type = TypeId::of::(); - self.push_effect(Effect::NotifyGlobalObservers { global_type }); - self.globals_by_type.insert(global_type, Box::new(global)); - } - - /// Clear all stored globals. Does not notify global observers. - #[cfg(any(test, feature = "test-support"))] - pub fn clear_globals(&mut self) { - self.globals_by_type.drain(); - } - - /// Remove the global of the given type from the app context. Does not notify global observers. - pub fn remove_global(&mut self) -> G { - let global_type = TypeId::of::(); - self.push_effect(Effect::NotifyGlobalObservers { global_type }); - *self - .globals_by_type - .remove(&global_type) - .unwrap_or_else(|| panic!("no global added for {}", type_name::())) - .downcast() - .unwrap() - } - - /// Register a callback to be invoked when a global of the given type is updated. - pub fn observe_global( - &mut self, - mut f: impl FnMut(&mut Self) + 'static, - ) -> Subscription { - let (subscription, activate) = self.global_observers.insert( - TypeId::of::(), - Box::new(move |cx| { - f(cx); - true - }), - ); - self.defer(move |_| activate()); - subscription - } - - /// Move the global of the given type to the stack. - #[track_caller] - pub(crate) fn lease_global(&mut self) -> GlobalLease { - GlobalLease::new( - self.globals_by_type - .remove(&TypeId::of::()) - .with_context(|| format!("no global registered of type {}", type_name::())) - .unwrap(), - ) - } - - /// Restore the global of the given type after it is moved to the stack. - pub(crate) fn end_global_lease(&mut self, lease: GlobalLease) { - let global_type = TypeId::of::(); - - self.push_effect(Effect::NotifyGlobalObservers { global_type }); - self.globals_by_type.insert(global_type, lease.global); - } - - pub(crate) fn new_entity_observer( - &self, - key: TypeId, - value: NewEntityListener, - ) -> Subscription { - let (subscription, activate) = self.new_entity_observers.insert(key, value); - activate(); - subscription - } - - /// Arrange for the given function to be invoked whenever a view of the specified type is created. - /// The function will be passed a mutable reference to the view along with an appropriate context. - pub fn observe_new( - &self, - on_new: impl 'static + Fn(&mut T, Option<&mut Window>, &mut Context), - ) -> Subscription { - self.new_entity_observer( - TypeId::of::(), - Box::new( - move |any_entity: AnyEntity, window: &mut Option<&mut Window>, cx: &mut App| { - any_entity - .downcast::() - .unwrap() - .update(cx, |entity_state, cx| { - on_new(entity_state, window.as_deref_mut(), cx) - }) - }, - ), - ) - } - - /// Observe the release of a entity. The callback is invoked after the entity - /// has no more strong references but before it has been dropped. - pub fn observe_release( - &self, - handle: &Entity, - on_release: impl FnOnce(&mut T, &mut App) + 'static, - ) -> Subscription - where - T: 'static, - { - let (subscription, activate) = self.release_listeners.insert( - handle.entity_id(), - Box::new(move |entity, cx| { - let entity = entity.downcast_mut().expect("invalid entity type"); - on_release(entity, cx) - }), - ); - activate(); - subscription - } - - /// Observe the release of a entity. The callback is invoked after the entity - /// has no more strong references but before it has been dropped. - pub fn observe_release_in( - &self, - handle: &Entity, - window: &Window, - on_release: impl FnOnce(&mut T, &mut Window, &mut App) + 'static, - ) -> Subscription - where - T: 'static, - { - let window_handle = window.handle; - self.observe_release(handle, move |entity, cx| { - let _ = window_handle.update(cx, |_, window, cx| on_release(entity, window, cx)); - }) - } - - /// Register a callback to be invoked when a keystroke is received by the application - /// in any window. Note that this fires after all other action and event mechanisms have resolved - /// and that this API will not be invoked if the event's propagation is stopped. - pub fn observe_keystrokes( - &mut self, - mut f: impl FnMut(&KeystrokeEvent, &mut Window, &mut App) + 'static, - ) -> Subscription { - fn inner( - keystroke_observers: &SubscriberSet<(), KeystrokeObserver>, - handler: KeystrokeObserver, - ) -> Subscription { - let (subscription, activate) = keystroke_observers.insert((), handler); - activate(); - subscription - } - - inner( - &self.keystroke_observers, - Box::new(move |event, window, cx| { - f(event, window, cx); - true - }), - ) - } - - /// Register a callback to be invoked when a keystroke is received by the application - /// in any window. Note that this fires _before_ all other action and event mechanisms have resolved - /// unlike [`App::observe_keystrokes`] which fires after. This means that `cx.stop_propagation` calls - /// within interceptors will prevent action dispatch - pub fn intercept_keystrokes( - &mut self, - mut f: impl FnMut(&KeystrokeEvent, &mut Window, &mut App) + 'static, - ) -> Subscription { - fn inner( - keystroke_interceptors: &SubscriberSet<(), KeystrokeObserver>, - handler: KeystrokeObserver, - ) -> Subscription { - let (subscription, activate) = keystroke_interceptors.insert((), handler); - activate(); - subscription - } - - inner( - &self.keystroke_interceptors, - Box::new(move |event, window, cx| { - f(event, window, cx); - true - }), - ) - } - - /// Register key bindings. - pub fn bind_keys(&mut self, bindings: impl IntoIterator) { - self.keymap.borrow_mut().add_bindings(bindings); - self.pending_effects.push_back(Effect::RefreshWindows); - } - - /// Clear all key bindings in the app. - pub fn clear_key_bindings(&mut self) { - self.keymap.borrow_mut().clear(); - self.pending_effects.push_back(Effect::RefreshWindows); - } - - /// Get all key bindings in the app. - pub fn key_bindings(&self) -> Rc> { - self.keymap.clone() - } - - /// Register a global handler for actions invoked via the keyboard. These handlers are run at - /// the end of the bubble phase for actions, and so will only be invoked if there are no other - /// handlers or if they called `cx.propagate()`. - pub fn on_action( - &mut self, - listener: impl Fn(&A, &mut Self) + 'static, - ) -> &mut Self { - self.global_action_listeners - .entry(TypeId::of::()) - .or_default() - .push(Rc::new(move |action, phase, cx| { - if phase == DispatchPhase::Bubble { - let action = action.downcast_ref().unwrap(); - listener(action, cx) - } - })); - self - } - - /// Event handlers propagate events by default. Call this method to stop dispatching to - /// event handlers with a lower z-index (mouse) or higher in the tree (keyboard). This is - /// the opposite of [`Self::propagate`]. It's also possible to cancel a call to [`Self::propagate`] by - /// calling this method before effects are flushed. - pub fn stop_propagation(&mut self) { - self.propagate_event = false; - } - - /// Action handlers stop propagation by default during the bubble phase of action dispatch - /// dispatching to action handlers higher in the element tree. This is the opposite of - /// [`Self::stop_propagation`]. It's also possible to cancel a call to [`Self::stop_propagation`] by calling - /// this method before effects are flushed. - pub fn propagate(&mut self) { - self.propagate_event = true; - } - - /// Build an action from some arbitrary data, typically a keymap entry. - pub fn build_action( - &self, - name: &str, - data: Option, - ) -> std::result::Result, ActionBuildError> { - self.actions.build_action(name, data) - } - - /// Get all action names that have been registered. Note that registration only allows for - /// actions to be built dynamically, and is unrelated to binding actions in the element tree. - pub fn all_action_names(&self) -> &[&'static str] { - self.actions.all_action_names() - } - - /// Returns key bindings that invoke the given action on the currently focused element, without - /// checking context. Bindings are returned in the order they were added. For display, the last - /// binding should take precedence. - pub fn all_bindings_for_input(&self, input: &[Keystroke]) -> Vec { - RefCell::borrow(&self.keymap).all_bindings_for_input(input) - } - - /// Get all non-internal actions that have been registered, along with their schemas. - pub fn action_schemas( - &self, - generator: &mut schemars::SchemaGenerator, - ) -> Vec<(&'static str, Option)> { - self.actions.action_schemas(generator) - } - - /// Get the schema for a specific action by name. - /// Returns `None` if the action is not found. - /// Returns `Some(None)` if the action exists but has no schema. - /// Returns `Some(Some(schema))` if the action exists and has a schema. - pub fn action_schema_by_name( - &self, - name: &str, - generator: &mut schemars::SchemaGenerator, - ) -> Option> { - self.actions.action_schema_by_name(name, generator) - } - - /// Get a map from a deprecated action name to the canonical name. - pub fn deprecated_actions_to_preferred_actions(&self) -> &HashMap<&'static str, &'static str> { - self.actions.deprecated_aliases() - } - - /// Get a map from an action name to the deprecation messages. - pub fn action_deprecation_messages(&self) -> &HashMap<&'static str, &'static str> { - self.actions.deprecation_messages() - } - - /// Get a map from an action name to the documentation. - pub fn action_documentation(&self) -> &HashMap<&'static str, &'static str> { - self.actions.documentation() - } - - /// Register a callback to be invoked when the application is about to quit. - /// It is not possible to cancel the quit event at this point. - pub fn on_app_quit( - &self, - mut on_quit: impl FnMut(&mut App) -> Fut + 'static, - ) -> Subscription - where - Fut: 'static + Future, - { - let (subscription, activate) = self.quit_observers.insert( - (), - Box::new(move |cx| { - let future = on_quit(cx); - future.boxed_local() - }), - ); - activate(); - subscription - } - - /// Register a callback to be invoked when the application is about to restart. - /// - /// These callbacks are called before any `on_app_quit` callbacks. - pub fn on_app_restart(&self, mut on_restart: impl 'static + FnMut(&mut App)) -> Subscription { - let (subscription, activate) = self.restart_observers.insert( - (), - Box::new(move |cx| { - on_restart(cx); - true - }), - ); - activate(); - subscription - } - - /// Register a callback to be invoked when a window is closed - /// The window is no longer accessible at the point this callback is invoked. - pub fn on_window_closed( - &self, - mut on_closed: impl FnMut(&mut App, WindowId) + 'static, - ) -> Subscription { - let (subscription, activate) = self.window_closed_observers.insert((), Box::new(on_closed)); - activate(); - subscription - } - - pub(crate) fn clear_pending_keystrokes(&mut self) { - for window in self.windows() { - window - .update(self, |_, window, cx| { - window.clear_pending_keystrokes(cx); - }) - .ok(); - } - } - - /// Checks if the given action is bound in the current context, as defined by the app's current focus, - /// the bindings in the element tree, and any global action listeners. - pub fn is_action_available(&mut self, action: &dyn Action) -> bool { - let mut action_available = false; - if let Some(window) = self.active_window() - && let Ok(window_action_available) = - window.update(self, |_, window, cx| window.is_action_available(action, cx)) - { - action_available = window_action_available; - } - - action_available - || self - .global_action_listeners - .contains_key(&action.as_any().type_id()) - } - - /// Sets the menu bar for this application. This will replace any existing menu bar. - pub fn set_menus(&self, menus: impl IntoIterator) { - let menus: Vec = menus.into_iter().collect(); - self.platform.set_menus(menus, &self.keymap.borrow()); - } - - /// Gets the menu bar for this application. - pub fn get_menus(&self) -> Option> { - self.platform.get_menus() - } - - /// Sets the right click menu for the app icon in the dock - pub fn set_dock_menu(&self, menus: Vec) { - self.platform.set_dock_menu(menus, &self.keymap.borrow()) - } - - /// Performs the action associated with the given dock menu item, only used on Windows for now. - pub fn perform_dock_menu_action(&self, action: usize) { - self.platform.perform_dock_menu_action(action); - } - - /// Adds given path to the bottom of the list of recent paths for the application. - /// The list is usually shown on the application icon's context menu in the dock, - /// and allows to open the recent files via that context menu. - /// If the path is already in the list, it will be moved to the bottom of the list. - pub fn add_recent_document(&self, path: &Path) { - self.platform.add_recent_document(path); - } - - /// Updates the jump list with the updated list of recent paths for the application, only used on Windows for now. - /// Note that this also sets the dock menu on Windows. - pub fn update_jump_list( - &self, - menus: Vec, - entries: Vec>, - ) -> Task>> { - self.platform.update_jump_list(menus, entries) - } - - /// Dispatch an action to the currently active window or global action handler - /// See [`crate::Action`] for more information on how actions work - pub fn dispatch_action(&mut self, action: &dyn Action) { - if let Some(active_window) = self.active_window() { - active_window - .update(self, |_, window, cx| { - window.dispatch_action(action.boxed_clone(), cx) - }) - .log_err(); - } else { - self.dispatch_global_action(action); - } - } - - fn dispatch_global_action(&mut self, action: &dyn Action) { - self.propagate_event = true; - - if let Some(mut global_listeners) = self - .global_action_listeners - .remove(&action.as_any().type_id()) - { - for listener in &global_listeners { - listener(action.as_any(), DispatchPhase::Capture, self); - if !self.propagate_event { - break; - } - } - - global_listeners.extend( - self.global_action_listeners - .remove(&action.as_any().type_id()) - .unwrap_or_default(), - ); - - self.global_action_listeners - .insert(action.as_any().type_id(), global_listeners); - } - - if self.propagate_event - && let Some(mut global_listeners) = self - .global_action_listeners - .remove(&action.as_any().type_id()) - { - for listener in global_listeners.iter().rev() { - listener(action.as_any(), DispatchPhase::Bubble, self); - if !self.propagate_event { - break; - } - } - - global_listeners.extend( - self.global_action_listeners - .remove(&action.as_any().type_id()) - .unwrap_or_default(), - ); - - self.global_action_listeners - .insert(action.as_any().type_id(), global_listeners); - } - } - - /// Is there currently something being dragged? - pub fn has_active_drag(&self) -> bool { - self.active_drag.is_some() - } - - /// Gets the cursor style of the currently active drag operation. - pub fn active_drag_cursor_style(&self) -> Option { - self.active_drag.as_ref().and_then(|drag| drag.cursor_style) - } - - /// Stops active drag and clears any related effects. - pub fn stop_active_drag(&mut self, window: &mut Window) -> bool { - if self.active_drag.is_some() { - self.active_drag = None; - if self.platform_owned_drag.as_ref().is_some_and(|drag| { - drag.source_window == window.window_handle().window_id() - && matches!(&drag.state, PlatformOwnedDragState::RestoredInSourceWindow) - }) { - self.platform_owned_drag = None; - } - window.refresh(); - true - } else { - false - } - } - - pub(crate) fn hand_active_drag_to_platform(&mut self, source_window: WindowId) -> bool { - let Some(drag) = self.active_drag.take() else { - return false; - }; - self.platform_owned_drag = Some(PlatformOwnedDrag { - source_window, - state: PlatformOwnedDragState::Suspended(drag), - }); - true - } - - pub(crate) fn restore_platform_drag(&mut self, source_window: WindowId) -> bool { - let Some(platform_drag) = self - .platform_owned_drag - .as_mut() - .filter(|drag| drag.source_window == source_window) - else { - return false; - }; - let state = std::mem::replace( - &mut platform_drag.state, - PlatformOwnedDragState::RestoredInSourceWindow, - ); - let PlatformOwnedDragState::Suspended(drag) = state else { - return false; - }; - self.active_drag = Some(drag); - true - } - - pub(crate) fn hand_restored_drag_to_platform(&mut self, source_window: WindowId) -> bool { - let Some(platform_drag) = self.platform_owned_drag.as_mut().filter(|drag| { - drag.source_window == source_window - && matches!(&drag.state, PlatformOwnedDragState::RestoredInSourceWindow) - }) else { - return false; - }; - let Some(drag) = self.active_drag.take() else { - return false; - }; - platform_drag.state = PlatformOwnedDragState::Suspended(drag); - true - } - - pub(crate) fn end_platform_drag(&mut self, source_window: WindowId) -> bool { - if !self - .platform_owned_drag - .as_ref() - .is_some_and(|drag| drag.source_window == source_window) - { - return false; - } - self.platform_owned_drag = None; - self.active_drag = None; - true - } - - /// Sets the cursor style for the currently active drag operation. - pub fn set_active_drag_cursor_style( - &mut self, - cursor_style: CursorStyle, - window: &mut Window, - ) -> bool { - if let Some(ref mut drag) = self.active_drag { - drag.cursor_style = Some(cursor_style); - window.refresh(); - true - } else { - false - } - } - - /// Set the prompt renderer for GPUI. This will replace the default or platform specific - /// prompts with this custom implementation. - pub fn set_prompt_builder( - &mut self, - renderer: impl Fn( - PromptLevel, - &str, - Option<&str>, - &[PromptButton], - PromptHandle, - &mut Window, - &mut App, - ) -> RenderablePromptHandle - + 'static, - ) { - self.prompt_builder = Some(PromptBuilder::Custom(Box::new(renderer))); - } - - /// Reset the prompt builder to the default implementation. - pub fn reset_prompt_builder(&mut self) { - self.prompt_builder = Some(PromptBuilder::Default); - } - - /// Remove an asset from GPUI's cache - pub fn remove_asset(&mut self, source: &A::Source) { - let asset_id = (TypeId::of::(), hash(source)); - self.loading_assets.remove(&asset_id); - } - - /// Check whether an asset is present in GPUI's cache (loading or loaded), - /// without fetching it. - #[cfg(any(test, feature = "test-support"))] - pub fn has_asset(&self, source: &A::Source) -> bool { - let asset_id = (TypeId::of::(), hash(source)); - self.loading_assets.contains_key(&asset_id) - } - - /// Asynchronously load an asset, if the asset hasn't finished loading this will return None. - /// - /// Note that the multiple calls to this method will only result in one `Asset::load` call at a - /// time, and the results of this call will be cached - pub fn fetch_asset(&mut self, source: &A::Source) -> (Shared>, bool) { - let asset_id = (TypeId::of::(), hash(source)); - let mut is_first = false; - let task = self - .loading_assets - .remove(&asset_id) - .map(|boxed_task| *boxed_task.downcast::>>().unwrap()) - .unwrap_or_else(|| { - is_first = true; - let future = A::load(source.clone(), self); - - self.background_executor().spawn(future).shared() - }); - - self.loading_assets.insert(asset_id, Box::new(task.clone())); - - (task, is_first) - } - - /// Obtain a new [`FocusHandle`], which allows you to track and manipulate the keyboard focus - /// for elements rendered within this window. - #[track_caller] - pub fn focus_handle(&self) -> FocusHandle { - FocusHandle::new(&self.focus_handles) - } - - /// Tell GPUI that an entity has changed and observers of it should be notified. - pub fn notify(&mut self, entity_id: EntityId) { - let window_invalidators = mem::take( - self.window_invalidators_by_entity - .entry(entity_id) - .or_default(), - ); - - // `window_invalidators_by_entity` is monotonic, so an entry alone - // doesn't mean the window is currently rendering the entity. Filter - // through `tracked_entities` to keep invalidation tight to windows - // that actually display this entity right now. - let live_invalidators: SmallVec<[WindowInvalidator; 2]> = window_invalidators - .iter() - .filter(|(window_id, _)| { - self.tracked_entities - .get(window_id) - .is_some_and(|set| set.contains(&entity_id)) - }) - .map(|(_, invalidator)| invalidator.clone()) - .collect(); - - if live_invalidators.is_empty() { - if self.pending_notifications.insert(entity_id) { - self.pending_effects - .push_back(Effect::Notify { emitter: entity_id }); - } - } else { - for invalidator in &live_invalidators { - invalidator.invalidate_view(entity_id, self); - } - } - - self.window_invalidators_by_entity - .insert(entity_id, window_invalidators); - } - - /// Returns the name for this [`App`]. - #[cfg(any(test, feature = "test-support", debug_assertions))] - pub fn get_name(&self) -> Option<&'static str> { - self.name - } - - /// Returns `true` if the platform file picker supports selecting a mix of files and directories. - pub fn can_select_mixed_files_and_dirs(&self) -> bool { - self.platform.can_select_mixed_files_and_dirs() - } - - /// Removes an image from the sprite atlas on all windows. - /// - /// If the current window is being updated, it will be removed from `App.windows`, you can use `current_window` to specify the current window. - /// This is a no-op if the image is not in the sprite atlas. - pub fn drop_image(&mut self, image: Arc, current_window: Option<&mut Window>) { - // remove the texture from all other windows - for window in self.windows.values_mut().flatten() { - _ = window.drop_image(image.clone()); - } - - // remove the texture from the current window - if let Some(window) = current_window { - _ = window.drop_image(image); - } - } - - /// Sets the renderer for the inspector. - #[cfg(any(feature = "inspector", debug_assertions))] - pub fn set_inspector_renderer(&mut self, f: crate::InspectorRenderer) { - self.inspector_renderer = Some(f); - } - - /// Registers a renderer specific to an inspector state. - #[cfg(any(feature = "inspector", debug_assertions))] - pub fn register_inspector_element( - &mut self, - f: impl 'static + Fn(crate::InspectorElementId, &T, &mut Window, &mut App) -> R, - ) { - self.inspector_element_registry.register(f); - } - - /// Initializes gpui's default colors for the application. - /// - /// These colors can be accessed through `cx.default_colors()`. - pub fn init_colors(&mut self) { - self.set_global(GlobalColors(Arc::new(Colors::default()))); - } -} - -impl AppContext for App { - /// Builds an entity that is owned by the application. - /// - /// The given function will be invoked with a [`Context`] and must return an object representing the entity. An - /// [`Entity`] handle will be returned, which can be used to access the entity in a context. - fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity { - self.update(|cx| { - let slot = cx.entities.reserve(); - let handle = slot.clone(); - let entity = build_entity(&mut Context::new_context(cx, slot.downgrade())); - - cx.push_effect(Effect::EntityCreated { - entity: handle.into_any(), - tid: TypeId::of::(), - window: cx.window_update_stack.last().cloned(), - }); - - cx.entities.insert(slot, entity) - }) - } - - fn reserve_entity(&mut self) -> Reservation { - Reservation(self.entities.reserve()) - } - - fn insert_entity( - &mut self, - reservation: Reservation, - build_entity: impl FnOnce(&mut Context) -> T, - ) -> Entity { - self.update(|cx| { - let slot = reservation.0; - let entity = build_entity(&mut Context::new_context(cx, slot.downgrade())); - cx.entities.insert(slot, entity) - }) - } - - /// Updates the entity referenced by the given handle. The function is passed a mutable reference to the - /// entity along with a `Context` for the entity. - fn update_entity( - &mut self, - handle: &Entity, - update: impl FnOnce(&mut T, &mut Context) -> R, - ) -> R { - self.update(|cx| { - let mut entity = cx.entities.lease(handle); - let result = update( - &mut entity, - &mut Context::new_context(cx, handle.downgrade()), - ); - cx.entities.end_lease(entity); - result - }) - } - - fn as_mut<'a, T>(&'a mut self, handle: &Entity) -> GpuiBorrow<'a, T> - where - T: 'static, - { - GpuiBorrow::new(handle.clone(), self) - } - - fn read_entity(&self, handle: &Entity, read: impl FnOnce(&T, &App) -> R) -> R - where - T: 'static, - { - let entity = self.entities.read(handle); - read(entity, self) - } - - fn update_window(&mut self, handle: AnyWindowHandle, update: F) -> Result - where - F: FnOnce(AnyView, &mut Window, &mut App) -> T, - { - self.update_window_id(handle.id, update) - } - - fn with_window( - &mut self, - entity_id: EntityId, - f: impl FnOnce(&mut Window, &mut App) -> R, - ) -> Option { - App::with_window(self, entity_id, f) - } - - fn read_window( - &self, - window: &WindowHandle, - read: impl FnOnce(Entity, &App) -> R, - ) -> Result - where - T: 'static, - { - let window = self - .windows - .get(window.id) - .context("window not found")? - .as_deref() - .expect("attempted to read a window that is already on the stack"); - - let root_view = window.root.clone().unwrap(); - let view = root_view - .downcast::() - .map_err(|_| anyhow!("root view's type has changed"))?; - - Ok(read(view, self)) - } - - fn background_spawn(&self, future: impl Future + Send + 'static) -> Task - where - R: Send + 'static, - { - self.background_executor.spawn(future) - } - - fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R - where - G: Global, - { - let mut g = self.global::(); - callback(g, self) - } -} - -/// These effects are processed at the end of each application update cycle. -pub(crate) enum Effect { - Notify { - emitter: EntityId, - }, - Emit { - emitter: EntityId, - event_type: TypeId, - event: ArenaBox, - }, - RefreshWindows, - NotifyGlobalObservers { - global_type: TypeId, - }, - Defer { - callback: Box, - }, - EntityCreated { - entity: AnyEntity, - tid: TypeId, - window: Option, - }, -} - -impl std::fmt::Debug for Effect { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Effect::Notify { emitter } => write!(f, "Notify({})", emitter), - Effect::Emit { emitter, .. } => write!(f, "Emit({:?})", emitter), - Effect::RefreshWindows => write!(f, "RefreshWindows"), - Effect::NotifyGlobalObservers { global_type } => { - write!(f, "NotifyGlobalObservers({:?})", global_type) - } - Effect::Defer { .. } => write!(f, "Defer(..)"), - Effect::EntityCreated { entity, .. } => write!(f, "EntityCreated({:?})", entity), - } - } -} - -/// Wraps a global variable value during `update_global` while the value has been moved to the stack. -pub(crate) struct GlobalLease { - global: Box, - global_type: PhantomData, -} - -impl GlobalLease { - fn new(global: Box) -> Self { - GlobalLease { - global, - global_type: PhantomData, - } - } -} - -impl Deref for GlobalLease { - type Target = G; - - fn deref(&self) -> &Self::Target { - self.global.downcast_ref().unwrap() - } -} - -impl DerefMut for GlobalLease { - fn deref_mut(&mut self) -> &mut Self::Target { - self.global.downcast_mut().unwrap() - } -} - -/// Contains state associated with an active drag operation, started by dragging an element -/// within the window or by dragging into the app from the underlying platform. -pub struct AnyDrag { - /// The view used to render this drag - pub view: AnyView, - - /// The value of the dragged item, to be dropped - pub value: Arc, - - /// This is used to render the dragged item in the same place - /// on the original element that the drag was initiated - pub cursor_offset: Point, - - /// The cursor style to use while dragging - pub cursor_style: Option, - - /// Resolves the payload to offer the platform if the drag leaves the window. - /// Invoked at most once per drag gesture, at promotion time. - pub external_payload_source: Option, -} - -/// Lazily resolves the payload handed to the platform when an internal drag is -/// promoted to a native drag session. -pub type ExternalDragPayloadSource = - Box Option + 'static>; - -/// Contains state associated with a tooltip. You'll only need this struct if you're implementing -/// tooltip behavior on a custom element. Otherwise, use [Div::tooltip](crate::Interactivity::tooltip). -#[derive(Clone)] -pub struct AnyTooltip { - /// The view used to display the tooltip - pub view: AnyView, - - /// The absolute position of the mouse when the tooltip was deployed. - pub mouse_position: Point, - - /// Given the bounds of the tooltip, checks whether the tooltip should still be visible and - /// updates its state accordingly. This is needed atop the hovered element's mouse move handler - /// to handle the case where the element is not painted (e.g. via use of `visible_on_hover`). - pub check_visible_and_update: Rc, &mut Window, &mut App) -> bool>, -} - -/// A keystroke event, and potentially the associated action -#[derive(Debug)] -pub struct KeystrokeEvent { - /// The keystroke that occurred - pub keystroke: Keystroke, - - /// The action that was resolved for the keystroke, if any - pub action: Option>, - - /// The context stack at the time - pub context_stack: Vec, -} - -struct NullHttpClient; - -impl HttpClient for NullHttpClient { - fn send( - &self, - _req: http_client::Request, - ) -> futures::future::BoxFuture< - 'static, - anyhow::Result>, - > { - async move { - anyhow::bail!("No HttpClient available"); - } - .boxed() - } - - fn user_agent(&self) -> Option<&http_client::http::HeaderValue> { - None - } - - fn proxy(&self) -> Option<&Url> { - None - } -} - -/// A mutable reference to an entity owned by GPUI -pub struct GpuiBorrow<'a, T> { - inner: Option>, - app: &'a mut App, -} - -impl<'a, T: 'static> GpuiBorrow<'a, T> { - fn new(inner: Entity, app: &'a mut App) -> Self { - app.start_update(); - let lease = app.entities.lease(&inner); - Self { - inner: Some(lease), - app, - } - } -} - -impl<'a, T: 'static> std::borrow::Borrow for GpuiBorrow<'a, T> { - fn borrow(&self) -> &T { - self.inner.as_ref().unwrap().borrow() - } -} - -impl<'a, T: 'static> std::borrow::BorrowMut for GpuiBorrow<'a, T> { - fn borrow_mut(&mut self) -> &mut T { - self.inner.as_mut().unwrap().borrow_mut() - } -} - -impl<'a, T: 'static> std::ops::Deref for GpuiBorrow<'a, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - self.inner.as_ref().unwrap() - } -} - -impl<'a, T: 'static> std::ops::DerefMut for GpuiBorrow<'a, T> { - fn deref_mut(&mut self) -> &mut T { - self.inner.as_mut().unwrap() - } -} - -impl<'a, T> Drop for GpuiBorrow<'a, T> { - fn drop(&mut self) { - let lease = self.inner.take().unwrap(); - self.app.notify(lease.id); - self.app.entities.end_lease(lease); - self.app.finish_update(); - } -} - -#[cfg(test)] -mod test { - use std::{ - cell::{Cell, RefCell}, - ffi::OsString, - path::PathBuf, - rc::Rc, - }; - - #[cfg(unix)] - use std::os::unix::ffi::OsStringExt; - - use crate::{AppContext, Context, Empty, IntoElement, Render, TestAppContext, Window}; - - struct RenderCounter(Rc>); - - impl Render for RenderCounter { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - self.0.set(self.0.get() + 1); - Empty - } - } - - #[gpui::test] - fn async_app_refresh_flushes_refresh_effect(cx: &mut TestAppContext) { - let render_count = Rc::new(Cell::new(0)); - - let _window = cx.add_window({ - let render_count = render_count.clone(); - move |_, _| RenderCounter(render_count) - }); - - cx.run_until_parked(); - let render_count_before_refresh = render_count.get(); - - cx.to_async().refresh(); - - assert_eq!(render_count.get(), render_count_before_refresh + 1); - } - - #[test] - fn test_gpui_borrow() { - let cx = TestAppContext::single(); - let observation_count = Rc::new(RefCell::new(0)); - - let state = cx.update(|cx| { - let state = cx.new(|_| false); - cx.observe(&state, { - let observation_count = observation_count.clone(); - move |_, _| { - let mut count = observation_count.borrow_mut(); - *count += 1; - } - }) - .detach(); - - state - }); - - cx.update(|cx| { - // Calling this like this so that we don't clobber the borrow_mut above - *std::borrow::BorrowMut::borrow_mut(&mut state.as_mut(cx)) = true; - }); - - cx.update(|cx| { - state.write(cx, false); - }); - - assert_eq!(*observation_count.borrow(), 2); - } - - #[gpui::test] - async fn test_restart_preserves_path_and_arguments(cx: &mut TestAppContext) { - #[cfg(unix)] - let user_data_dir = OsString::from_vec(b"/tmp/zed data/\xff".to_vec()); - #[cfg(not(unix))] - let user_data_dir = OsString::from("C:\\zed data"); - let arguments = vec![OsString::from("--user-data-dir"), user_data_dir]; - let restart_path = PathBuf::from("updated-zed"); - let _application = - super::Application(cx.app.clone()).with_restart_arguments(arguments.clone()); - let restart = cx.expect_restart(); - - cx.update(|cx| { - cx.set_restart_path(restart_path.clone()); - cx.restart(); - }); - - let (path, restart_arguments) = restart.await.expect("restart was not requested"); - assert_eq!(path, Some(restart_path)); - assert_eq!(restart_arguments, arguments); - } -} diff --git a/crates/gpui_pre/src/app/async_context.rs b/crates/gpui_pre/src/app/async_context.rs deleted file mode 100644 index 48c084a..0000000 --- a/crates/gpui_pre/src/app/async_context.rs +++ /dev/null @@ -1,537 +0,0 @@ -use crate::{ - AnyView, AnyWindowHandle, App, AppCell, AppContext, BackgroundExecutor, BorrowAppContext, - Entity, EntityId, EventEmitter, Focusable, ForegroundExecutor, Global, GpuiBorrow, - PromptButton, PromptLevel, Render, Reservation, Result, Subscription, Task, VisualContext, - Window, WindowHandle, -}; -use anyhow::{Context as _, bail}; -use derive_more::{Deref, DerefMut}; -use futures::channel::oneshot; -use futures::future::FutureExt; -use std::{future::Future, rc::Weak}; - -use super::{Context, WeakEntity}; - -/// An async-friendly version of [App] with a static lifetime so it can be held across `await` points in async code. -/// You're provided with an instance when calling [App::spawn], and you can also create one with [App::to_async]. -/// -/// Internally, this holds a weak reference to an `App`. Methods will panic if the app has been dropped, -/// but this should not happen in practice when using foreground tasks spawned via `cx.spawn()`, -/// as the executor checks if the app is alive before running each task. -#[derive(Clone)] -pub struct AsyncApp { - pub(crate) app: Weak, - pub(crate) background_executor: BackgroundExecutor, - pub(crate) foreground_executor: ForegroundExecutor, -} - -impl AsyncApp { - fn app(&self) -> std::rc::Rc { - self.app - .upgrade() - .expect("app was released before async operation completed") - } -} - -impl AppContext for AsyncApp { - fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity { - let app = self.app(); - let mut app = app.borrow_mut(); - app.new(build_entity) - } - - fn reserve_entity(&mut self) -> Reservation { - let app = self.app(); - let mut app = app.borrow_mut(); - app.reserve_entity() - } - - fn insert_entity( - &mut self, - reservation: Reservation, - build_entity: impl FnOnce(&mut Context) -> T, - ) -> Entity { - let app = self.app(); - let mut app = app.borrow_mut(); - app.insert_entity(reservation, build_entity) - } - - fn update_entity( - &mut self, - handle: &Entity, - update: impl FnOnce(&mut T, &mut Context) -> R, - ) -> R { - let app = self.app(); - let mut app = app.borrow_mut(); - app.update_entity(handle, update) - } - - fn as_mut<'a, T>(&'a mut self, _handle: &Entity) -> GpuiBorrow<'a, T> - where - T: 'static, - { - panic!("Cannot as_mut with an async context. Try calling update() first") - } - - fn read_entity(&self, handle: &Entity, callback: impl FnOnce(&T, &App) -> R) -> R - where - T: 'static, - { - let app = self.app(); - let lock = app.borrow(); - lock.read_entity(handle, callback) - } - - fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result - where - F: FnOnce(AnyView, &mut Window, &mut App) -> T, - { - let app = self.app.upgrade().context("app was released")?; - let mut lock = app.try_borrow_mut()?; - if lock.quitting { - bail!("app is quitting"); - } - lock.update_window(window, f) - } - - fn with_window( - &mut self, - entity_id: EntityId, - f: impl FnOnce(&mut Window, &mut App) -> R, - ) -> Option { - let app = self.app.upgrade()?; - let mut lock = app.try_borrow_mut().ok()?; - if lock.quitting { - return None; - } - lock.with_window(entity_id, f) - } - - fn read_window( - &self, - window: &WindowHandle, - read: impl FnOnce(Entity, &App) -> R, - ) -> Result - where - T: 'static, - { - let app = self.app.upgrade().context("app was released")?; - let lock = app.borrow(); - if lock.quitting { - bail!("app is quitting"); - } - lock.read_window(window, read) - } - - #[track_caller] - fn background_spawn(&self, future: impl Future + Send + 'static) -> Task - where - R: Send + 'static, - { - self.background_executor.spawn(future) - } - - fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R - where - G: Global, - { - let app = self.app(); - let mut lock = app.borrow_mut(); - lock.update(|this| this.read_global(callback)) - } -} - -impl AsyncApp { - /// Schedules all windows in the application to be redrawn. - pub fn refresh(&self) { - let app = self.app(); - let mut lock = app.borrow_mut(); - // A direct call would leave the refresh effect queued, which cannot wake - // a platform render loop that has already parked. - lock.update(|cx| cx.refresh_windows()); - } - - /// Get an executor which can be used to spawn futures in the background. - pub fn background_executor(&self) -> &BackgroundExecutor { - &self.background_executor - } - - /// Get an executor which can be used to spawn futures in the foreground. - pub fn foreground_executor(&self) -> &ForegroundExecutor { - &self.foreground_executor - } - - /// Invoke the given function in the context of the app, then flush any effects produced during its invocation. - pub fn update(&self, f: impl FnOnce(&mut App) -> R) -> R { - let app = self.app(); - let mut lock = app.borrow_mut(); - lock.update(f) - } - - /// Arrange for the given callback to be invoked whenever the given entity emits an event of a given type. - /// The callback is provided a handle to the emitting entity and a reference to the emitted event. - pub fn subscribe( - &mut self, - entity: &Entity, - on_event: impl FnMut(Entity, &Event, &mut App) + 'static, - ) -> Subscription - where - T: 'static + EventEmitter, - Event: 'static, - { - let app = self.app(); - let mut lock = app.borrow_mut(); - lock.subscribe(entity, on_event) - } - - /// Open a window with the given options based on the root view returned by the given function. - pub fn open_window( - &self, - options: crate::WindowOptions, - build_root_view: impl FnOnce(&mut Window, &mut App) -> Entity, - ) -> Result> - where - V: 'static + Render, - { - let app = self.app(); - let mut lock = app.borrow_mut(); - if lock.quitting { - bail!("app is quitting"); - } - lock.open_window(options, build_root_view) - } - - /// Schedule a future to be polled in the foreground. - #[track_caller] - pub fn spawn(&self, f: AsyncFn) -> Task - where - AsyncFn: AsyncFnOnce(&mut AsyncApp) -> R + 'static, - R: 'static, - { - let mut cx = self.clone(); - self.foreground_executor - .spawn(async move { f(&mut cx).await }.boxed_local()) - } - - /// Determine whether global state of the specified type has been assigned. - pub fn has_global(&self) -> bool { - let app = self.app(); - let app = app.borrow_mut(); - app.has_global::() - } - - /// Reads the global state of the specified type, passing it to the given callback. - /// - /// Panics if no global state of the specified type has been assigned. - pub fn read_global(&self, read: impl FnOnce(&G, &App) -> R) -> R { - let app = self.app(); - let app = app.borrow_mut(); - read(app.global(), &app) - } - - /// Reads the global state of the specified type, passing it to the given callback. - /// - /// Similar to [`AsyncApp::read_global`], but returns an error instead of panicking - pub fn try_read_global(&self, read: impl FnOnce(&G, &App) -> R) -> Option { - let app = self.app(); - let app = app.borrow_mut(); - if app.quitting { - return None; - } - Some(read(app.try_global()?, &app)) - } - - /// Reads the global state of the specified type, passing it to the given callback. - /// A default value is assigned if a global of this type has not yet been assigned. - pub fn read_default_global( - &self, - read: impl FnOnce(&G, &App) -> R, - ) -> R { - let app = self.app(); - let mut app = app.borrow_mut(); - app.update(|cx| { - cx.default_global::(); - }); - read(app.global(), &app) - } - - /// A convenience method for [`App::update_global`](BorrowAppContext::update_global) - /// for updating the global state of the specified type. - pub fn update_global(&self, update: impl FnOnce(&mut G, &mut App) -> R) -> R { - let app = self.app(); - let mut app = app.borrow_mut(); - app.update(|cx| cx.update_global(update)) - } - - /// Run something using this entity and cx, when the returned struct is dropped - pub fn on_drop) + 'static>( - &self, - entity: &WeakEntity, - f: Callback, - ) -> gpui_util::Deferred> { - let entity = entity.clone(); - let mut cx = self.clone(); - gpui_util::defer(move || { - entity.update(&mut cx, f).ok(); - }) - } -} - -/// A cloneable, owned handle to the application context, -/// composed with the window associated with the current task. -#[derive(Clone, Deref, DerefMut)] -pub struct AsyncWindowContext { - #[deref] - #[deref_mut] - app: AsyncApp, - window: AnyWindowHandle, -} - -impl AsyncWindowContext { - pub(crate) fn new_context(app: AsyncApp, window: AnyWindowHandle) -> Self { - Self { app, window } - } - - /// Get the handle of the window this context is associated with. - pub fn window_handle(&self) -> AnyWindowHandle { - self.window - } - - /// A convenience method for [`App::update_window`]. - pub fn update(&mut self, update: impl FnOnce(&mut Window, &mut App) -> R) -> Result { - self.app - .update_window(self.window, |_, window, cx| update(window, cx)) - } - - /// A convenience method for [`App::update_window`]. - pub fn update_root( - &mut self, - update: impl FnOnce(AnyView, &mut Window, &mut App) -> R, - ) -> Result { - self.app.update_window(self.window, update) - } - - /// A convenience method for [`Window::on_next_frame`]. - pub fn on_next_frame(&mut self, f: impl FnOnce(&mut Window, &mut App) + 'static) { - self.app - .update_window(self.window, |_, window, _| window.on_next_frame(f)) - .ok(); - } - - /// A convenience method for [`App::global`]. - pub fn read_global( - &mut self, - read: impl FnOnce(&G, &Window, &App) -> R, - ) -> Result { - self.app - .update_window(self.window, |_, window, cx| read(cx.global(), window, cx)) - } - - /// A convenience method for [`App::update_global`](BorrowAppContext::update_global). - /// for updating the global state of the specified type. - pub fn update_global( - &mut self, - update: impl FnOnce(&mut G, &mut Window, &mut App) -> R, - ) -> Result - where - G: Global, - { - self.app.update_window(self.window, |_, window, cx| { - cx.update_global(|global, cx| update(global, window, cx)) - }) - } - - /// Schedule a future to be executed on the main thread. This is used for collecting - /// the results of background tasks and updating the UI. - #[track_caller] - pub fn spawn(&self, f: AsyncFn) -> Task - where - AsyncFn: AsyncFnOnce(&mut AsyncWindowContext) -> R + 'static, - R: 'static, - { - let mut cx = self.clone(); - self.foreground_executor - .spawn(async move { f(&mut cx).await }.boxed_local()) - } - - /// Present a platform dialog. - /// The provided message will be presented, along with buttons for each answer. - /// When a button is clicked, the returned Receiver will receive the index of the clicked button. - pub fn prompt( - &mut self, - level: PromptLevel, - message: &str, - detail: Option<&str>, - answers: &[T], - ) -> oneshot::Receiver - where - T: Clone + Into, - { - self.app - .update_window(self.window, |_, window, cx| { - window.prompt(level, message, detail, answers, cx) - }) - .unwrap_or_else(|_| oneshot::channel().1) - } -} - -impl AppContext for AsyncWindowContext { - fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity - where - T: 'static, - { - let mut build_entity = Some(build_entity); - match self.app.update_window(self.window, |_, _, cx| { - cx.new( - build_entity - .take() - .expect("build_entity is taken exactly once"), - ) - }) { - Ok(entity) => entity, - Err(_) => self.app.new( - build_entity - .take() - .expect("update_window returned Err without invoking the closure"), - ), - } - } - - fn reserve_entity(&mut self) -> Reservation { - self.app.reserve_entity() - } - - fn insert_entity( - &mut self, - reservation: Reservation, - build_entity: impl FnOnce(&mut Context) -> T, - ) -> Entity { - let mut args = Some((reservation, build_entity)); - match self.app.update_window(self.window, |_, _, cx| { - let (reservation, build_entity) = args.take().expect("args are taken exactly once"); - cx.insert_entity(reservation, build_entity) - }) { - Ok(entity) => entity, - Err(_) => { - let (reservation, build_entity) = args - .take() - .expect("update_window returned Err without invoking the closure"); - self.app.insert_entity(reservation, build_entity) - } - } - } - - fn update_entity( - &mut self, - handle: &Entity, - update: impl FnOnce(&mut T, &mut Context) -> R, - ) -> R { - self.app.update_entity(handle, update) - } - - fn as_mut<'a, T>(&'a mut self, _: &Entity) -> GpuiBorrow<'a, T> - where - T: 'static, - { - panic!("Cannot use as_mut() from an async context, call `update`") - } - - fn read_entity(&self, handle: &Entity, read: impl FnOnce(&T, &App) -> R) -> R - where - T: 'static, - { - self.app.read_entity(handle, read) - } - - fn update_window(&mut self, window: AnyWindowHandle, update: F) -> Result - where - F: FnOnce(AnyView, &mut Window, &mut App) -> T, - { - self.app.update_window(window, update) - } - - fn with_window( - &mut self, - entity_id: EntityId, - f: impl FnOnce(&mut Window, &mut App) -> R, - ) -> Option { - self.app.with_window(entity_id, f) - } - - fn read_window( - &self, - window: &WindowHandle, - read: impl FnOnce(Entity, &App) -> R, - ) -> Result - where - T: 'static, - { - self.app.read_window(window, read) - } - - #[track_caller] - fn background_spawn(&self, future: impl Future + Send + 'static) -> Task - where - R: Send + 'static, - { - self.app.background_executor.spawn(future) - } - - fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R - where - G: Global, - { - self.app.read_global(callback) - } -} - -impl VisualContext for AsyncWindowContext { - type Result = Result; - - fn window_handle(&self) -> AnyWindowHandle { - self.window - } - - fn new_window_entity( - &mut self, - build_entity: impl FnOnce(&mut Window, &mut Context) -> T, - ) -> Result> { - self.app.update_window(self.window, |_, window, cx| { - cx.new(|cx| build_entity(window, cx)) - }) - } - - fn update_window_entity( - &mut self, - view: &Entity, - update: impl FnOnce(&mut T, &mut Window, &mut Context) -> R, - ) -> Result { - let view = view.clone(); - self.app - .with_window(view.entity_id(), |window, app| { - view.update(app, |entity, cx| update(entity, window, cx)) - }) - .context("entity has no current window") - } - - fn replace_root_view( - &mut self, - build_view: impl FnOnce(&mut Window, &mut Context) -> V, - ) -> Result> - where - V: 'static + Render, - { - self.app.update_window(self.window, |_, window, cx| { - window.replace_root(cx, build_view) - }) - } - - fn focus(&mut self, view: &Entity) -> Result<()> - where - V: Focusable, - { - self.app.update_window(self.window, |_, window, cx| { - view.read(cx).focus_handle(cx).focus(window, cx); - }) - } -} diff --git a/crates/gpui_pre/src/app/bench_context.rs b/crates/gpui_pre/src/app/bench_context.rs deleted file mode 100644 index 278f26c..0000000 --- a/crates/gpui_pre/src/app/bench_context.rs +++ /dev/null @@ -1,1283 +0,0 @@ -use std::{ - cell::{OnceCell, RefCell}, - future::Future, - rc::Rc, - sync::Arc, - time::Duration, -}; - -use anyhow::{Result, anyhow}; -use hdrhistogram::Histogram; - -use crate::{ - AnyView, AnyWindowHandle, App, AppCell, AppContext, BackgroundExecutor, Bounds, Context, Empty, - Entity, EntityId, Focusable, ForegroundExecutor, Global, Platform, PlatformHeadlessRenderer, - PlatformTextSystem, Render, Reservation, Task, TestPlatform, ThreadedDispatcher, VisualContext, - Window, WindowBounds, WindowHandle, WindowOptions, - app::GpuiBorrow, - profiler::{ - self, FrameEvent, FrameTimingCollector, - journal::{ForegroundEvent, ForegroundJournalCollector, ForegroundJournalEntry}, - }, -}; - -/// Returns a benchmark platform backed by this thread's shared dispatcher. -/// -/// The platform uses this thread's shared multithreaded [`ThreadedDispatcher`], so -/// background work runs with production concurrency in real time. The dispatcher -/// is cached per thread and reused across benchmark invocations so worker and -/// timer threads persist for the whole process instead of being recreated for -/// every Criterion calibration pass. -/// -/// Text is shaped with the provided platform text system. Benchmarks generated -/// by `#[gpui::bench]` use the current platform's text system, so text-heavy -/// benchmark measurements include production shaping and glyph rasterization. -/// -/// `headless_renderer_factory` supplies a renderer for benchmark windows, e.g. -/// `gpui_platform::current_headless_renderer`. When present, scenes drawn by -/// benchmarks are rasterized through the real sprite atlas and submitted to -/// the GPU on present, so quad/sprite regressions show up in measurements. -/// When `None`, presenting discards the scene. Currently only macOS provides -/// a headless renderer (Metal), so GPU submission is excluded from benchmark -/// measurements on other platforms. -pub fn bench_platform( - headless_renderer_factory: Option Option>>>, - text_system: Arc, -) -> Rc { - thread_local! { - static DISPATCHER: OnceCell> = const { OnceCell::new() }; - } - let dispatcher = DISPATCHER.with(|cell| { - cell.get_or_init(|| Arc::new(ThreadedDispatcher::new())) - .clone() - }); - let background_executor = BackgroundExecutor::new(dispatcher.clone()); - let foreground_executor = ForegroundExecutor::new(dispatcher); - TestPlatform::with_platform( - background_executor, - foreground_executor, - text_system, - headless_renderer_factory, - ) as Rc -} - -/// Default target frame rate when a benchmark doesn't specify `fps = N`. -const DEFAULT_FPS: u64 = 120; - -const NANOS_PER_SECOND: u128 = 1_000_000_000; - -/// Aggregate statistics for total foreground executor work observed during a -/// measured interval, returned by [`BenchReport::foreground_work`]. -#[derive(Clone, Copy, Debug)] -pub struct ForegroundWorkSummary { - /// Number of foreground work items recorded: task polls, action - /// handlers, input dispatches, and folded sub-floor poll flushes. - pub count: u64, - /// Sum of every recorded item's duration. - pub total: Duration, - /// The longest single recorded item. - pub max: Duration, - /// 50th percentile duration. - pub p50: Duration, - /// 90th percentile duration. - pub p90: Duration, - /// 95th percentile duration. - pub p95: Duration, - /// 99th percentile duration. - pub p99: Duration, - /// How many whole frame budgets (at the report's configured FPS) were - /// exceeded in total, summed across every recorded item. - pub frame_budget_overruns_total: u64, - /// How many whole frame budgets the longest recorded item exceeded. - pub frame_budget_overruns_max: u64, -} - -/// A small report produced by GPUI benchmarks. -#[derive(Clone)] -pub struct BenchReport { - frame_snapshot: Rc>, - frame_budget_nanos: u128, -} - -impl Default for BenchReport { - fn default() -> Self { - Self::with_fps(DEFAULT_FPS) - } -} - -impl BenchReport { - /// Creates a report whose per-frame budget is one frame at `fps` when - /// counting frame budget overruns. - pub fn with_fps(fps: u64) -> Self { - assert!(fps > 0, "frame rate must be greater than zero"); - Self::with_frame_budget_nanos(NANOS_PER_SECOND / fps as u128) - } - - /// Creates a report that treats `frame_budget_nanos` as the per-frame budget - /// when counting frame budget overruns. - pub fn with_frame_budget_nanos(frame_budget_nanos: u128) -> Self { - Self { - frame_snapshot: Rc::new(RefCell::new(WindowFrameSnapshot::new())), - frame_budget_nanos, - } - } - - fn record_frame_timings<'i>(&self, events: impl IntoIterator) { - let mut snapshot = self.frame_snapshot.borrow_mut(); - // `.ok()` on `record`: this operation is infallible (the histograms auto-resize). - for event in events { - match event { - FrameEvent::Draw(timing) => { - snapshot - .draw - .record(timing.draw_duration().as_nanos() as u64) - .ok(); - if let Some(dirty_to_draw) = timing.dirty_to_draw_duration() { - snapshot - .dirty_to_draw - .record(dirty_to_draw.as_nanos() as u64) - .ok(); - } - if timing.invalidations > 0 { - snapshot - .invalidations_per_frame - .record(timing.invalidations) - .ok(); - } - } - FrameEvent::Present(timing) => { - if let Some(animation_interval) = timing.animation_interval { - snapshot - .present_interval - .record(animation_interval.as_nanos() as u64) - .ok(); - } - } - } - } - } - - /// Records total foreground executor work observed during a measured - /// interval: task polls, action handlers, and input dispatches, whether - /// or not they produced a window draw. Draws and presents are excluded - /// here since [`Self::record_frame_timings`] already accounts for them. - fn record_foreground_events<'i>(&self, events: impl IntoIterator) { - let mut snapshot = self.frame_snapshot.borrow_mut(); - for event in events { - let duration = match event { - ForegroundEvent::Draw(_) | ForegroundEvent::Present(_) => continue, - // A flush's span (used by `ForegroundEvent::duration`) is not - // the time spent polling; its summary total is. - ForegroundEvent::SmallPolls(flush) => flush.summary.total, - _ => event.duration(), - }; - snapshot.foreground_work.record(duration); - } - } - - fn total_budget_overruns(&self, histogram: &Histogram) -> u64 { - histogram - .iter_recorded() - .map(|value| { - self.budget_overruns(Duration::from_nanos(value.value_iterated_to())) - * value.count_at_value() - }) - .sum() - } - - /// Returns how many whole frame budgets `foreground_time` exceeded the - /// per frame budget by. This is a synthetic proxy for missed frames: the - /// benchmark harness has no vsync, so it counts how many frame deadlines - /// would have elapsed while the foreground thread was busy. - fn budget_overruns(&self, foreground_time: Duration) -> u64 { - let foreground_nanos = foreground_time.as_nanos(); - if foreground_nanos <= self.frame_budget_nanos { - return 0; - } - - let over_budget_nanos = foreground_nanos - self.frame_budget_nanos; - over_budget_nanos.div_ceil(self.frame_budget_nanos) as u64 - } - - /// Returns aggregate statistics for total foreground executor work - /// observed during the measured interval: every task poll, action - /// handler, and input dispatch on the foreground thread, whether or not - /// it produced a window draw. This is captured through GPUI's foreground - /// journal, so it requires no window and surfaces a slow or stalled task - /// even when nothing was drawn while it ran. - /// - /// Returns `None` when no foreground work was recorded, e.g. a - /// [`BenchAppContext::bench_iter`] measurement that does no async work. - pub fn foreground_work(&self) -> Option { - let frame_snapshot = self.frame_snapshot.borrow(); - let foreground_work = &frame_snapshot.foreground_work; - if foreground_work.histogram.is_empty() { - return None; - } - - let max = Duration::from_nanos(foreground_work.histogram.max()); - Some(ForegroundWorkSummary { - count: foreground_work.histogram.len(), - total: Duration::from_nanos(foreground_work.total_nanos), - max, - p50: Duration::from_nanos(foreground_work.histogram.value_at_quantile(0.50)), - p90: Duration::from_nanos(foreground_work.histogram.value_at_quantile(0.90)), - p95: Duration::from_nanos(foreground_work.histogram.value_at_quantile(0.95)), - p99: Duration::from_nanos(foreground_work.histogram.value_at_quantile(0.99)), - frame_budget_overruns_total: self.total_budget_overruns(&foreground_work.histogram), - frame_budget_overruns_max: self.budget_overruns(max), - }) - } - - /// Prints this report to stderr. - pub fn print(&self, benchmark_name: Option<&'static str>) { - let frame_snapshot = self.frame_snapshot.borrow(); - if frame_snapshot.is_empty() { - return; - } - - let benchmark_name = benchmark_name.unwrap_or("unknown benchmark"); - eprintln!("GPUI bench report (all observed iterations): {benchmark_name}"); - eprintln!(" note: includes Criterion warmup/calibration"); - self.print_histogram("window dirty-to-draw", &frame_snapshot.dirty_to_draw); - self.print_histogram("window draw", &frame_snapshot.draw); - self.print_histogram("window present interval", &frame_snapshot.present_interval); - if !frame_snapshot.invalidations_per_frame.is_empty() { - eprintln!( - " invalidations per frame: mean {:.2}, max {}", - frame_snapshot.invalidations_per_frame.mean(), - frame_snapshot.invalidations_per_frame.max() - ); - } - self.print_foreground_work(&frame_snapshot.foreground_work); - } - - fn print_histogram(&self, name: &str, histogram: &Histogram) { - if histogram.is_empty() { - return; - } - - eprintln!(" {name}:"); - self.print_histogram_body(histogram); - } - - fn print_foreground_work(&self, foreground_work: &DurationHistogram) { - if foreground_work.histogram.is_empty() { - return; - } - - eprintln!(" foreground executor work (task polls, actions, input dispatch):"); - eprintln!(" note: excludes window draw/present, reported separately above"); - eprintln!( - " total: {}", - format_duration(Duration::from_nanos(foreground_work.total_nanos)) - ); - self.print_histogram_body(&foreground_work.histogram); - } - - fn print_histogram_body(&self, histogram: &Histogram) { - let max_foreground_time = Duration::from_nanos(histogram.max()); - eprintln!(" samples: {}", histogram.len()); - eprintln!( - " mean: {}", - format_duration(Duration::from_nanos(histogram.mean() as u64)) - ); - eprintln!( - " p50: {}", - format_duration(Duration::from_nanos(histogram.value_at_quantile(0.50))) - ); - eprintln!( - " p90: {}", - format_duration(Duration::from_nanos(histogram.value_at_quantile(0.90))) - ); - eprintln!( - " p95: {}", - format_duration(Duration::from_nanos(histogram.value_at_quantile(0.95))) - ); - eprintln!( - " p99: {}", - format_duration(Duration::from_nanos(histogram.value_at_quantile(0.99))) - ); - eprintln!(" max: {}", format_duration(max_foreground_time)); - eprintln!( - " frame budget overruns total: {}", - self.total_budget_overruns(histogram) - ); - eprintln!( - " frame budget overruns max: {}", - self.budget_overruns(max_foreground_time) - ); - } -} - -struct WindowFrameSnapshot { - dirty_to_draw: Histogram, - draw: Histogram, - present_interval: Histogram, - invalidations_per_frame: Histogram, - foreground_work: DurationHistogram, -} - -impl WindowFrameSnapshot { - fn new() -> Self { - Self { - dirty_to_draw: Histogram::new(3).expect("3 significant digits is valid"), - draw: Histogram::new(3).expect("3 significant digits is valid"), - present_interval: Histogram::new(3).expect("3 significant digits is valid"), - invalidations_per_frame: Histogram::new(3).expect("3 significant digits is valid"), - foreground_work: DurationHistogram::new(), - } - } - - fn is_empty(&self) -> bool { - self.dirty_to_draw.is_empty() - && self.draw.is_empty() - && self.present_interval.is_empty() - && self.foreground_work.histogram.is_empty() - } -} - -/// A duration histogram paired with an exact running total, since the -/// histogram's bucketed values (3 significant digits) approximate a sum less -/// precisely than tracking it directly. -struct DurationHistogram { - histogram: Histogram, - total_nanos: u64, -} - -impl DurationHistogram { - fn new() -> Self { - Self { - histogram: Histogram::new(3).expect("3 significant digits is valid"), - total_nanos: 0, - } - } - - fn record(&mut self, duration: Duration) { - let nanos = duration.as_nanos() as u64; - // Infallible: the histogram auto-resizes. - self.histogram.record(nanos).ok(); - self.total_nanos += nanos; - } -} - -fn format_duration(duration: Duration) -> String { - format!("{:.3}ms", duration.as_secs_f64() * 1000.) -} - -/// Enables profiler tracing for a measurement and collects its frame events -/// and foreground journal entries. -/// -/// The previous tracing state is restored on drop, so a panicking measurement -/// doesn't leave tracing enabled for unrelated code such as a later benchmark -/// in the same process. -/// -/// The foreground journal collector is created at the same point, so -/// foreground work recorded before the scope starts (e.g. per-iteration -/// setup) is excluded from what [`Self::finish`] returns: a collector only -/// observes entries recorded after its creation. -struct TraceScope { - collector: FrameTimingCollector, - journal_collector: ForegroundJournalCollector, - _trace_guard: profiler::TraceGuard, -} - -impl TraceScope { - fn start(journal_collector: ForegroundJournalCollector) -> Self { - let trace_guard = profiler::trace_scope(); - Self { - collector: FrameTimingCollector::new(), - journal_collector, - _trace_guard: trace_guard, - } - } - - fn finish(mut self) -> TracedEvents { - TracedEvents { - frame_events: self.collector.collect_unseen(), - journal_entries: self.journal_collector.collect_unseen().entries, - } - } -} - -/// Events observed during one [`TraceScope`]. -struct TracedEvents { - frame_events: Vec, - journal_entries: Vec, -} - -impl TracedEvents { - /// Foreground journal entries that describe completed work (task polls, - /// action handlers, input dispatches, draws, presents, and folded - /// sub-floor polls), excluding interval boundaries and metadata. - fn foreground_events(&self) -> impl Iterator { - self.journal_entries.iter().filter_map(|entry| match entry { - ForegroundJournalEntry::Event(event) => Some(event), - _ => None, - }) - } -} - -struct MeasuredTaskInput { - input: Input, - trace_scope: Option, -} - -struct MeasuredTaskOutput { - trace_scope: Option, - report: BenchReport, - _output: Output, -} - -impl Drop for MeasuredTaskOutput { - fn drop(&mut self) { - let trace_scope = self - .trace_scope - .take() - .expect("measured task output should retain its trace scope"); - let events = trace_scope.finish(); - self.report.record_frame_timings(events.frame_events.iter()); - self.report - .record_foreground_events(events.foreground_events()); - } -} - -fn run_task_to_completion( - foreground_executor: &ForegroundExecutor, - task: Task, -) -> Output -where - Output: 'static, -{ - let output = Rc::new(RefCell::new(None)); - foreground_executor - .spawn({ - let output = output.clone(); - async move { - *output.borrow_mut() = Some(task.await); - } - }) - .detach(); - - foreground_executor - .dispatcher() - .as_threaded() - .expect("BenchAppContext requires a ThreadedDispatcher") - .run_until(|| output.borrow_mut().take()) -} - -/// A GPUI app context for Criterion benchmarks. -/// -/// `BenchAppContext` is intentionally separate from `TestAppContext`: it owns a -/// benchmark app instance and exposes only the app/window operations needed by -/// benchmark setup. Criterion remains responsible for the measured loop via its -/// `Bencher` API. -#[derive(Clone)] -pub struct BenchAppContext<'a, 'measurement> { - app: Rc, - background_executor: BackgroundExecutor, - foreground_executor: ForegroundExecutor, - benchmark_name: Option<&'static str>, - bencher: Rc>>>, - report: BenchReport, -} - -impl<'a, 'measurement> BenchAppContext<'a, 'measurement> { - /// Creates a new benchmark app context backed by the provided platform. - /// - /// The platform's executors must be backed by a [`ThreadedDispatcher`] - /// (see [`bench_platform`]) so the context can drain foreground work via - /// [`Self::run_until_idle`]; panics otherwise. - pub fn new( - platform: Rc, - benchmark_name: Option<&'static str>, - bencher: &'a mut criterion::Bencher<'measurement>, - ) -> Self { - Self::build(platform, benchmark_name, bencher, BenchReport::default()) - } - - /// Creates a new benchmark app context backed by the provided platform. - /// - /// The platform's executors must be backed by a [`ThreadedDispatcher`] - /// (see [`bench_platform`]) so the context can drain foreground work via - /// [`Self::run_until_idle`]; panics otherwise. - #[doc(hidden)] - pub fn new_with_platform_and_report( - platform: Rc, - benchmark_name: Option<&'static str>, - bencher: &'a mut criterion::Bencher<'measurement>, - report: BenchReport, - ) -> Self { - Self::build(platform, benchmark_name, bencher, report) - } - - fn build( - platform: Rc, - benchmark_name: Option<&'static str>, - bencher: &'a mut criterion::Bencher<'measurement>, - report: BenchReport, - ) -> Self { - let background_executor = platform.background_executor(); - // Validate up front so misconfiguration fails at construction with a - // clear message instead of deep inside `run_until_idle`. - assert!( - background_executor.dispatcher().as_threaded().is_some(), - "BenchAppContext requires a platform whose executors are backed by a \ - ThreadedDispatcher; construct one with gpui::bench_platform" - ); - let foreground_executor = platform.foreground_executor(); - let asset_source = Arc::new(()); - // Benchmark setup must not make accidental network requests. The - // production `BlockedHttpClient` reports them without enabling a - // configurable test double through `test-support`. - let http_client: Arc = - Arc::new(http_client::BlockedHttpClient::new()); - let app = App::new_app(platform, asset_source, http_client); - - Self { - app, - background_executor, - foreground_executor, - benchmark_name, - bencher: Rc::new(RefCell::new(Some(bencher))), - report, - } - } - - /// The benchmark function name that created this context. - pub fn benchmark_name(&self) -> Option<&'static str> { - self.benchmark_name - } - - /// Returns the background executor used by this benchmark app. - pub fn background_executor(&self) -> &BackgroundExecutor { - &self.background_executor - } - - /// Returns the foreground executor used by this benchmark app. - pub fn foreground_executor(&self) -> &ForegroundExecutor { - &self.foreground_executor - } - - /// Updates the app and flushes synchronous GPUI effects afterward. - pub fn update(&mut self, update: impl FnOnce(&mut App) -> R) -> R { - let mut app = self.app.borrow_mut(); - app.update(update) - } - - /// Reads app state. - pub fn read(&self, read: impl FnOnce(&App) -> R) -> R { - let app = self.app.borrow(); - read(&app) - } - - /// Runs queued foreground tasks on this thread and waits for in flight - /// background work to finish. Timers that aren't due yet are not waited - /// for (see [`ThreadedDispatcher::run_until_idle`]). - pub fn run_until_idle(&self) { - self.background_executor - .dispatcher() - .as_threaded() - .expect("validated in BenchAppContext::build") - .run_until_idle(); - } - - /// Alternates draining queued work with GPUI update cycles until neither - /// makes progress, so state dropped by benchmark code is fully released. - /// - /// Dropped entities are released only inside an update's effect flush, and - /// releases cascade: one flush drops the entities whose handles are gone, - /// their drops release further handles and can queue foreground work, and - /// a later flush collects those. Executor pumping alone never runs a - /// flush, so without this dropped state would linger in the entity map - /// until some woken task happened to run an update. Production gets this - /// cadence for free from frames and input events. - pub fn settle(&mut self) { - let dispatcher = self.background_executor.dispatcher().clone(); - let dispatcher = dispatcher - .as_threaded() - .expect("validated in BenchAppContext::build"); - loop { - self.run_until_idle(); - self.update(|_| ()); - if dispatcher.is_idle() { - return; - } - } - } - - /// Runs main-thread tasks until `ready` returns a value. - /// - /// Unlike [`Self::run_until_idle`], this returns as soon as `ready` - /// reports completion, leaving any remaining queued work pending. - pub fn run_until(&self, ready: impl FnMut() -> Option) -> R { - self.background_executor - .dispatcher() - .as_threaded() - .expect("validated in BenchAppContext::build") - .run_until(ready) - } - - /// Creates a collector observing foreground journal entries recorded - /// from this point on, for use by a new [`TraceScope`]. - fn foreground_journal_collector(&self) -> ForegroundJournalCollector { - self.read(|app| app.foreground_journal().collector()) - } - - /// Measures a generic benchmark workload using Criterion's iteration loop. - /// - /// The closure is invoked once per Criterion iteration with this - /// benchmark app context so it can update GPUI state. - /// - /// Any window draws triggered by the workload are recorded into the - /// benchmark's frame report through the GPUI frame profiler. - pub fn bench_iter(&mut self, mut benchmark: impl FnMut(&mut Self)) { - let bencher = self.take_bencher("bench_iter"); - let collector = TraceScope::start(self.foreground_journal_collector()); - let mut benchmark = || benchmark(self); - bencher.iter(&mut benchmark); - let events = collector.finish(); - self.report.record_frame_timings(events.frame_events.iter()); - self.report - .record_foreground_events(events.foreground_events()); - self.replace_bencher(bencher); - } - - /// Measures a GPUI task to completion using Criterion's iteration loop. - /// - /// The closure is invoked once per Criterion iteration. The returned task - /// may depend on foreground work, background work, timers, or external - /// workers that wake GPUI tasks. Its output is dropped after the timed - /// interval. - /// - /// Any window draws triggered by the task are recorded into the benchmark's - /// frame report through the GPUI frame profiler. - pub fn bench_task(&mut self, mut benchmark: impl FnMut(&mut Self) -> Task) - where - Output: 'static, - { - self.bench_batched_task_internal("bench_task", |_| (), |_, cx| benchmark(cx)); - } - - /// Measures a GPUI task with per-iteration setup outside the timed interval. - /// - /// `setup` runs before timing starts. The returned input is passed by mutable - /// reference to `benchmark`, which returns the task whose completion is - /// measured. Both the setup input and task output are dropped after timing - /// stops. - /// - /// Each iteration is kept in its own Criterion batch so profiler tracing and - /// destruction cannot overlap adjacent measurements. - pub fn bench_batched_task( - &mut self, - setup: impl FnMut(&mut Self) -> Input, - benchmark: impl FnMut(&mut Input, &mut Self) -> Task, - ) where - Output: 'static, - { - self.bench_batched_task_internal("bench_batched_task", setup, benchmark); - } - - fn bench_batched_task_internal( - &mut self, - benchmark_kind: &str, - mut setup: impl FnMut(&mut Self) -> Input, - mut benchmark: impl FnMut(&mut Input, &mut Self) -> Task, - ) where - Output: 'static, - { - let bencher = self.take_bencher(benchmark_kind); - let mut setup_context = self.clone(); - let mut benchmark_context = self.clone(); - let foreground_executor = self.foreground_executor.clone(); - let report = self.report.clone(); - - bencher.iter_batched_ref( - || { - // The previous iteration's input and output were just - // dropped; settling here releases their entities before the - // next setup, so per-iteration state cannot accumulate - // across a measurement. - setup_context.settle(); - MeasuredTaskInput { - input: setup(&mut setup_context), - trace_scope: Some(TraceScope::start( - setup_context.foreground_journal_collector(), - )), - } - }, - |measured_input| { - let task = benchmark(&mut measured_input.input, &mut benchmark_context); - let output = run_task_to_completion(&foreground_executor, task); - MeasuredTaskOutput { - trace_scope: measured_input.trace_scope.take(), - report: report.clone(), - _output: output, - } - }, - criterion::BatchSize::PerIteration, - ); - self.replace_bencher(bencher); - } - - /// Measures frame latency after updating a GPUI entity in its current window. - /// - /// Each iteration runs `update` against the entity in its current window. In - /// bench builds, flushing the update's effects synchronously draws dirty - /// windows. The entity should be part of the window's render tree, such as the - /// root view or a child of it. - /// - /// Frame events are collected through the GPUI frame profiler - /// ([`crate::profiler::record_frame_event`]), which is enabled for the - /// duration of the measurement. - pub fn bench_renderer( - &mut self, - view: Entity, - mut update: impl FnMut(&mut V, &mut Window, &mut Context), - ) where - V: 'static + Render, - { - let bencher = self.take_bencher("bench_renderer"); - let window_id = self - .with_window(view.entity_id(), |window, _| { - window.window_handle().window_id() - }) - .expect("cannot benchmark renderer for entity without a current window"); - - let dispatcher = self.background_executor.dispatcher().clone(); - let collector = TraceScope::start(self.foreground_journal_collector()); - - let mut benchmark = || { - // Work already queued at frame start delays the frame in - // production too, so run it inside the measured interval. - dispatcher - .as_threaded() - .expect("validated in BenchAppContext::build") - .run_ready_main_tasks(); - self.with_window(view.entity_id(), |window, cx| { - view.update(cx, |view, cx| update(view, window, cx)); - }) - .expect("cannot benchmark renderer for entity without a current window"); - // Submit the frame drawn by the update's effect flush, mirroring - // production where every drawn frame is presented. With a headless - // renderer this includes scene submission to the GPU. - self.with_window(view.entity_id(), |window, _| { - window.present_if_needed(); - }) - .expect("cannot benchmark renderer for entity without a current window"); - }; - bencher.iter(&mut benchmark); - - let events = collector.finish(); - self.report - .record_frame_timings(events.frame_events.iter().filter(|event| match event { - FrameEvent::Draw(timing) => timing.window_id == window_id, - FrameEvent::Present(timing) => timing.window_id == window_id, - })); - // Foreground work isn't attributed to a window, so unlike frame - // timings above it isn't filtered by `window_id`. A benchmark app - // hosts one window at a time, so this cannot pick up unrelated - // windows' work. - self.report - .record_foreground_events(events.foreground_events()); - self.replace_bencher(bencher); - } - - /// Adds a window with an empty root view for benchmark setup. - pub fn add_empty_window(&mut self) -> BenchWindowContext<'a, 'measurement> { - let bounds = { - let app = self.app.borrow(); - Bounds::maximized(None, &app) - }; - let window = { - let mut app = self.app.borrow_mut(); - let window: AnyWindowHandle = app - .open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| Empty), - ) - .expect("failed to open benchmark window") - .into(); - window - }; - - self.run_until_idle(); - BenchWindowContext { - cx: self.clone(), - window, - } - } - - fn take_bencher(&self, benchmark_kind: &str) -> &'a mut criterion::Bencher<'measurement> { - self.bencher.borrow_mut().take().unwrap_or_else(|| { - panic!("cannot start {benchmark_kind}: benchmark measurement is already running") - }) - } - - fn replace_bencher(&self, bencher: &'a mut criterion::Bencher<'measurement>) { - let previous = self.bencher.borrow_mut().replace(bencher); - assert!( - previous.is_none(), - "benchmark bencher was unexpectedly present after measurement" - ); - } - - /// Runs GPUI benchmark teardown. - /// - /// Cancels any timers still armed on the shared dispatcher and drains the - /// work that cancellation unblocks so they can't fire during a later - /// benchmark; assumes no other `BenchAppContext` is live on this thread. - pub fn teardown(mut self) { - self.run_until_idle(); - self.update(|cx| { - cx.quit(); - }); - self.run_until_idle(); - - let dispatcher = self.background_executor.dispatcher(); - let dispatcher = dispatcher - .as_threaded() - .expect("validated in BenchAppContext::build"); - - drop(self.app); - drop(self.foreground_executor); - - for _ in 0..100 { - if dispatcher.cancel_pending_timers() == 0 { - return; - } - dispatcher.run_until_idle(); - } - panic!( - "benchmark teardown kept scheduling timers: {}", - dispatcher.debug_state() - ); - } -} - -impl AppContext for BenchAppContext<'_, '_> { - fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity { - let mut app = self.app.borrow_mut(); - app.new(build_entity) - } - - fn reserve_entity(&mut self) -> Reservation { - let mut app = self.app.borrow_mut(); - app.reserve_entity() - } - - fn insert_entity( - &mut self, - reservation: Reservation, - build_entity: impl FnOnce(&mut Context) -> T, - ) -> Entity { - let mut app = self.app.borrow_mut(); - app.insert_entity(reservation, build_entity) - } - - fn update_entity( - &mut self, - handle: &Entity, - update: impl FnOnce(&mut T, &mut Context) -> R, - ) -> R { - let mut app = self.app.borrow_mut(); - app.update_entity(handle, update) - } - - fn as_mut<'b, T>(&'b mut self, _: &Entity) -> GpuiBorrow<'b, T> - where - T: 'static, - { - panic!("Cannot use as_mut with BenchAppContext. Call update() instead.") - } - - fn read_entity(&self, handle: &Entity, read: impl FnOnce(&T, &App) -> R) -> R - where - T: 'static, - { - let app = self.app.borrow(); - app.read_entity(handle, read) - } - - fn update_window(&mut self, window: AnyWindowHandle, update: F) -> Result - where - F: FnOnce(AnyView, &mut Window, &mut App) -> T, - { - let mut app = self.app.borrow_mut(); - app.update_window(window, update) - } - - fn with_window( - &mut self, - entity_id: EntityId, - update: impl FnOnce(&mut Window, &mut App) -> R, - ) -> Option { - let mut app = self.app.borrow_mut(); - app.with_window(entity_id, update) - } - - fn read_window( - &self, - window: &WindowHandle, - read: impl FnOnce(Entity, &App) -> R, - ) -> Result - where - T: 'static, - { - let app = self.app.borrow(); - app.read_window(window, read) - } - - fn background_spawn(&self, future: impl Future + Send + 'static) -> Task - where - R: Send + 'static, - { - self.background_executor.spawn(future) - } - - fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R - where - G: Global, - { - let app = self.app.borrow(); - app.read_global(callback) - } -} - -/// A window-specific context for GPUI benchmarks. -/// -/// This is separate from `VisualTestContext`; it provides access to a benchmark -/// window without exposing test-only helpers such as input simulation. -#[derive(Clone)] -pub struct BenchWindowContext<'a, 'measurement> { - cx: BenchAppContext<'a, 'measurement>, - window: AnyWindowHandle, -} - -impl<'a, 'measurement> BenchWindowContext<'a, 'measurement> { - /// Returns the underlying benchmark app context. - pub fn app_context(&mut self) -> &mut BenchAppContext<'a, 'measurement> { - &mut self.cx - } - - /// Returns the window associated with this context. - pub fn window_handle(&self) -> AnyWindowHandle { - self.window - } - - /// Runs queued foreground tasks on this thread and waits for in-flight - /// background work to finish. Pending timers are not waited for. - pub fn run_until_idle(&self) { - self.cx.run_until_idle(); - } - - /// Updates the benchmark window. - pub fn update(&mut self, update: impl FnOnce(&mut Window, &mut App) -> R) -> R { - self.cx - .update_window(self.window, |_, window, cx| update(window, cx)) - .expect("benchmark window was unexpectedly closed") - } -} - -impl AppContext for BenchWindowContext<'_, '_> { - fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity { - self.window - .update(&mut self.cx, |_, _, cx| cx.new(build_entity)) - .expect("benchmark window was unexpectedly closed") - } - - fn reserve_entity(&mut self) -> Reservation { - self.cx.reserve_entity() - } - - fn insert_entity( - &mut self, - reservation: Reservation, - build_entity: impl FnOnce(&mut Context) -> T, - ) -> Entity { - self.window - .update(&mut self.cx, |_, _, cx| { - cx.insert_entity(reservation, build_entity) - }) - .expect("benchmark window was unexpectedly closed") - } - - fn update_entity( - &mut self, - handle: &Entity, - update: impl FnOnce(&mut T, &mut Context) -> R, - ) -> R { - self.cx.update_entity(handle, update) - } - - fn as_mut<'b, T>(&'b mut self, handle: &Entity) -> GpuiBorrow<'b, T> - where - T: 'static, - { - self.cx.as_mut(handle) - } - - fn read_entity(&self, handle: &Entity, read: impl FnOnce(&T, &App) -> R) -> R - where - T: 'static, - { - self.cx.read_entity(handle, read) - } - - fn update_window(&mut self, window: AnyWindowHandle, update: F) -> Result - where - F: FnOnce(AnyView, &mut Window, &mut App) -> T, - { - self.cx.update_window(window, update) - } - - fn with_window( - &mut self, - entity_id: EntityId, - update: impl FnOnce(&mut Window, &mut App) -> R, - ) -> Option { - self.cx.with_window(entity_id, update) - } - - fn read_window( - &self, - window: &WindowHandle, - read: impl FnOnce(Entity, &App) -> R, - ) -> Result - where - T: 'static, - { - self.cx.read_window(window, read) - } - - fn background_spawn(&self, future: impl Future + Send + 'static) -> Task - where - R: Send + 'static, - { - self.cx.background_spawn(future) - } - - fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R - where - G: Global, - { - self.cx.read_global(callback) - } -} - -impl VisualContext for BenchWindowContext<'_, '_> { - type Result = Result; - - fn window_handle(&self) -> AnyWindowHandle { - self.window - } - - fn update_window_entity( - &mut self, - entity: &Entity, - update: impl FnOnce(&mut T, &mut Window, &mut Context) -> R, - ) -> Result { - let entity = entity.clone(); - self.cx - .app - .borrow_mut() - .with_window(entity.entity_id(), |window, app| { - entity.update(app, |entity, cx| update(entity, window, cx)) - }) - .ok_or_else(|| { - anyhow!("entity has no current window; use `update` instead of `update_in`") - }) - } - - fn new_window_entity( - &mut self, - build_entity: impl FnOnce(&mut Window, &mut Context) -> T, - ) -> Result> { - self.window.update(&mut self.cx, |_, window, cx| { - cx.new(|cx| build_entity(window, cx)) - }) - } - - fn replace_root_view( - &mut self, - build_view: impl FnOnce(&mut Window, &mut Context) -> V, - ) -> Result> - where - V: 'static + Render, - { - self.window.update(&mut self.cx, |_, window, cx| { - window.replace_root(cx, build_view) - }) - } - - fn focus(&mut self, entity: &Entity) -> Result<()> - where - V: Focusable, - { - self.window.update(&mut self.cx, |_, window, cx| { - entity.read(cx).focus_handle(cx).focus(window, cx) - }) - } -} - -#[cfg(test)] -mod tests { - use std::{rc::Rc, sync::Arc}; - - use super::*; - use crate::profiler::journal::install_test_foreground_journal; - - #[test] - fn foreground_work_reports_long_task_without_window_draw() { - let (journal, _journal_guard) = install_test_foreground_journal(1024, 64); - let dispatcher = Arc::new(ThreadedDispatcher::new()); - let foreground_executor = ForegroundExecutor::new(dispatcher); - - let trace_scope = TraceScope::start(journal.collector()); - - // A single foreground task poll that never touches a window, akin - // to the stall a debounced background computation can cause. - let task = foreground_executor.spawn(async move { - std::thread::sleep(Duration::from_millis(60)); - }); - run_task_to_completion(&foreground_executor, task); - - let events = trace_scope.finish(); - assert!( - events.frame_events.is_empty(), - "no window was involved, so no frame events should be recorded" - ); - - let report = BenchReport::default(); - report.record_foreground_events(events.foreground_events()); - - let summary = report - .foreground_work() - .expect("a long task poll should be reported even without a window draw"); - // The spawned task's own poll is one sample; the tiny wrapper poll - // that observes its completion in `run_task_to_completion` folds - // into a second, near-zero sample rather than being dropped. - assert!(summary.count >= 1, "expected at least one recorded item"); - assert!( - summary.max >= Duration::from_millis(55), - "expected the long poll's duration to be recorded, got {:?}", - summary.max - ); - // `total` is an exact sum, while `max` may be rounded up to its - // histogram bucket's boundary, so compare each against the expected - // floor directly instead of against each other. - assert!( - summary.total >= Duration::from_millis(55), - "expected the long poll's duration to be included in the total, got {:?}", - summary.total - ); - } - - #[test] - fn foreground_work_excludes_setup_before_trace_scope_starts() { - let (journal, _journal_guard) = install_test_foreground_journal(1024, 64); - let dispatcher = Arc::new(ThreadedDispatcher::new()); - let foreground_executor = ForegroundExecutor::new(dispatcher); - - // Fixture/setup work that must not be attributed to the measurement: - // a long poll recorded before the trace scope (and its journal - // collector) is created. - let setup_task = foreground_executor.spawn(async move { - std::thread::sleep(Duration::from_millis(80)); - }); - run_task_to_completion(&foreground_executor, setup_task); - - let trace_scope = TraceScope::start(journal.collector()); - - let measured_task = foreground_executor.spawn(async move { - std::thread::sleep(Duration::from_millis(10)); - }); - run_task_to_completion(&foreground_executor, measured_task); - - let events = trace_scope.finish(); - let report = BenchReport::default(); - report.record_foreground_events(events.foreground_events()); - - let summary = report - .foreground_work() - .expect("the measured task's poll should be reported"); - assert!( - summary.max < Duration::from_millis(40), - "setup work's 80ms poll must not leak into the measured summary, got {:?}", - summary.max - ); - assert!( - summary.total < Duration::from_millis(40), - "setup work's 80ms poll must not leak into the measured total, got {:?}", - summary.total - ); - } - - #[test] - fn bench_task_reports_long_task_without_window() { - let platform = bench_platform(None, Arc::new(crate::NoopTextSystem::new())); - let report = BenchReport::default(); - let name = "bench_task_reports_long_task_without_window"; - - let mut criterion = criterion::Criterion::default() - .without_plots() - .sample_size(10) - .warm_up_time(Duration::from_millis(1)) - .measurement_time(Duration::from_millis(1)); - - criterion.bench_function(name, |bencher| { - let mut cx = BenchAppContext::new_with_platform_and_report( - platform.clone(), - Some(name), - bencher, - report.clone(), - ); - cx.bench_task(|cx| { - cx.foreground_executor().spawn(async move { - std::thread::sleep(Duration::from_millis(20)); - }) - }); - cx.teardown(); - }); - - let summary = report - .foreground_work() - .expect("bench_task should report foreground work with no window involved"); - assert!( - summary.max >= Duration::from_millis(15), - "expected a ~20ms task poll to be recorded, got {:?}", - summary.max - ); - } - - #[test] - fn task_completion_supports_non_send_foreground_output() { - let dispatcher = Arc::new(ThreadedDispatcher::new()); - let background_executor = BackgroundExecutor::new(dispatcher.clone()); - let foreground_executor = ForegroundExecutor::new(dispatcher); - let (sender, receiver) = futures::channel::oneshot::channel(); - - background_executor - .spawn(async move { - sender - .send(()) - .expect("foreground receiver should remain alive"); - }) - .detach(); - let expected_output = Rc::new(42); - let task_output = expected_output.clone(); - let task = foreground_executor.spawn(async move { - receiver.await.expect("background task should send a value"); - task_output - }); - - let output = run_task_to_completion(&foreground_executor, task); - assert!( - Rc::ptr_eq(&output, &expected_output), - "task runner should preserve non-Send foreground output" - ); - } -} diff --git a/crates/gpui_pre/src/app/context.rs b/crates/gpui_pre/src/app/context.rs deleted file mode 100644 index 0d1ee47..0000000 --- a/crates/gpui_pre/src/app/context.rs +++ /dev/null @@ -1,883 +0,0 @@ -use crate::{ - AnyView, AnyWindowHandle, AppContext, AsyncApp, DispatchPhase, Effect, EntityId, EventEmitter, - FocusHandle, FocusOutEvent, Focusable, Global, KeystrokeObserver, Priority, Reservation, - SubscriberSet, Subscription, Task, WeakEntity, WeakFocusHandle, Window, WindowHandle, -}; -use anyhow::Result; -use futures::FutureExt; -use gpui_util::Deferred; -use std::{ - any::{Any, TypeId}, - borrow::{Borrow, BorrowMut}, - future::Future, - ops, - sync::Arc, -}; - -use super::{App, AsyncWindowContext, Entity, KeystrokeEvent}; - -/// The app context, with specialized behavior for the given entity. -pub struct Context<'a, T> { - app: &'a mut App, - entity_state: WeakEntity, -} - -impl<'a, T> ops::Deref for Context<'a, T> { - type Target = App; - - fn deref(&self) -> &Self::Target { - self.app - } -} - -impl<'a, T> ops::DerefMut for Context<'a, T> { - fn deref_mut(&mut self) -> &mut Self::Target { - self.app - } -} - -impl<'a, T: 'static> Context<'a, T> { - pub(crate) fn new_context(app: &'a mut App, entity_state: WeakEntity) -> Self { - Self { app, entity_state } - } - - /// The entity id of the entity backing this context. - pub fn entity_id(&self) -> EntityId { - self.entity_state.entity_id - } - - /// Returns a handle to the entity belonging to this context. - pub fn entity(&self) -> Entity { - self.weak_entity() - .upgrade() - .expect("The entity must be alive if we have a entity context") - } - - /// Returns a weak handle to the entity belonging to this context. - pub fn weak_entity(&self) -> WeakEntity { - self.entity_state.clone() - } - - /// Arranges for the given function to be called whenever [`Context::notify`] is - /// called with the given entity. - pub fn observe( - &mut self, - entity: &Entity, - mut on_notify: impl FnMut(&mut T, Entity, &mut Context) + 'static, - ) -> Subscription - where - T: 'static, - W: 'static, - { - let this = self.weak_entity(); - self.app.observe_internal(entity, move |e, cx| { - if let Some(this) = this.upgrade() { - this.update(cx, |this, cx| on_notify(this, e, cx)); - true - } else { - false - } - }) - } - - /// Observe changes to ourselves - pub fn observe_self( - &mut self, - mut on_event: impl FnMut(&mut T, &mut Context) + 'static, - ) -> Subscription - where - T: 'static, - { - let this = self.entity(); - self.app.observe(&this, move |this, cx| { - this.update(cx, |this, cx| on_event(this, cx)) - }) - } - - /// Subscribe to an event type from another entity - pub fn subscribe( - &mut self, - entity: &Entity, - mut on_event: impl FnMut(&mut T, Entity, &Evt, &mut Context) + 'static, - ) -> Subscription - where - T: 'static, - T2: 'static + EventEmitter, - Evt: 'static, - { - let this = self.weak_entity(); - self.app.subscribe_internal(entity, move |e, event, cx| { - if let Some(this) = this.upgrade() { - this.update(cx, |this, cx| on_event(this, e, event, cx)); - true - } else { - false - } - }) - } - - /// Subscribe to an event type from ourself - pub fn subscribe_self( - &mut self, - mut on_event: impl FnMut(&mut T, &Evt, &mut Context) + 'static, - ) -> Subscription - where - T: 'static + EventEmitter, - Evt: 'static, - { - let this = self.entity(); - self.app.subscribe(&this, move |this, evt, cx| { - this.update(cx, |this, cx| on_event(this, evt, cx)) - }) - } - - /// Register a callback to be invoked when GPUI releases this entity. - pub fn on_release(&self, on_release: impl FnOnce(&mut T, &mut App) + 'static) -> Subscription - where - T: 'static, - { - let (subscription, activate) = self.app.release_listeners.insert( - self.entity_state.entity_id, - Box::new(move |this, cx| { - let this = this.downcast_mut().expect("invalid entity type"); - on_release(this, cx); - }), - ); - activate(); - subscription - } - - /// Register a callback to be run on the release of another entity - pub fn observe_release( - &self, - entity: &Entity, - on_release: impl FnOnce(&mut T, &mut T2, &mut Context) + 'static, - ) -> Subscription - where - T: Any, - T2: 'static, - { - let entity_id = entity.entity_id(); - let this = self.weak_entity(); - let (subscription, activate) = self.app.release_listeners.insert( - entity_id, - Box::new(move |entity, cx| { - let entity = entity.downcast_mut().expect("invalid entity type"); - if let Some(this) = this.upgrade() { - this.update(cx, |this, cx| on_release(this, entity, cx)); - } - }), - ); - activate(); - subscription - } - - /// Register a callback to for updates to the given global - pub fn observe_global( - &mut self, - mut f: impl FnMut(&mut T, &mut Context) + 'static, - ) -> Subscription - where - T: 'static, - { - let handle = self.weak_entity(); - let (subscription, activate) = self.global_observers.insert( - TypeId::of::(), - Box::new(move |cx| handle.update(cx, |view, cx| f(view, cx)).is_ok()), - ); - self.defer(move |_| activate()); - subscription - } - - /// Register a callback to be invoked when the application is about to restart. - pub fn on_app_restart( - &self, - mut on_restart: impl FnMut(&mut T, &mut App) + 'static, - ) -> Subscription - where - T: 'static, - { - let handle = self.weak_entity(); - self.app.on_app_restart(move |cx| { - handle.update(cx, |entity, cx| on_restart(entity, cx)).ok(); - }) - } - - /// Arrange for the given function to be invoked whenever the application is quit. - /// The future returned from this callback will be polled for up to [crate::SHUTDOWN_TIMEOUT] until the app fully quits. - pub fn on_app_quit( - &self, - mut on_quit: impl FnMut(&mut T, &mut Context) -> Fut + 'static, - ) -> Subscription - where - Fut: 'static + Future, - T: 'static, - { - let handle = self.weak_entity(); - self.app.on_app_quit(move |cx| { - let future = handle.update(cx, |entity, cx| on_quit(entity, cx)).ok(); - async move { - if let Some(future) = future { - future.await; - } - } - .boxed_local() - }) - } - - /// Tell GPUI that this entity has changed and observers of it should be notified. - pub fn notify(&mut self) { - self.app.notify(self.entity_state.entity_id); - } - - /// Spawn the future returned by the given function. - /// The function is provided a weak handle to the entity owned by this context and a context that can be held across await points. - /// The returned task must be held or detached. - #[track_caller] - pub fn spawn(&self, f: AsyncFn) -> Task - where - T: 'static, - AsyncFn: AsyncFnOnce(WeakEntity, &mut AsyncApp) -> R + 'static, - R: 'static, - { - let this = self.weak_entity(); - self.app.spawn(async move |cx| f(this, cx).await) - } - - /// Convenience method for accessing view state in an event callback. - /// - /// Many GPUI callbacks take the form of `Fn(&E, &mut Window, &mut App)`, - /// but it's often useful to be able to access view state in these - /// callbacks. This method provides a convenient way to do so. - pub fn listener( - &self, - f: impl Fn(&mut T, &E, &mut Window, &mut Context) + 'static, - ) -> impl Fn(&E, &mut Window, &mut App) + 'static { - let view = self.entity().downgrade(); - move |e: &E, window: &mut Window, cx: &mut App| { - view.update(cx, |view, cx| f(view, e, window, cx)).ok(); - } - } - - /// Convenience method for producing view state in a closure. - /// See `listener` for more details. - pub fn processor( - &self, - f: impl Fn(&mut T, E, &mut Window, &mut Context) -> R + 'static, - ) -> impl Fn(E, &mut Window, &mut App) -> R + 'static { - let view = self.entity(); - move |e: E, window: &mut Window, cx: &mut App| { - view.update(cx, |view, cx| f(view, e, window, cx)) - } - } - - /// Run something using this entity and cx, when the returned struct is dropped - pub fn on_drop( - &self, - f: impl FnOnce(&mut T, &mut Context) + 'static, - ) -> Deferred { - let this = self.weak_entity(); - let mut cx = self.to_async(); - gpui_util::defer(move || { - this.update(&mut cx, f).ok(); - }) - } - - /// Focus the given view in the given window. View type is required to implement Focusable. - pub fn focus_view(&mut self, view: &Entity, window: &mut Window) { - window.focus(&view.focus_handle(self), self); - } - - /// Sets a given callback to be run on the next frame. - pub fn on_next_frame( - &self, - window: &mut Window, - f: impl FnOnce(&mut T, &mut Window, &mut Context) + 'static, - ) where - T: 'static, - { - let view = self.entity(); - window.on_next_frame(move |window, cx| view.update(cx, |view, cx| f(view, window, cx))); - } - - /// Schedules the given function to be run at the end of the current effect cycle, allowing entities - /// that are currently on the stack to be returned to the app. - pub fn defer_in( - &mut self, - window: &Window, - f: impl FnOnce(&mut T, &mut Window, &mut Context) + 'static, - ) { - let view = self.weak_entity(); - let entity_id = self.entity_id(); - self.ensure_window(entity_id, window.handle.id); - self.app.defer(move |cx| { - cx.with_window(entity_id, |window, cx| { - view.update(cx, |view, cx| f(view, window, cx)).ok(); - }); - }); - } - - /// Observe another entity for changes to its state, as tracked by [`Context::notify`]. - pub fn observe_in( - &mut self, - observed: &Entity, - window: &mut Window, - mut on_notify: impl FnMut(&mut T, Entity, &mut Window, &mut Context) + 'static, - ) -> Subscription - where - V2: 'static, - T: 'static, - { - let observed_id = observed.entity_id(); - let observed = observed.downgrade(); - let observer = self.weak_entity(); - let observer_id = self.entity_id(); - self.ensure_window(observer_id, window.handle.id); - self.new_observer( - observed_id, - Box::new(move |cx| { - let Some((observer, observed)) = observer.upgrade().zip(observed.upgrade()) else { - return false; - }; - cx.with_window(observer_id, |window, cx| { - observer.update(cx, |observer, cx| { - on_notify(observer, observed, window, cx); - }); - }); - true - }), - ) - } - - /// Subscribe to events emitted by another entity. - /// The entity to which you're subscribing must implement the [`EventEmitter`] trait. - /// The callback will be invoked with a reference to the current view, a handle to the emitting `Entity`, the event, a mutable reference to the `Window`, and the context for the entity. - pub fn subscribe_in( - &mut self, - emitter: &Entity, - window: &Window, - mut on_event: impl FnMut(&mut T, &Entity, &Evt, &mut Window, &mut Context) + 'static, - ) -> Subscription - where - Emitter: EventEmitter, - Evt: 'static, - { - let emitter = emitter.downgrade(); - let subscriber = self.weak_entity(); - let subscriber_id = self.entity_id(); - self.ensure_window(subscriber_id, window.handle.id); - self.new_subscription( - emitter.entity_id(), - ( - TypeId::of::(), - Box::new(move |event, cx| { - let Some((subscriber, emitter)) = subscriber.upgrade().zip(emitter.upgrade()) - else { - return false; - }; - let event = event.downcast_ref().expect("invalid event type"); - cx.with_window(subscriber_id, |window, cx| { - subscriber.update(cx, |subscriber, cx| { - on_event(subscriber, &emitter, event, window, cx); - }); - }); - true - }), - ), - ) - } - - /// Register a callback to be invoked when the view is released. - /// - /// The callback receives a handle to the view's window. This handle may be - /// invalid, if the window was closed before the view was released. - pub fn on_release_in( - &mut self, - window: &Window, - on_release: impl FnOnce(&mut T, &mut Window, &mut App) + 'static, - ) -> Subscription { - let entity = self.entity(); - self.app.observe_release_in(&entity, window, on_release) - } - - /// Register a callback to be invoked when the given Entity is released. - pub fn observe_release_in( - &self, - observed: &Entity, - window: &Window, - mut on_release: impl FnMut(&mut T, &mut T2, &mut Window, &mut Context) + 'static, - ) -> Subscription - where - T: 'static, - T2: 'static, - { - let observer = self.weak_entity(); - self.app - .observe_release_in(observed, window, move |observed, window, cx| { - observer - .update(cx, |observer, cx| { - on_release(observer, observed, window, cx) - }) - .ok(); - }) - } - - /// Register a callback to be invoked when the window is resized. - pub fn observe_window_bounds( - &self, - window: &mut Window, - mut callback: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, - ) -> Subscription { - let view = self.weak_entity(); - let (subscription, activate) = window.bounds_observers.insert( - (), - Box::new(move |window, cx| { - view.update(cx, |view, cx| callback(view, window, cx)) - .is_ok() - }), - ); - activate(); - subscription - } - - /// Register a callback to be invoked when the window is activated or deactivated. - pub fn observe_window_activation( - &self, - window: &mut Window, - mut callback: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, - ) -> Subscription { - let view = self.weak_entity(); - let (subscription, activate) = window.activation_observers.insert( - (), - Box::new(move |window, cx| { - view.update(cx, |view, cx| callback(view, window, cx)) - .is_ok() - }), - ); - activate(); - subscription - } - - /// Registers a callback to be invoked when the window appearance changes. - pub fn observe_window_appearance( - &self, - window: &mut Window, - mut callback: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, - ) -> Subscription { - let view = self.weak_entity(); - let (subscription, activate) = window.appearance_observers.insert( - (), - Box::new(move |window, cx| { - view.update(cx, |view, cx| callback(view, window, cx)) - .is_ok() - }), - ); - activate(); - subscription - } - - /// Registers a callback to be invoked when the window button layout changes. - pub fn observe_button_layout_changed( - &self, - window: &mut Window, - mut callback: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, - ) -> Subscription { - let view = self.weak_entity(); - let (subscription, activate) = window.button_layout_observers.insert( - (), - Box::new(move |window, cx| { - view.update(cx, |view, cx| callback(view, window, cx)) - .is_ok() - }), - ); - activate(); - subscription - } - - /// Register a callback to be invoked when a keystroke is received by the application - /// in any window. Note that this fires after all other action and event mechanisms have resolved - /// and that this API will not be invoked if the event's propagation is stopped. - pub fn observe_keystrokes( - &mut self, - mut f: impl FnMut(&mut T, &KeystrokeEvent, &mut Window, &mut Context) + 'static, - ) -> Subscription { - fn inner( - keystroke_observers: &SubscriberSet<(), KeystrokeObserver>, - handler: KeystrokeObserver, - ) -> Subscription { - let (subscription, activate) = keystroke_observers.insert((), handler); - activate(); - subscription - } - - let view = self.weak_entity(); - inner( - &self.keystroke_observers, - Box::new(move |event, window, cx| { - if let Some(view) = view.upgrade() { - view.update(cx, |view, cx| f(view, event, window, cx)); - true - } else { - false - } - }), - ) - } - - /// Register a callback to be invoked when the window's pending input changes. - pub fn observe_pending_input( - &self, - window: &mut Window, - mut callback: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, - ) -> Subscription { - let view = self.weak_entity(); - let (subscription, activate) = window.pending_input_observers.insert( - (), - Box::new(move |window, cx| { - view.update(cx, |view, cx| callback(view, window, cx)) - .is_ok() - }), - ); - activate(); - subscription - } - - /// Register a listener to be called when the given focus handle receives focus. - /// Returns a subscription and persists until the subscription is dropped. - pub fn on_focus( - &mut self, - handle: &FocusHandle, - window: &mut Window, - mut listener: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, - ) -> Subscription { - let view = self.weak_entity(); - let focus_id = handle.id; - let (subscription, activate) = - window.new_focus_listener(Box::new(move |event, window, cx| { - view.update(cx, |view, cx| { - if event.previous_focus_path.last() != Some(&focus_id) - && event.current_focus_path.last() == Some(&focus_id) - { - listener(view, window, cx) - } - }) - .is_ok() - })); - self.defer(|_| activate()); - subscription - } - - /// Register a listener to be called when the given focus handle or one of its descendants receives focus. - /// This does not fire if the given focus handle - or one of its descendants - was previously focused. - /// Returns a subscription and persists until the subscription is dropped. - pub fn on_focus_in( - &mut self, - handle: &FocusHandle, - window: &mut Window, - mut listener: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, - ) -> Subscription { - let view = self.weak_entity(); - let focus_id = handle.id; - let (subscription, activate) = - window.new_focus_listener(Box::new(move |event, window, cx| { - view.update(cx, |view, cx| { - if event.is_focus_in(focus_id) { - listener(view, window, cx) - } - }) - .is_ok() - })); - self.defer(|_| activate()); - subscription - } - - /// Register a listener to be called when the given focus handle loses focus. - /// Returns a subscription and persists until the subscription is dropped. - pub fn on_blur( - &mut self, - handle: &FocusHandle, - window: &mut Window, - mut listener: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, - ) -> Subscription { - let view = self.weak_entity(); - let focus_id = handle.id; - let (subscription, activate) = - window.new_focus_listener(Box::new(move |event, window, cx| { - view.update(cx, |view, cx| { - if event.previous_focus_path.last() == Some(&focus_id) - && event.current_focus_path.last() != Some(&focus_id) - { - listener(view, window, cx) - } - }) - .is_ok() - })); - self.defer(|_| activate()); - subscription - } - - /// Register a listener to be called when nothing in the window has focus. - /// This typically happens when the node that was focused is removed from the tree, - /// and this callback lets you chose a default place to restore the users focus. - /// Returns a subscription and persists until the subscription is dropped. - pub fn on_focus_lost( - &mut self, - window: &mut Window, - mut listener: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, - ) -> Subscription { - let view = self.weak_entity(); - let (subscription, activate) = window.focus_lost_listeners.insert( - (), - Box::new(move |window, cx| { - view.update(cx, |view, cx| listener(view, window, cx)) - .is_ok() - }), - ); - self.defer(|_| activate()); - subscription - } - - /// Register a listener to be called when the given focus handle or one of its descendants loses focus. - /// Returns a subscription and persists until the subscription is dropped. - pub fn on_focus_out( - &mut self, - handle: &FocusHandle, - window: &mut Window, - mut listener: impl FnMut(&mut T, FocusOutEvent, &mut Window, &mut Context) + 'static, - ) -> Subscription { - let view = self.weak_entity(); - let focus_id = handle.id; - let (subscription, activate) = - window.new_focus_listener(Box::new(move |event, window, cx| { - view.update(cx, |view, cx| { - if let Some(blurred_id) = event.previous_focus_path.last().copied() - && event.is_focus_out(focus_id) - { - let event = FocusOutEvent { - blurred: WeakFocusHandle { - id: blurred_id, - handles: Arc::downgrade(&cx.focus_handles), - }, - }; - listener(view, event, window, cx) - } - }) - .is_ok() - })); - self.defer(|_| activate()); - subscription - } - - /// Schedule a future to be run asynchronously. - /// The given callback is invoked with a [`WeakEntity`] to avoid leaking the entity for a long-running process. - /// It's also given an [`AsyncWindowContext`], which can be used to access the state of the entity across await points. - /// The returned future will be polled on the main thread. - #[track_caller] - pub fn spawn_in(&self, window: &Window, f: AsyncFn) -> Task - where - R: 'static, - AsyncFn: AsyncFnOnce(WeakEntity, &mut AsyncWindowContext) -> R + 'static, - { - let view = self.weak_entity(); - window.spawn(self, async move |cx| f(view, cx).await) - } - - /// Schedule a future to be run asynchronously with the given priority. - /// The given callback is invoked with a [`WeakEntity`] to avoid leaking the entity for a long-running process. - /// It's also given an [`AsyncWindowContext`], which can be used to access the state of the entity across await points. - /// The returned future will be polled on the main thread. - #[track_caller] - pub fn spawn_in_with_priority( - &self, - priority: Priority, - window: &Window, - f: AsyncFn, - ) -> Task - where - R: 'static, - AsyncFn: AsyncFnOnce(WeakEntity, &mut AsyncWindowContext) -> R + 'static, - { - let view = self.weak_entity(); - window.spawn_with_priority(priority, self, async move |cx| f(view, cx).await) - } - - /// Register a callback to be invoked when the given global state changes. - pub fn observe_global_in( - &mut self, - window: &Window, - mut f: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, - ) -> Subscription { - let window_handle = window.handle; - let view = self.weak_entity(); - let (subscription, activate) = self.global_observers.insert( - TypeId::of::(), - Box::new(move |cx| { - // If the entity has been dropped, remove this observer. - if view.upgrade().is_none() { - return false; - } - // If the window is unavailable (e.g. temporarily taken during a - // nested update, or already closed), skip this notification but - // keep the observer alive so it can fire on future changes. - let Ok(entity_alive) = window_handle.update(cx, |_, window, cx| { - view.update(cx, |view, cx| f(view, window, cx)).is_ok() - }) else { - return true; - }; - entity_alive - }), - ); - self.defer(move |_| activate()); - subscription - } - - /// Register a callback to be invoked when the given Action type is dispatched to the window. - pub fn on_action( - &mut self, - action_type: TypeId, - window: &mut Window, - listener: impl Fn(&mut T, &dyn Any, DispatchPhase, &mut Window, &mut Context) + 'static, - ) { - let handle = self.weak_entity(); - window.on_action(action_type, move |action, phase, window, cx| { - handle - .update(cx, |view, cx| { - listener(view, action, phase, window, cx); - }) - .ok(); - }); - } - - /// Move focus to the current view, assuming it implements [`Focusable`]. - pub fn focus_self(&mut self, window: &mut Window) - where - T: Focusable, - { - let view = self.entity(); - window.defer(self, move |window, cx| { - view.read(cx).focus_handle(cx).focus(window, cx) - }) - } -} - -impl Context<'_, T> { - /// Emit an event of the specified type, which can be handled by other entities that have subscribed via `subscribe` methods on their respective contexts. - pub fn emit(&mut self, event: Evt) - where - T: EventEmitter, - Evt: 'static, - { - let event = self - .event_arena - .alloc(|| event) - .map(|it| it as &mut dyn Any); - self.app.pending_effects.push_back(Effect::Emit { - emitter: self.entity_state.entity_id, - event_type: TypeId::of::(), - event, - }); - } -} - -impl AppContext for Context<'_, T> { - #[inline] - fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> U) -> Entity { - self.app.new(build_entity) - } - - #[inline] - fn reserve_entity(&mut self) -> Reservation { - self.app.reserve_entity() - } - - #[inline] - fn insert_entity( - &mut self, - reservation: Reservation, - build_entity: impl FnOnce(&mut Context) -> U, - ) -> Entity { - self.app.insert_entity(reservation, build_entity) - } - - #[inline] - fn update_entity( - &mut self, - handle: &Entity, - update: impl FnOnce(&mut U, &mut Context) -> R, - ) -> R { - self.app.update_entity(handle, update) - } - - #[inline] - fn as_mut<'a, E>(&'a mut self, handle: &Entity) -> super::GpuiBorrow<'a, E> - where - E: 'static, - { - self.app.as_mut(handle) - } - - #[inline] - fn read_entity(&self, handle: &Entity, read: impl FnOnce(&U, &App) -> R) -> R - where - U: 'static, - { - self.app.read_entity(handle, read) - } - - #[inline] - fn update_window(&mut self, window: AnyWindowHandle, update: F) -> Result - where - F: FnOnce(AnyView, &mut Window, &mut App) -> R, - { - self.app.update_window(window, update) - } - - #[inline] - fn with_window( - &mut self, - entity_id: EntityId, - f: impl FnOnce(&mut Window, &mut App) -> R, - ) -> Option { - self.app.with_window(entity_id, f) - } - - #[inline] - fn read_window( - &self, - window: &WindowHandle, - read: impl FnOnce(Entity, &App) -> R, - ) -> Result - where - U: 'static, - { - self.app.read_window(window, read) - } - - #[inline] - fn background_spawn(&self, future: impl Future + Send + 'static) -> Task - where - R: Send + 'static, - { - self.app.background_executor.spawn(future) - } - - #[inline] - fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R - where - G: Global, - { - self.app.read_global(callback) - } -} - -impl Borrow for Context<'_, T> { - fn borrow(&self) -> &App { - self.app - } -} - -impl BorrowMut for Context<'_, T> { - fn borrow_mut(&mut self) -> &mut App { - self.app - } -} diff --git a/crates/gpui_pre/src/app/entity_map.rs b/crates/gpui_pre/src/app/entity_map.rs deleted file mode 100644 index e4e9f3b..0000000 --- a/crates/gpui_pre/src/app/entity_map.rs +++ /dev/null @@ -1,1278 +0,0 @@ -use crate::{App, AppContext, GpuiBorrow, VisualContext, Window, seal::Sealed}; -use anyhow::{Context as _, Result}; -use collections::FxHashSet; -use derive_more::{Deref, DerefMut}; -use parking_lot::{RwLock, RwLockUpgradableReadGuard}; -use slotmap::{KeyData, SecondaryMap, SlotMap}; -use std::{ - any::{Any, TypeId, type_name}, - cell::RefCell, - cmp::Ordering, - fmt::{self, Display}, - hash::{Hash, Hasher}, - marker::PhantomData, - num::NonZeroU64, - sync::{ - Arc, Weak, - atomic::{AtomicU64, AtomicUsize, Ordering::SeqCst}, - }, - thread::panicking, -}; - -use super::Context; -use crate::util::atomic_incr_if_not_zero; -#[cfg(any(test, feature = "leak-detection"))] -use collections::HashMap; - -slotmap::new_key_type! { - /// A unique identifier for a entity across the application. - pub struct EntityId; -} - -impl From for EntityId { - fn from(value: u64) -> Self { - Self(KeyData::from_ffi(value)) - } -} - -impl EntityId { - /// Converts this entity id to a [NonZeroU64] - pub fn as_non_zero_u64(self) -> NonZeroU64 { - NonZeroU64::new(self.0.as_ffi()).unwrap() - } - - /// Converts this entity id to a [u64] - pub fn as_u64(self) -> u64 { - self.0.as_ffi() - } -} - -impl Display for EntityId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.as_u64()) - } -} - -pub(crate) struct EntityMap { - entities: SecondaryMap>, - pub accessed_entities: RefCell>, - ref_counts: Arc>, -} - -#[doc(hidden)] -pub(crate) struct EntityRefCounts { - counts: SlotMap, - dropped_entity_ids: Vec, - #[cfg(any(test, feature = "leak-detection"))] - leak_detector: LeakDetector, -} - -impl EntityMap { - pub fn new() -> Self { - Self { - entities: SecondaryMap::new(), - accessed_entities: RefCell::new(FxHashSet::default()), - ref_counts: Arc::new(RwLock::new(EntityRefCounts { - counts: SlotMap::with_key(), - dropped_entity_ids: Vec::new(), - #[cfg(any(test, feature = "leak-detection"))] - leak_detector: LeakDetector { - next_handle_id: 0, - entity_handles: HashMap::default(), - }, - })), - } - } - - #[doc(hidden)] - pub fn ref_counts_drop_handle(&self) -> Arc> { - self.ref_counts.clone() - } - - /// Captures a snapshot of all entities that currently have alive handles. - /// - /// The returned [`LeakDetectorSnapshot`] can later be passed to - /// [`assert_no_new_leaks`](Self::assert_no_new_leaks) to verify that no - /// entities created after the snapshot are still alive. - #[cfg(any(test, feature = "leak-detection"))] - pub fn leak_detector_snapshot(&self) -> LeakDetectorSnapshot { - self.ref_counts.read().leak_detector.snapshot() - } - - /// Asserts that no entities created after `snapshot` still have alive handles. - /// - /// See [`LeakDetector::assert_no_new_leaks`] for details. - #[cfg(any(test, feature = "leak-detection"))] - pub fn assert_no_new_leaks(&self, snapshot: &LeakDetectorSnapshot) { - self.ref_counts - .read() - .leak_detector - .assert_no_new_leaks(snapshot) - } - - /// Reserve a slot for an entity, which you can subsequently use with `insert`. - pub fn reserve(&self) -> Slot { - let id = self.ref_counts.write().counts.insert(1.into()); - Slot(Entity::new(id, Arc::downgrade(&self.ref_counts))) - } - - /// Insert an entity into a slot obtained by calling `reserve`. - pub fn insert(&mut self, slot: Slot, entity: T) -> Entity - where - T: 'static, - { - let mut accessed_entities = self.accessed_entities.get_mut(); - accessed_entities.insert(slot.entity_id); - - let handle = slot.0; - self.entities.insert(handle.entity_id, Box::new(entity)); - handle - } - - /// Move an entity to the stack. - #[track_caller] - pub fn lease(&mut self, pointer: &Entity) -> Lease { - self.assert_valid_context(pointer); - let mut accessed_entities = self.accessed_entities.get_mut(); - accessed_entities.insert(pointer.entity_id); - - let entity = Some( - self.entities - .remove(pointer.entity_id) - .unwrap_or_else(|| double_lease_panic::("update")), - ); - Lease { - entity, - id: pointer.entity_id, - entity_type: PhantomData, - } - } - - /// Returns an entity after moving it to the stack. - pub fn end_lease(&mut self, mut lease: Lease) { - self.entities.insert(lease.id, lease.entity.take().unwrap()); - } - - pub fn read(&self, entity: &Entity) -> &T { - self.assert_valid_context(entity); - let mut accessed_entities = self.accessed_entities.borrow_mut(); - accessed_entities.insert(entity.entity_id); - - self.entities - .get(entity.entity_id) - .and_then(|entity| entity.downcast_ref()) - .unwrap_or_else(|| double_lease_panic::("read")) - } - - fn assert_valid_context(&self, entity: &AnyEntity) { - debug_assert!( - Weak::ptr_eq(&entity.entity_map, &Arc::downgrade(&self.ref_counts)), - "used a entity with the wrong context" - ); - } - - pub fn extend_accessed(&mut self, entities: &FxHashSet) { - self.accessed_entities - .get_mut() - .extend(entities.iter().copied()); - } - - pub fn clear_accessed(&mut self) { - self.accessed_entities.get_mut().clear(); - } - - pub fn take_dropped(&mut self) -> Vec<(EntityId, Box)> { - let mut ref_counts = &mut *self.ref_counts.write(); - let dropped_entity_ids = ref_counts.dropped_entity_ids.drain(..); - let mut accessed_entities = self.accessed_entities.get_mut(); - - dropped_entity_ids - .filter_map(|entity_id| { - let count = ref_counts.counts.remove(entity_id).unwrap(); - debug_assert_eq!( - count.load(SeqCst), - 0, - "dropped an entity that was referenced" - ); - accessed_entities.remove(&entity_id); - // If the EntityId was allocated with `Context::reserve`, - // the entity may not have been inserted. - Some((entity_id, self.entities.remove(entity_id)?)) - }) - .collect() - } -} - -#[track_caller] -fn double_lease_panic(operation: &str) -> ! { - panic!( - "cannot {operation} {} while it is already being updated", - std::any::type_name::() - ) -} - -pub(crate) struct Lease { - entity: Option>, - pub id: EntityId, - entity_type: PhantomData, -} - -impl core::ops::Deref for Lease { - type Target = T; - - fn deref(&self) -> &Self::Target { - self.entity.as_ref().unwrap().downcast_ref().unwrap() - } -} - -impl core::ops::DerefMut for Lease { - fn deref_mut(&mut self) -> &mut Self::Target { - self.entity.as_mut().unwrap().downcast_mut().unwrap() - } -} - -impl Drop for Lease { - fn drop(&mut self) { - if self.entity.is_some() && !panicking() { - panic!("Leases must be ended with EntityMap::end_lease") - } - } -} - -#[derive(Deref, DerefMut)] -pub(crate) struct Slot(Entity); - -/// A dynamically typed reference to a entity, which can be downcast into a `Entity`. -pub struct AnyEntity { - pub(crate) entity_id: EntityId, - pub(crate) entity_type: TypeId, - entity_map: Weak>, - #[cfg(any(test, feature = "leak-detection"))] - handle_id: HandleId, -} - -impl AnyEntity { - fn new( - id: EntityId, - entity_type: TypeId, - entity_map: Weak>, - #[cfg(any(test, feature = "leak-detection"))] type_name: &'static str, - ) -> Self { - Self { - entity_id: id, - entity_type, - #[cfg(any(test, feature = "leak-detection"))] - handle_id: entity_map - .clone() - .upgrade() - .unwrap() - .write() - .leak_detector - .handle_created(id, Some(type_name)), - entity_map, - } - } - - /// Returns the id associated with this entity. - #[inline] - pub fn entity_id(&self) -> EntityId { - self.entity_id - } - - /// Returns the [TypeId] associated with this entity. - #[inline] - pub fn entity_type(&self) -> TypeId { - self.entity_type - } - - /// Converts this entity handle into a weak variant, which does not prevent it from being released. - pub fn downgrade(&self) -> AnyWeakEntity { - AnyWeakEntity { - entity_id: self.entity_id, - entity_type: self.entity_type, - entity_ref_counts: self.entity_map.clone(), - } - } - - /// Converts this entity handle into a strongly-typed entity handle of the given type. - /// If this entity handle is not of the specified type, returns itself as an error variant. - pub fn downcast(self) -> Result, AnyEntity> { - if TypeId::of::() == self.entity_type { - Ok(Entity { - any_entity: self, - entity_type: PhantomData, - }) - } else { - Err(self) - } - } -} - -impl Clone for AnyEntity { - fn clone(&self) -> Self { - if let Some(entity_map) = self.entity_map.upgrade() { - let entity_map = entity_map.read(); - let count = entity_map - .counts - .get(self.entity_id) - .expect("detected over-release of a entity"); - let prev_count = count.fetch_add(1, SeqCst); - assert_ne!(prev_count, 0, "Detected over-release of a entity."); - } - - Self { - entity_id: self.entity_id, - entity_type: self.entity_type, - entity_map: self.entity_map.clone(), - #[cfg(any(test, feature = "leak-detection"))] - handle_id: self - .entity_map - .upgrade() - .unwrap() - .write() - .leak_detector - .handle_created(self.entity_id, None), - } - } -} - -impl Drop for AnyEntity { - fn drop(&mut self) { - if let Some(entity_map) = self.entity_map.upgrade() { - let entity_map = entity_map.upgradable_read(); - let count = entity_map - .counts - .get(self.entity_id) - .expect("detected over-release of a handle."); - let prev_count = count.fetch_sub(1, SeqCst); - assert_ne!(prev_count, 0, "Detected over-release of a entity."); - if prev_count == 1 { - // We were the last reference to this entity, so we can remove it. - let mut entity_map = RwLockUpgradableReadGuard::upgrade(entity_map); - entity_map.dropped_entity_ids.push(self.entity_id); - } - } - - #[cfg(any(test, feature = "leak-detection"))] - if let Some(entity_map) = self.entity_map.upgrade() { - entity_map - .write() - .leak_detector - .handle_released(self.entity_id, self.handle_id) - } - } -} - -impl From> for AnyEntity { - #[inline] - fn from(entity: Entity) -> Self { - entity.any_entity - } -} - -impl Hash for AnyEntity { - #[inline] - fn hash(&self, state: &mut H) { - self.entity_id.hash(state); - } -} - -impl PartialEq for AnyEntity { - #[inline] - fn eq(&self, other: &Self) -> bool { - self.entity_id == other.entity_id - } -} - -impl Eq for AnyEntity {} - -impl Ord for AnyEntity { - #[inline] - fn cmp(&self, other: &Self) -> Ordering { - self.entity_id.cmp(&other.entity_id) - } -} - -impl PartialOrd for AnyEntity { - #[inline] - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl std::fmt::Debug for AnyEntity { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("AnyEntity") - .field("entity_id", &self.entity_id.as_u64()) - .finish() - } -} - -/// A strong, well-typed reference to a struct which is managed -/// by GPUI -#[derive(Deref, DerefMut)] -pub struct Entity { - #[deref] - #[deref_mut] - pub(crate) any_entity: AnyEntity, - pub(crate) entity_type: PhantomData T>, -} - -impl Sealed for Entity {} - -impl Entity { - #[inline] - fn new(id: EntityId, entity_map: Weak>) -> Self - where - T: 'static, - { - Self { - any_entity: AnyEntity::new( - id, - TypeId::of::(), - entity_map, - #[cfg(any(test, feature = "leak-detection"))] - std::any::type_name::(), - ), - entity_type: PhantomData, - } - } - - /// Get the entity ID associated with this entity - #[inline] - pub fn entity_id(&self) -> EntityId { - self.any_entity.entity_id - } - - /// Downgrade this entity pointer to a non-retaining weak pointer - #[inline] - pub fn downgrade(&self) -> WeakEntity { - WeakEntity { - any_entity: self.any_entity.downgrade(), - entity_type: self.entity_type, - } - } - - /// Convert this into a dynamically typed entity. - #[inline] - pub fn into_any(self) -> AnyEntity { - self.any_entity - } - - /// Grab a reference to this entity from the context. - #[inline] - pub fn read<'a>(&self, cx: &'a App) -> &'a T { - cx.entities.read(self) - } - - /// Read the entity referenced by this handle with the given function. - #[inline] - pub fn read_with(&self, cx: &C, f: impl FnOnce(&T, &App) -> R) -> R { - cx.read_entity(self, f) - } - - /// Updates the entity referenced by this handle with the given function. - #[inline] - pub fn update( - &self, - cx: &mut C, - update: impl FnOnce(&mut T, &mut Context) -> R, - ) -> R { - cx.update_entity(self, update) - } - - /// Updates the entity referenced by this handle with the given function. - #[inline] - pub fn as_mut<'a, C: AppContext>(&self, cx: &'a mut C) -> GpuiBorrow<'a, T> { - cx.as_mut(self) - } - - /// Updates the entity referenced by this handle with the given function. - pub fn write(&self, cx: &mut C, value: T) { - self.update(cx, |entity, cx| { - *entity = value; - cx.notify(); - }) - } - - /// Updates the entity referenced by this handle with the given function if - /// the referenced entity still exists, within a visual context that has a window. - /// Returns an error if the window has been closed. - #[inline] - pub fn update_in( - &self, - cx: &mut C, - update: impl FnOnce(&mut T, &mut Window, &mut Context) -> R, - ) -> C::Result { - cx.update_window_entity(self, update) - } -} - -impl Clone for Entity { - #[inline] - fn clone(&self) -> Self { - Self { - any_entity: self.any_entity.clone(), - entity_type: self.entity_type, - } - } -} - -impl std::fmt::Debug for Entity { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Entity") - .field("entity_id", &self.any_entity.entity_id) - .field("entity_type", &type_name::()) - .finish() - } -} - -impl Hash for Entity { - #[inline] - fn hash(&self, state: &mut H) { - self.any_entity.hash(state); - } -} - -impl PartialEq for Entity { - #[inline] - fn eq(&self, other: &Self) -> bool { - self.any_entity == other.any_entity - } -} - -impl Eq for Entity {} - -impl PartialEq> for Entity { - #[inline] - fn eq(&self, other: &WeakEntity) -> bool { - self.any_entity.entity_id() == other.entity_id() - } -} - -impl Ord for Entity { - #[inline] - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.entity_id().cmp(&other.entity_id()) - } -} - -impl PartialOrd for Entity { - #[inline] - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -/// A type erased, weak reference to a entity. -#[derive(Clone)] -pub struct AnyWeakEntity { - pub(crate) entity_id: EntityId, - entity_type: TypeId, - entity_ref_counts: Weak>, -} - -impl AnyWeakEntity { - /// Get the entity ID associated with this weak reference. - #[inline] - pub fn entity_id(&self) -> EntityId { - self.entity_id - } - - /// Check if this weak handle can be upgraded, or if the entity has already been dropped - pub fn is_upgradable(&self) -> bool { - let ref_count = self - .entity_ref_counts - .upgrade() - .and_then(|ref_counts| Some(ref_counts.read().counts.get(self.entity_id)?.load(SeqCst))) - .unwrap_or(0); - ref_count > 0 - } - - /// Upgrade this weak entity reference to a strong reference. - pub fn upgrade(&self) -> Option { - let ref_counts = &self.entity_ref_counts.upgrade()?; - let ref_counts = ref_counts.read(); - let ref_count = ref_counts.counts.get(self.entity_id)?; - - if atomic_incr_if_not_zero(ref_count) == 0 { - // entity_id is in dropped_entity_ids - return None; - } - drop(ref_counts); - - Some(AnyEntity { - entity_id: self.entity_id, - entity_type: self.entity_type, - entity_map: self.entity_ref_counts.clone(), - #[cfg(any(test, feature = "leak-detection"))] - handle_id: self - .entity_ref_counts - .upgrade() - .unwrap() - .write() - .leak_detector - .handle_created(self.entity_id, None), - }) - } - - /// Asserts that the entity referenced by this weak handle has been fully released. - /// - /// # Example - /// - /// ```ignore - /// let entity = cx.new(|_| MyEntity::new()); - /// let weak = entity.downgrade(); - /// drop(entity); - /// - /// // Verify the entity was released - /// weak.assert_released(); - /// ``` - /// - /// # Debugging Leaks - /// - /// If this method panics due to leaked handles, set the `LEAK_BACKTRACE` environment - /// variable to see where the leaked handles were allocated: - /// - /// ```bash - /// LEAK_BACKTRACE=1 cargo test my_test - /// ``` - /// - /// # Panics - /// - /// - Panics if any strong handles to the entity are still alive. - /// - Panics if the entity was recently dropped but cleanup hasn't completed yet - /// (resources are retained until the end of the effect cycle). - #[cfg(any(test, feature = "leak-detection"))] - pub fn assert_released(&self) { - self.entity_ref_counts - .upgrade() - .unwrap() - .write() - .leak_detector - .assert_released(self.entity_id); - - if self - .entity_ref_counts - .upgrade() - .and_then(|ref_counts| Some(ref_counts.read().counts.get(self.entity_id)?.load(SeqCst))) - .is_some() - { - panic!( - "entity was recently dropped but resources are retained until the end of the effect cycle." - ) - } - } - - /// Creates a weak entity that can never be upgraded. - pub fn new_invalid() -> Self { - /// To hold the invariant that all ids are unique, and considering that slotmap - /// increases their IDs from `0`, we can decrease ours from `u64::MAX` so these - /// two will never conflict (u64 is way too large). - static UNIQUE_NON_CONFLICTING_ID_GENERATOR: AtomicU64 = AtomicU64::new(u64::MAX); - let entity_id = UNIQUE_NON_CONFLICTING_ID_GENERATOR.fetch_sub(1, SeqCst); - - Self { - // Safety: - // Docs say this is safe but can be unspecified if slotmap changes the representation - // after `1.0.7`, that said, providing a valid entity_id here is not necessary as long - // as we guarantee that `entity_id` is never used if `entity_ref_counts` equals - // to `Weak::new()` (that is, it's unable to upgrade), that is the invariant that - // actually needs to be hold true. - // - // And there is no sane reason to read an entity slot if `entity_ref_counts` can't be - // read in the first place, so we're good! - entity_id: entity_id.into(), - entity_type: TypeId::of::<()>(), - entity_ref_counts: Weak::new(), - } - } -} - -impl std::fmt::Debug for AnyWeakEntity { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct(type_name::()) - .field("entity_id", &self.entity_id) - .field("entity_type", &self.entity_type) - .finish() - } -} - -impl From> for AnyWeakEntity { - #[inline] - fn from(entity: WeakEntity) -> Self { - entity.any_entity - } -} - -impl Hash for AnyWeakEntity { - #[inline] - fn hash(&self, state: &mut H) { - self.entity_id.hash(state); - } -} - -impl PartialEq for AnyWeakEntity { - #[inline] - fn eq(&self, other: &Self) -> bool { - self.entity_id == other.entity_id - } -} - -impl Eq for AnyWeakEntity {} - -impl Ord for AnyWeakEntity { - #[inline] - fn cmp(&self, other: &Self) -> Ordering { - self.entity_id.cmp(&other.entity_id) - } -} - -impl PartialOrd for AnyWeakEntity { - #[inline] - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -/// A weak reference to a entity of the given type. -#[derive(Deref, DerefMut)] -pub struct WeakEntity { - #[deref] - #[deref_mut] - any_entity: AnyWeakEntity, - entity_type: PhantomData T>, -} - -impl std::fmt::Debug for WeakEntity { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct(type_name::()) - .field("entity_id", &self.any_entity.entity_id) - .field("entity_type", &type_name::()) - .finish() - } -} - -impl Clone for WeakEntity { - fn clone(&self) -> Self { - Self { - any_entity: self.any_entity.clone(), - entity_type: self.entity_type, - } - } -} - -impl WeakEntity { - /// Upgrade this weak entity reference into a strong entity reference - pub fn upgrade(&self) -> Option> { - Some(Entity { - any_entity: self.any_entity.upgrade()?, - entity_type: self.entity_type, - }) - } - - /// Updates the entity referenced by this handle with the given function if - /// the referenced entity still exists. Returns an error if the entity has - /// been released. - pub fn update( - &self, - cx: &mut C, - update: impl FnOnce(&mut T, &mut Context) -> R, - ) -> Result - where - C: AppContext, - { - let entity = self.upgrade().context("entity released")?; - Ok(cx.update_entity(&entity, update)) - } - - /// Updates the entity referenced by this handle with the given function if - /// the referenced entity still exists, within a visual context that has a window. - /// Returns an error if the entity has been released. - pub fn update_in( - &self, - cx: &mut C, - update: impl FnOnce(&mut T, &mut Window, &mut Context) -> R, - ) -> Result - where - C: AppContext, - { - let entity = self.upgrade().context("entity released")?; - cx.with_window(entity.entity_id(), |window, app| { - entity.update(app, |entity, cx| update(entity, window, cx)) - }) - .context("entity has no current window") - } - - /// Reads the entity referenced by this handle with the given function if - /// the referenced entity still exists. Returns an error if the entity has - /// been released. - pub fn read_with(&self, cx: &C, read: impl FnOnce(&T, &App) -> R) -> Result - where - C: AppContext, - { - let entity = self.upgrade().context("entity released")?; - Ok(cx.read_entity(&entity, read)) - } - - /// Create a new weak entity that can never be upgraded. - #[inline] - pub fn new_invalid() -> Self { - Self { - any_entity: AnyWeakEntity::new_invalid(), - entity_type: PhantomData, - } - } -} - -impl Hash for WeakEntity { - #[inline] - fn hash(&self, state: &mut H) { - self.any_entity.hash(state); - } -} - -impl PartialEq for WeakEntity { - #[inline] - fn eq(&self, other: &Self) -> bool { - self.any_entity == other.any_entity - } -} - -impl Eq for WeakEntity {} - -impl PartialEq> for WeakEntity { - #[inline] - fn eq(&self, other: &Entity) -> bool { - self.entity_id() == other.any_entity.entity_id() - } -} - -impl Ord for WeakEntity { - #[inline] - fn cmp(&self, other: &Self) -> Ordering { - self.entity_id().cmp(&other.entity_id()) - } -} - -impl PartialOrd for WeakEntity { - #[inline] - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -/// Controls whether backtraces are captured when entity handles are created. -/// -/// Set the `LEAK_BACKTRACE` environment variable to any non-empty value to enable -/// backtrace capture. This helps identify where leaked handles were allocated. -#[cfg(any(test, feature = "leak-detection"))] -static LEAK_BACKTRACE: std::sync::LazyLock = - std::sync::LazyLock::new(|| std::env::var("LEAK_BACKTRACE").is_ok_and(|b| !b.is_empty())); - -/// Unique identifier for a specific entity handle instance. -/// -/// This is distinct from `EntityId` - while multiple handles can point to the same -/// entity (same `EntityId`), each handle has its own unique `HandleId`. -#[cfg(any(test, feature = "leak-detection"))] -#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)] -pub(crate) struct HandleId { - id: u64, -} - -/// Tracks entity handle allocations to detect leaks. -/// -/// The leak detector is enabled in tests and when the `leak-detection` feature is active. -/// It tracks every `Entity` and `AnyEntity` handle that is created and released, -/// allowing you to verify that all handles to an entity have been properly dropped. -/// -/// # How do leaks happen? -/// -/// Entities are reference-counted structures that can own other entities -/// allowing to form cycles. If such a strong-reference counted cycle is -/// created, all participating strong entities in this cycle will effectively -/// leak as they cannot be released anymore. -/// -/// Cycles can also happen if an entity owns a task or subscription that it -/// itself owns a strong reference to the entity again. -/// -/// # Usage -/// -/// You can use `WeakEntity::assert_released` or `AnyWeakEntity::assert_released` -/// to verify that an entity has been fully released: -/// -/// ```ignore -/// let entity = cx.new(|_| MyEntity::new()); -/// let weak = entity.downgrade(); -/// drop(entity); -/// -/// // This will panic if any handles to the entity are still alive -/// weak.assert_released(); -/// ``` -/// -/// # Debugging Leaks -/// -/// When a leak is detected, the detector will panic with information about the leaked -/// handles. To see where the leaked handles were allocated, set the `LEAK_BACKTRACE` -/// environment variable: -/// -/// ```bash -/// LEAK_BACKTRACE=1 cargo test my_test -/// ``` -/// -/// This will capture and display backtraces for each leaked handle, helping you -/// identify where leaked handles were created. -/// -/// # How It Works -/// -/// - When an entity handle is created (via `Entity::new`, `Entity::clone`, or -/// `WeakEntity::upgrade`), `handle_created` is called to register the handle. -/// - When a handle is dropped, `handle_released` removes it from tracking. -/// - `assert_released` verifies that no handles remain for a given entity. -#[cfg(any(test, feature = "leak-detection"))] -pub(crate) struct LeakDetector { - next_handle_id: u64, - entity_handles: HashMap, -} - -/// A snapshot of the set of alive entities at a point in time. -/// -/// Created by [`LeakDetector::snapshot`]. Can later be passed to -/// [`LeakDetector::assert_no_new_leaks`] to verify that no new entity -/// handles remain between the snapshot and the current state. -#[cfg(any(test, feature = "leak-detection"))] -pub struct LeakDetectorSnapshot { - entity_ids: collections::HashSet, -} - -#[cfg(any(test, feature = "leak-detection"))] -struct EntityLeakData { - handles: HashMap>, - type_name: &'static str, -} - -#[cfg(any(test, feature = "leak-detection"))] -impl LeakDetector { - /// Records that a new handle has been created for the given entity. - /// - /// Returns a unique `HandleId` that must be passed to `handle_released` when - /// the handle is dropped. If `LEAK_BACKTRACE` is set, captures a backtrace - /// at the allocation site. - #[track_caller] - pub fn handle_created( - &mut self, - entity_id: EntityId, - type_name: Option<&'static str>, - ) -> HandleId { - let id = gpui_util::post_inc(&mut self.next_handle_id); - let handle_id = HandleId { id }; - let handles = self - .entity_handles - .entry(entity_id) - .or_insert_with(|| EntityLeakData { - handles: HashMap::default(), - type_name: type_name.unwrap_or(""), - }); - handles.handles.insert( - handle_id, - LEAK_BACKTRACE.then(backtrace::Backtrace::new_unresolved), - ); - handle_id - } - - /// Records that a handle has been released (dropped). - /// - /// This removes the handle from tracking. The `handle_id` should be the same - /// one returned by `handle_created` when the handle was allocated. - pub fn handle_released(&mut self, entity_id: EntityId, handle_id: HandleId) { - if let std::collections::hash_map::Entry::Occupied(mut data) = - self.entity_handles.entry(entity_id) - { - data.get_mut().handles.remove(&handle_id); - if data.get().handles.is_empty() { - data.remove(); - } - } - } - - /// Asserts that all handles to the given entity have been released. - /// - /// # Panics - /// - /// Panics if any handles to the entity are still alive. The panic message - /// includes backtraces for each leaked handle if `LEAK_BACKTRACE` is set, - /// otherwise it suggests setting the environment variable to get more info. - pub fn assert_released(&mut self, entity_id: EntityId) { - use std::fmt::Write as _; - - if let Some(data) = self.entity_handles.remove(&entity_id) { - let mut out = String::new(); - for (_, backtrace) in data.handles { - if let Some(mut backtrace) = backtrace { - backtrace.resolve(); - let backtrace = BacktraceFormatter(backtrace); - writeln!(out, "Leaked handle:\n{:?}", backtrace).unwrap(); - } else { - writeln!( - out, - "Leaked handle: (export LEAK_BACKTRACE to find allocation site)" - ) - .unwrap(); - } - } - panic!("Handles for {} leaked:\n{out}", data.type_name); - } - } - - /// Captures a snapshot of all entity IDs that currently have alive handles. - /// - /// The returned [`LeakDetectorSnapshot`] can later be passed to - /// [`assert_no_new_leaks`](Self::assert_no_new_leaks) to verify that no - /// entities created after the snapshot are still alive. - pub fn snapshot(&self) -> LeakDetectorSnapshot { - LeakDetectorSnapshot { - entity_ids: self.entity_handles.keys().copied().collect(), - } - } - - /// Asserts that no entities created after `snapshot` still have alive handles. - /// - /// Entities that were already tracked at the time of the snapshot are ignored, - /// even if they still have handles. Only *new* entities (those whose - /// `EntityId` was not present in the snapshot) are considered leaks. - /// - /// # Panics - /// - /// Panics if any new entity handles exist. The panic message lists every - /// leaked entity with its type name, and includes allocation-site backtraces - /// when `LEAK_BACKTRACE` is set. - pub fn assert_no_new_leaks(&self, snapshot: &LeakDetectorSnapshot) { - use std::fmt::Write as _; - - let mut out = String::new(); - for (entity_id, data) in &self.entity_handles { - if snapshot.entity_ids.contains(entity_id) { - continue; - } - for (_, backtrace) in &data.handles { - if let Some(backtrace) = backtrace { - let mut backtrace = backtrace.clone(); - backtrace.resolve(); - let backtrace = BacktraceFormatter(backtrace); - writeln!( - out, - "Leaked handle for entity {} ({entity_id:?}):\n{:?}", - data.type_name, backtrace - ) - .unwrap(); - } else { - writeln!( - out, - "Leaked handle for entity {} ({entity_id:?}): (export LEAK_BACKTRACE to find allocation site)", - data.type_name - ) - .unwrap(); - } - } - } - - if !out.is_empty() { - panic!("New entity leaks detected since snapshot:\n{out}"); - } - } -} - -#[cfg(any(test, feature = "leak-detection"))] -impl Drop for LeakDetector { - fn drop(&mut self) { - use std::fmt::Write; - - if self.entity_handles.is_empty() || std::thread::panicking() { - return; - } - - let mut out = String::new(); - for (entity_id, data) in self.entity_handles.drain() { - for (_handle, backtrace) in data.handles { - if let Some(mut backtrace) = backtrace { - backtrace.resolve(); - let backtrace = BacktraceFormatter(backtrace); - writeln!( - out, - "Leaked handle for entity {} ({entity_id:?}):\n{:?}", - data.type_name, backtrace - ) - .unwrap(); - } else { - writeln!( - out, - "Leaked handle for entity {} ({entity_id:?}): (export LEAK_BACKTRACE to find allocation site)", - data.type_name - ) - .unwrap(); - } - } - } - panic!("Exited with leaked handles:\n{out}"); - } -} - -#[cfg(any(test, feature = "leak-detection"))] -struct BacktraceFormatter(backtrace::Backtrace); - -#[cfg(any(test, feature = "leak-detection"))] -impl fmt::Debug for BacktraceFormatter { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - use backtrace::{BacktraceFmt, BytesOrWideString, PrintFmt}; - - let style = if fmt.alternate() { - PrintFmt::Full - } else { - PrintFmt::Short - }; - - // When printing paths we try to strip the cwd if it exists, otherwise - // we just print the path as-is. Note that we also only do this for the - // short format, because if it's full we presumably want to print - // everything. - let cwd = std::env::current_dir(); - let mut print_path = move |fmt: &mut fmt::Formatter<'_>, path: BytesOrWideString<'_>| { - let path = path.into_path_buf(); - if style != PrintFmt::Full { - if let Ok(cwd) = &cwd { - if let Ok(suffix) = path.strip_prefix(cwd) { - return fmt::Display::fmt(&suffix.display(), fmt); - } - } - } - fmt::Display::fmt(&path.display(), fmt) - }; - - let mut f = BacktraceFmt::new(fmt, style, &mut print_path); - f.add_context()?; - let mut strip = true; - for frame in self.0.frames() { - if let [symbol, ..] = frame.symbols() - && let Some(name) = symbol.name() - && let Some(filename) = name.as_str() - { - match filename { - "test::run_test_in_process" - | "scheduler::executor::spawn_local_with_source_location::impl$1::poll > > > >,alloc::alloc::Global> > >" => { - strip = true - } - "gpui::app::entity_map::LeakDetector::handle_created" => { - strip = false; - continue; - } - "zed::main" => { - strip = true; - f.frame().backtrace_frame(frame)?; - } - _ => {} - } - } - if strip { - continue; - } - f.frame().backtrace_frame(frame)?; - } - f.finish()?; - Ok(()) - } -} - -#[cfg(test)] -mod test { - use crate::EntityMap; - - struct TestEntity { - pub i: i32, - } - - #[test] - fn test_entity_map_slot_assignment_before_cleanup() { - // Tests that slots are not re-used before take_dropped. - let mut entity_map = EntityMap::new(); - - let slot = entity_map.reserve::(); - entity_map.insert(slot, TestEntity { i: 1 }); - - let slot = entity_map.reserve::(); - entity_map.insert(slot, TestEntity { i: 2 }); - - let dropped = entity_map.take_dropped(); - assert_eq!(dropped.len(), 2); - - assert_eq!( - dropped - .into_iter() - .map(|(_, entity)| entity.downcast::().unwrap().i) - .collect::>(), - vec![1, 2], - ); - } - - #[test] - fn test_entity_map_weak_upgrade_before_cleanup() { - // Tests that weak handles are not upgraded before take_dropped - let mut entity_map = EntityMap::new(); - - let slot = entity_map.reserve::(); - let handle = entity_map.insert(slot, TestEntity { i: 1 }); - let weak = handle.downgrade(); - drop(handle); - - let strong = weak.upgrade(); - assert_eq!(strong, None); - - let dropped = entity_map.take_dropped(); - assert_eq!(dropped.len(), 1); - - assert_eq!( - dropped - .into_iter() - .map(|(_, entity)| entity.downcast::().unwrap().i) - .collect::>(), - vec![1], - ); - } - - #[test] - fn test_leak_detector_snapshot_no_leaks() { - let mut entity_map = EntityMap::new(); - - let slot = entity_map.reserve::(); - let pre_existing = entity_map.insert(slot, TestEntity { i: 1 }); - - let snapshot = entity_map.leak_detector_snapshot(); - - let slot = entity_map.reserve::(); - let temporary = entity_map.insert(slot, TestEntity { i: 2 }); - drop(temporary); - - entity_map.assert_no_new_leaks(&snapshot); - - drop(pre_existing); - } - - #[test] - #[should_panic(expected = "New entity leaks detected since snapshot")] - fn test_leak_detector_snapshot_detects_new_leak() { - let mut entity_map = EntityMap::new(); - - let slot = entity_map.reserve::(); - let pre_existing = entity_map.insert(slot, TestEntity { i: 1 }); - - let snapshot = entity_map.leak_detector_snapshot(); - - let slot = entity_map.reserve::(); - let leaked = entity_map.insert(slot, TestEntity { i: 2 }); - - // `leaked` is still alive, so this should panic. - entity_map.assert_no_new_leaks(&snapshot); - - drop(pre_existing); - drop(leaked); - } -} diff --git a/crates/gpui_pre/src/app/headless_app_context.rs b/crates/gpui_pre/src/app/headless_app_context.rs deleted file mode 100644 index b21e64f..0000000 --- a/crates/gpui_pre/src/app/headless_app_context.rs +++ /dev/null @@ -1,284 +0,0 @@ -//! Cross-platform headless app context for tests that need real text shaping. -//! -//! This replaces the macOS-only `HeadlessMetalAppContext` with a platform-neutral -//! implementation backed by `TestPlatform`. Tests supply a real `PlatformTextSystem` -//! (e.g. `DirectWriteTextSystem` on Windows, `MacTextSystem` on macOS) to get -//! accurate glyph measurements while keeping everything else deterministic. -//! -//! Optionally, a renderer factory can be provided to enable real GPU rendering -//! and screenshot capture via [`HeadlessAppContext::capture_screenshot`]. - -use crate::{ - AnyView, AnyWindowHandle, App, AppCell, AppContext, AssetSource, BackgroundExecutor, Bounds, - Context, Entity, EntityId, ForegroundExecutor, Global, Pixels, PlatformHeadlessRenderer, - PlatformTextSystem, Render, Reservation, Size, Task, TestDispatcher, TestPlatform, TextSystem, - Window, WindowBounds, WindowHandle, WindowOptions, - app::{GpuiBorrow, GpuiMode}, -}; -use anyhow::Result; -use image::RgbaImage; -use std::{future::Future, rc::Rc, sync::Arc, time::Duration}; - -/// A cross-platform headless app context for tests that need real text shaping. -/// -/// Unlike the old `HeadlessMetalAppContext`, this works on any platform. It uses -/// `TestPlatform` for deterministic scheduling and accepts a pluggable -/// `PlatformTextSystem` so tests get real glyph measurements. -/// -/// # Usage -/// -/// ```ignore -/// let text_system = Arc::new(gpui_wgpu::CosmicTextSystem::new("fallback")); -/// let mut cx = HeadlessAppContext::with_platform( -/// text_system, -/// Arc::new(Assets), -/// || gpui_platform::current_headless_renderer(), -/// ); -/// ``` -pub struct HeadlessAppContext { - /// The underlying app cell. - pub app: Rc, - /// The background executor for running async tasks. - pub background_executor: BackgroundExecutor, - /// The foreground executor for running tasks on the main thread. - pub foreground_executor: ForegroundExecutor, - dispatcher: TestDispatcher, - text_system: Arc, -} - -impl HeadlessAppContext { - /// Creates a new headless app context with the given text system. - pub fn new(platform_text_system: Arc) -> Self { - Self::with_platform(platform_text_system, Arc::new(()), || None) - } - - /// Creates a new headless app context with a custom text system and asset source. - pub fn with_asset_source( - platform_text_system: Arc, - asset_source: Arc, - ) -> Self { - Self::with_platform(platform_text_system, asset_source, || None) - } - - /// Creates a new headless app context with the given text system, asset source, - /// and an optional renderer factory for screenshot support. - pub fn with_platform( - platform_text_system: Arc, - asset_source: Arc, - renderer_factory: impl Fn() -> Option> + 'static, - ) -> Self { - let seed = std::env::var("SEED") - .ok() - .and_then(|s| s.parse().ok()) - .unwrap_or(0); - - let dispatcher = TestDispatcher::new(seed); - let arc_dispatcher = Arc::new(dispatcher.clone()); - let background_executor = BackgroundExecutor::new(arc_dispatcher.clone()); - let foreground_executor = ForegroundExecutor::new(arc_dispatcher); - - let renderer_factory: Box Option>> = - Box::new(renderer_factory); - let platform = TestPlatform::with_platform( - background_executor.clone(), - foreground_executor.clone(), - platform_text_system.clone(), - Some(renderer_factory), - ); - - let text_system = Arc::new(TextSystem::new(platform_text_system)); - let http_client = http_client::FakeHttpClient::with_404_response(); - let app = App::new_app(platform, asset_source, http_client); - app.borrow_mut().mode = GpuiMode::test(); - - Self { - app, - background_executor, - foreground_executor, - dispatcher, - text_system, - } - } - - /// Opens a window for headless rendering. - pub fn open_window( - &mut self, - size: Size, - build_root: impl FnOnce(&mut Window, &mut App) -> Entity, - ) -> Result> { - use crate::{point, px}; - - let bounds = Bounds { - origin: point(px(0.0), px(0.0)), - size, - }; - - let mut cx = self.app.borrow_mut(); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - focus: false, - show: false, - ..Default::default() - }, - build_root, - ) - } - - /// Runs all pending tasks until parked. - pub fn run_until_parked(&self) { - self.dispatcher.run_until_parked(); - } - - /// Advances the simulated clock. - pub fn advance_clock(&self, duration: Duration) { - self.dispatcher.advance_clock(duration); - } - - /// Enables parking mode, allowing blocking on real I/O (e.g., async asset loading). - pub fn allow_parking(&self) { - self.dispatcher.allow_parking(); - } - - /// Disables parking mode, returning to deterministic test execution. - pub fn forbid_parking(&self) { - self.dispatcher.forbid_parking(); - } - - /// Updates app state. - pub fn update(&mut self, f: impl FnOnce(&mut App) -> R) -> R { - let mut app = self.app.borrow_mut(); - f(&mut app) - } - - /// Updates a window and calls draw to render. - pub fn update_window( - &mut self, - window: AnyWindowHandle, - f: impl FnOnce(AnyView, &mut Window, &mut App) -> R, - ) -> Result { - let mut app = self.app.borrow_mut(); - app.update_window(window, f) - } - - /// Captures a screenshot from a window. - /// - /// Requires that the context was created with a renderer factory that - /// returns `Some` via [`HeadlessAppContext::with_platform`]. - pub fn capture_screenshot(&mut self, window: AnyWindowHandle) -> Result { - let mut app = self.app.borrow_mut(); - app.update_window(window, |_, window, _| window.render_to_image())? - } - - /// Returns the text system. - pub fn text_system(&self) -> &Arc { - &self.text_system - } - - /// Returns the background executor. - pub fn background_executor(&self) -> &BackgroundExecutor { - &self.background_executor - } - - /// Returns the foreground executor. - pub fn foreground_executor(&self) -> &ForegroundExecutor { - &self.foreground_executor - } -} - -impl Drop for HeadlessAppContext { - fn drop(&mut self) { - // Shut down the app so windows are closed and entity handles are - // released before the LeakDetector runs. - self.app.borrow_mut().shutdown(); - } -} - -impl AppContext for HeadlessAppContext { - fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity { - let mut app = self.app.borrow_mut(); - app.new(build_entity) - } - - fn reserve_entity(&mut self) -> Reservation { - let mut app = self.app.borrow_mut(); - app.reserve_entity() - } - - fn insert_entity( - &mut self, - reservation: Reservation, - build_entity: impl FnOnce(&mut Context) -> T, - ) -> Entity { - let mut app = self.app.borrow_mut(); - app.insert_entity(reservation, build_entity) - } - - fn update_entity( - &mut self, - handle: &Entity, - update: impl FnOnce(&mut T, &mut Context) -> R, - ) -> R { - let mut app = self.app.borrow_mut(); - app.update_entity(handle, update) - } - - fn as_mut<'a, T>(&'a mut self, _: &Entity) -> GpuiBorrow<'a, T> - where - T: 'static, - { - panic!("Cannot use as_mut with HeadlessAppContext. Call update() instead.") - } - - fn read_entity(&self, handle: &Entity, read: impl FnOnce(&T, &App) -> R) -> R - where - T: 'static, - { - let app = self.app.borrow(); - app.read_entity(handle, read) - } - - fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result - where - F: FnOnce(AnyView, &mut Window, &mut App) -> T, - { - let mut lock = self.app.borrow_mut(); - lock.update_window(window, f) - } - - fn with_window( - &mut self, - entity_id: EntityId, - f: impl FnOnce(&mut Window, &mut App) -> R, - ) -> Option { - let mut lock = self.app.borrow_mut(); - lock.with_window(entity_id, f) - } - - fn read_window( - &self, - window: &WindowHandle, - read: impl FnOnce(Entity, &App) -> R, - ) -> Result - where - T: 'static, - { - let app = self.app.borrow(); - app.read_window(window, read) - } - - fn background_spawn(&self, future: impl Future + Send + 'static) -> Task - where - R: Send + 'static, - { - self.background_executor.spawn(future) - } - - fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R - where - G: Global, - { - let app = self.app.borrow(); - app.read_global(callback) - } -} diff --git a/crates/gpui_pre/src/app/test_app.rs b/crates/gpui_pre/src/app/test_app.rs deleted file mode 100644 index d241863..0000000 --- a/crates/gpui_pre/src/app/test_app.rs +++ /dev/null @@ -1,607 +0,0 @@ -//! A clean testing API for GPUI applications. -//! -//! `TestApp` provides a simpler alternative to `TestAppContext` with: -//! - Automatic effect flushing after updates -//! - Clean window creation and inspection -//! - Input simulation helpers -//! -//! # Example -//! ```ignore -//! #[test] -//! fn test_my_view() { -//! let mut app = TestApp::new(); -//! -//! let mut window = app.open_window(|window, cx| { -//! MyView::new(window, cx) -//! }); -//! -//! window.update(|view, window, cx| { -//! view.do_something(cx); -//! }); -//! -//! // Check rendered state -//! assert_eq!(window.title(), Some("Expected Title")); -//! } -//! ``` - -use crate::{ - AnyWindowHandle, App, AppCell, AppContext, AsyncApp, BackgroundExecutor, BorrowAppContext, - Bounds, ClipboardItem, Context, Entity, ForegroundExecutor, Global, InputEvent, Keystroke, - MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Platform, - PlatformTextSystem, Point, Render, Size, Task, TestDispatcher, TestPlatform, TextSystem, - Window, WindowBounds, WindowHandle, WindowOptions, app::GpuiMode, -}; -use std::{future::Future, rc::Rc, sync::Arc, time::Duration}; - -/// A test application context with a clean API. -/// -/// Unlike `TestAppContext`, `TestApp` automatically flushes effects after -/// each update and provides simpler window management. -pub struct TestApp { - app: Rc, - platform: Rc, - background_executor: BackgroundExecutor, - foreground_executor: ForegroundExecutor, - #[allow(dead_code)] - dispatcher: TestDispatcher, - text_system: Arc, -} - -impl TestApp { - /// Create a new test application. - pub fn new() -> Self { - Self::with_seed(0) - } - - /// Create a new test application with a specific random seed. - pub fn with_seed(seed: u64) -> Self { - Self::build(seed, None, Arc::new(())) - } - - /// Create a new test application with a custom text system for real font shaping. - pub fn with_text_system(text_system: Arc) -> Self { - Self::build(0, Some(text_system), Arc::new(())) - } - - /// Create a new test application with a custom text system and asset source. - pub fn with_text_system_and_assets( - text_system: Arc, - asset_source: Arc, - ) -> Self { - Self::build(0, Some(text_system), asset_source) - } - - fn build( - seed: u64, - platform_text_system: Option>, - asset_source: Arc, - ) -> Self { - let dispatcher = TestDispatcher::new(seed); - let arc_dispatcher = Arc::new(dispatcher.clone()); - let background_executor = BackgroundExecutor::new(arc_dispatcher.clone()); - let foreground_executor = ForegroundExecutor::new(arc_dispatcher); - let platform = match platform_text_system.clone() { - Some(ts) => TestPlatform::with_text_system( - background_executor.clone(), - foreground_executor.clone(), - ts, - ), - None => TestPlatform::new(background_executor.clone(), foreground_executor.clone()), - }; - let http_client = http_client::FakeHttpClient::with_404_response(); - let text_system = Arc::new(TextSystem::new( - platform_text_system.unwrap_or_else(|| platform.text_system.clone()), - )); - - let app = App::new_app(platform.clone(), asset_source, http_client); - app.borrow_mut().mode = GpuiMode::test(); - - Self { - app, - platform, - background_executor, - foreground_executor, - dispatcher, - text_system, - } - } - - /// Run a closure with mutable access to the App context. - /// Automatically runs until parked after the closure completes. - pub fn update(&mut self, f: impl FnOnce(&mut App) -> R) -> R { - let result = { - let mut app = self.app.borrow_mut(); - app.update(f) - }; - self.run_until_parked(); - result - } - - /// Run a closure with read-only access to the App context. - pub fn read(&self, f: impl FnOnce(&App) -> R) -> R { - let app = self.app.borrow(); - f(&app) - } - - /// Create a new entity in the app. - pub fn new_entity( - &mut self, - build: impl FnOnce(&mut Context) -> T, - ) -> Entity { - self.update(|cx| cx.new(build)) - } - - /// Update an entity. - pub fn update_entity( - &mut self, - entity: &Entity, - f: impl FnOnce(&mut T, &mut Context) -> R, - ) -> R { - self.update(|cx| entity.update(cx, f)) - } - - /// Read an entity. - pub fn read_entity( - &self, - entity: &Entity, - f: impl FnOnce(&T, &App) -> R, - ) -> R { - self.read(|cx| f(entity.read(cx), cx)) - } - - /// Open a test window with the given root view, using maximized bounds. - pub fn open_window( - &mut self, - build_view: impl FnOnce(&mut Window, &mut Context) -> V, - ) -> TestAppWindow { - let bounds = self.read(|cx| Bounds::maximized(None, cx)); - let handle = self.update(|cx| { - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |window, cx| cx.new(|cx| build_view(window, cx)), - ) - .unwrap() - }); - - TestAppWindow { - handle, - app: self.app.clone(), - platform: self.platform.clone(), - background_executor: self.background_executor.clone(), - } - } - - /// Open a test window with specific options. - pub fn open_window_with_options( - &mut self, - options: WindowOptions, - build_view: impl FnOnce(&mut Window, &mut Context) -> V, - ) -> TestAppWindow { - let handle = self.update(|cx| { - cx.open_window(options, |window, cx| cx.new(|cx| build_view(window, cx))) - .unwrap() - }); - - TestAppWindow { - handle, - app: self.app.clone(), - platform: self.platform.clone(), - background_executor: self.background_executor.clone(), - } - } - - /// Run pending tasks until there's nothing left to do. - pub fn run_until_parked(&self) { - self.background_executor.run_until_parked(); - } - - /// Advance the simulated clock by the given duration. - pub fn advance_clock(&self, duration: Duration) { - self.background_executor.advance_clock(duration); - } - - /// Spawn a future on the foreground executor. - pub fn spawn(&self, f: impl FnOnce(AsyncApp) -> Fut) -> Task - where - Fut: Future + 'static, - R: 'static, - { - self.foreground_executor.spawn(f(self.to_async())) - } - - /// Spawn a future on the background executor. - pub fn background_spawn(&self, future: impl Future + Send + 'static) -> Task - where - R: Send + 'static, - { - self.background_executor.spawn(future) - } - - /// Get an async handle to the app. - pub fn to_async(&self) -> AsyncApp { - AsyncApp { - app: Rc::downgrade(&self.app), - background_executor: self.background_executor.clone(), - foreground_executor: self.foreground_executor.clone(), - } - } - - /// Get the background executor. - pub fn background_executor(&self) -> &BackgroundExecutor { - &self.background_executor - } - - /// Get the foreground executor. - pub fn foreground_executor(&self) -> &ForegroundExecutor { - &self.foreground_executor - } - - /// Get the text system. - pub fn text_system(&self) -> &Arc { - &self.text_system - } - - /// Check if a global of the given type exists. - pub fn has_global(&self) -> bool { - self.read(|cx| cx.has_global::()) - } - - /// Set a global value. - pub fn set_global(&mut self, global: G) { - self.update(|cx| cx.set_global(global)); - } - - /// Read a global value. - pub fn read_global(&self, f: impl FnOnce(&G, &App) -> R) -> R { - self.read(|cx| f(cx.global(), cx)) - } - - /// Update a global value. - pub fn update_global(&mut self, f: impl FnOnce(&mut G, &mut App) -> R) -> R { - self.update(|cx| cx.update_global(f)) - } - - // Platform simulation methods - - /// Write text to the simulated clipboard. - pub fn write_to_clipboard(&self, item: ClipboardItem) { - self.platform.write_to_clipboard(item); - } - - /// Read from the simulated clipboard. - pub fn read_from_clipboard(&self) -> Option { - self.platform.read_from_clipboard() - } - - /// Get URLs that have been opened via `cx.open_url()`. - pub fn opened_url(&self) -> Option { - self.platform.opened_url.borrow().clone() - } - - /// Check if a file path prompt is pending. - pub fn did_prompt_for_new_path(&self) -> bool { - self.platform.did_prompt_for_new_path() - } - - /// Simulate answering a path selection dialog. - pub fn simulate_new_path_selection( - &self, - select: impl FnOnce(&std::path::Path) -> Option, - ) { - self.platform.simulate_new_path_selection(select); - } - - /// Check if a prompt dialog is pending. - pub fn has_pending_prompt(&self) -> bool { - self.platform.has_pending_prompt() - } - - /// Simulate answering a prompt dialog. - pub fn simulate_prompt_answer(&self, button: &str) { - self.platform.simulate_prompt_answer(button); - } - - /// Get all open windows. - pub fn windows(&self) -> Vec { - self.read(|cx| cx.windows()) - } -} - -impl Default for TestApp { - fn default() -> Self { - Self::new() - } -} - -/// A test window with inspection and simulation capabilities. -pub struct TestAppWindow { - handle: WindowHandle, - app: Rc, - platform: Rc, - background_executor: BackgroundExecutor, -} - -impl TestAppWindow { - /// Get the window handle. - pub fn handle(&self) -> WindowHandle { - self.handle - } - - /// Get the root view entity. - pub fn root(&self) -> Entity { - let mut app = self.app.borrow_mut(); - let any_handle: AnyWindowHandle = self.handle.into(); - app.update_window(any_handle, |root_view, _, _| { - root_view.downcast::().expect("root view type mismatch") - }) - .expect("window not found") - } - - /// Update the root view. - pub fn update(&mut self, f: impl FnOnce(&mut V, &mut Window, &mut Context) -> R) -> R { - let result = { - let mut app = self.app.borrow_mut(); - let any_handle: AnyWindowHandle = self.handle.into(); - app.update_window(any_handle, |root_view, window, cx| { - let view = root_view.downcast::().expect("root view type mismatch"); - view.update(cx, |view, cx| f(view, window, cx)) - }) - .expect("window not found") - }; - self.background_executor.run_until_parked(); - result - } - - /// Read the root view. - pub fn read(&self, f: impl FnOnce(&V, &App) -> R) -> R { - let app = self.app.borrow(); - let view = self - .app - .borrow() - .windows - .get(self.handle.window_id()) - .and_then(|w| w.as_ref()) - .and_then(|w| w.root.clone()) - .and_then(|r| r.downcast::().ok()) - .expect("window or root view not found"); - f(view.read(&app), &app) - } - - /// Get the window title. - pub fn title(&self) -> Option { - let app = self.app.borrow(); - app.read_window(&self.handle, |_, _cx| { - // TODO: expose title through Window API - None - }) - .unwrap() - } - - /// Simulate a keystroke. - pub fn simulate_keystroke(&mut self, keystroke: &str) { - let keystroke = Keystroke::parse(keystroke).unwrap(); - { - let mut app = self.app.borrow_mut(); - let any_handle: AnyWindowHandle = self.handle.into(); - app.update_window(any_handle, |_, window, cx| { - window.dispatch_keystroke(keystroke, cx); - }) - .unwrap(); - } - self.background_executor.run_until_parked(); - } - - /// Simulate multiple keystrokes (space-separated). - pub fn simulate_keystrokes(&mut self, keystrokes: &str) { - for keystroke in keystrokes.split(' ') { - self.simulate_keystroke(keystroke); - } - } - - /// Simulate typing text. - pub fn simulate_input(&mut self, input: &str) { - for char in input.chars() { - self.simulate_keystroke(&char.to_string()); - } - } - - /// Simulate a mouse move. - pub fn simulate_mouse_move(&mut self, position: Point) { - self.simulate_event(MouseMoveEvent { - position, - modifiers: Default::default(), - pressed_button: None, - }); - } - - /// Simulate a mouse down event. - pub fn simulate_mouse_down(&mut self, position: Point, button: MouseButton) { - self.simulate_event(MouseDownEvent { - position, - button, - modifiers: Default::default(), - click_count: 1, - first_mouse: false, - }); - } - - /// Simulate a mouse up event. - pub fn simulate_mouse_up(&mut self, position: Point, button: MouseButton) { - self.simulate_event(MouseUpEvent { - position, - button, - modifiers: Default::default(), - click_count: 1, - }); - } - - /// Simulate a click at the given position. - pub fn simulate_click(&mut self, position: Point, button: MouseButton) { - self.simulate_mouse_down(position, button); - self.simulate_mouse_up(position, button); - } - - /// Simulate a scroll event. - pub fn simulate_scroll(&mut self, position: Point, delta: Point) { - self.simulate_event(crate::ScrollWheelEvent { - position, - delta: crate::ScrollDelta::Pixels(delta), - modifiers: Default::default(), - touch_phase: crate::TouchPhase::Moved, - }); - } - - /// Simulate an input event. - pub fn simulate_event(&mut self, event: E) { - let platform_input = event.to_platform_input(); - { - let mut app = self.app.borrow_mut(); - let any_handle: AnyWindowHandle = self.handle.into(); - app.update_window(any_handle, |_, window, cx| { - window.dispatch_event(platform_input, cx); - }) - .unwrap(); - } - self.background_executor.run_until_parked(); - } - - /// Simulate resizing the window. - pub fn simulate_resize(&mut self, size: Size) { - let window_id = self.handle.window_id(); - let mut app = self.app.borrow_mut(); - if let Some(Some(window)) = app.windows.get_mut(window_id) { - if let Some(test_window) = window.platform_window.as_test() { - test_window.simulate_resize(size); - } - } - drop(app); - self.background_executor.run_until_parked(); - } - - /// Force a redraw of the window. - pub fn draw(&mut self) { - let mut app = self.app.borrow_mut(); - let any_handle: AnyWindowHandle = self.handle.into(); - app.update_window(any_handle, |_, window, cx| { - window.draw(cx).clear(cx); - }) - .unwrap(); - } -} - -impl Clone for TestAppWindow { - fn clone(&self) -> Self { - Self { - handle: self.handle, - app: self.app.clone(), - platform: self.platform.clone(), - background_executor: self.background_executor.clone(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{FocusHandle, Focusable, div, prelude::*}; - - struct Counter { - count: usize, - focus_handle: FocusHandle, - } - - impl Counter { - fn new(_window: &mut Window, cx: &mut Context) -> Self { - let focus_handle = cx.focus_handle(); - Self { - count: 0, - focus_handle, - } - } - - fn increment(&mut self, _cx: &mut Context) { - self.count += 1; - } - } - - impl Focusable for Counter { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } - } - - impl Render for Counter { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().child(format!("Count: {}", self.count)) - } - } - - #[test] - fn test_basic_usage() { - let mut app = TestApp::new(); - - let mut window = app.open_window(Counter::new); - - window.update(|counter, _window, cx| { - counter.increment(cx); - }); - - window.read(|counter, _| { - assert_eq!(counter.count, 1); - }); - - drop(window); - app.update(|cx| cx.shutdown()); - } - - #[test] - fn test_entity_creation() { - let mut app = TestApp::new(); - - let entity = app.new_entity(|cx| Counter { - count: 42, - focus_handle: cx.focus_handle(), - }); - - app.read_entity(&entity, |counter, _| { - assert_eq!(counter.count, 42); - }); - - app.update_entity(&entity, |counter, _cx| { - counter.count += 1; - }); - - app.read_entity(&entity, |counter, _| { - assert_eq!(counter.count, 43); - }); - } - - #[test] - fn test_globals() { - let mut app = TestApp::new(); - - struct MyGlobal(String); - impl Global for MyGlobal {} - - assert!(!app.has_global::()); - - app.set_global(MyGlobal("hello".into())); - - assert!(app.has_global::()); - - app.read_global::(|global, _| { - assert_eq!(global.0, "hello"); - }); - - app.update_global::(|global, _| { - global.0 = "world".into(); - }); - - app.read_global::(|global, _| { - assert_eq!(global.0, "world"); - }); - } -} diff --git a/crates/gpui_pre/src/app/test_context.rs b/crates/gpui_pre/src/app/test_context.rs deleted file mode 100644 index 1dceee0..0000000 --- a/crates/gpui_pre/src/app/test_context.rs +++ /dev/null @@ -1,1337 +0,0 @@ -use crate::{ - Action, AnyView, AnyWindowHandle, App, AppCell, AppContext, AsyncApp, AvailableSpace, - BackgroundExecutor, BorrowAppContext, Bounds, Capslock, ClipboardItem, DrawPhase, Drawable, - Element, Empty, EntityId, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, - Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, - Pixels, Platform, Point, Render, Result, SharedString, Size, SystemNotification, - SystemNotificationResponse, Task, TestDispatcher, TestPlatform, TestScreenCaptureSource, - TestWindow, TextSystem, VisualContext, Window, WindowBounds, WindowHandle, WindowOptions, - app::GpuiMode, window::ElementArenaScope, -}; -use anyhow::{anyhow, bail}; -use futures::{Stream, StreamExt, channel::oneshot}; - -use std::{ - cell::RefCell, future::Future, ops::Deref, path::PathBuf, rc::Rc, sync::Arc, time::Duration, -}; - -/// A TestAppContext is provided to tests created with `#[gpui::test]`, it provides -/// an implementation of `Context` with additional methods that are useful in tests. -#[derive(Clone)] -pub struct TestAppContext { - #[doc(hidden)] - pub background_executor: BackgroundExecutor, - #[doc(hidden)] - pub foreground_executor: ForegroundExecutor, - #[doc(hidden)] - pub dispatcher: TestDispatcher, - test_platform: Rc, - text_system: Arc, - fn_name: Option<&'static str>, - on_quit: Rc>>>, - #[doc(hidden)] - pub app: Rc, -} - -impl AppContext for TestAppContext { - fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity { - let mut app = self.app.borrow_mut(); - app.new(build_entity) - } - - fn reserve_entity(&mut self) -> crate::Reservation { - let mut app = self.app.borrow_mut(); - app.reserve_entity() - } - - fn insert_entity( - &mut self, - reservation: crate::Reservation, - build_entity: impl FnOnce(&mut Context) -> T, - ) -> Entity { - let mut app = self.app.borrow_mut(); - app.insert_entity(reservation, build_entity) - } - - fn update_entity( - &mut self, - handle: &Entity, - update: impl FnOnce(&mut T, &mut Context) -> R, - ) -> R { - let mut app = self.app.borrow_mut(); - app.update_entity(handle, update) - } - - fn as_mut<'a, T>(&'a mut self, _: &Entity) -> super::GpuiBorrow<'a, T> - where - T: 'static, - { - panic!("Cannot use as_mut with a test app context. Try calling update() first") - } - - fn read_entity(&self, handle: &Entity, read: impl FnOnce(&T, &App) -> R) -> R - where - T: 'static, - { - let app = self.app.borrow(); - app.read_entity(handle, read) - } - - fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result - where - F: FnOnce(AnyView, &mut Window, &mut App) -> T, - { - let mut lock = self.app.borrow_mut(); - lock.update_window(window, f) - } - - fn with_window( - &mut self, - entity_id: EntityId, - f: impl FnOnce(&mut Window, &mut App) -> R, - ) -> Option { - let mut lock = self.app.borrow_mut(); - lock.with_window(entity_id, f) - } - - fn read_window( - &self, - window: &WindowHandle, - read: impl FnOnce(Entity, &App) -> R, - ) -> Result - where - T: 'static, - { - let app = self.app.borrow(); - app.read_window(window, read) - } - - fn background_spawn(&self, future: impl Future + Send + 'static) -> Task - where - R: Send + 'static, - { - self.background_executor.spawn(future) - } - - fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R - where - G: Global, - { - let app = self.app.borrow(); - app.read_global(callback) - } -} - -impl TestAppContext { - /// Creates a new `TestAppContext`. Usually you can rely on `#[gpui::test]` to do this for you. - pub fn build(dispatcher: TestDispatcher, fn_name: Option<&'static str>) -> Self { - let arc_dispatcher = Arc::new(dispatcher.clone()); - let background_executor = BackgroundExecutor::new(arc_dispatcher.clone()); - let foreground_executor = ForegroundExecutor::new(arc_dispatcher); - let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone()); - let asset_source = Arc::new(()); - let http_client = http_client::FakeHttpClient::with_404_response(); - let text_system = Arc::new(TextSystem::new(platform.text_system())); - - let app = App::new_app(platform.clone(), asset_source, http_client); - app.borrow_mut().mode = GpuiMode::test(); - - Self { - app, - background_executor, - foreground_executor, - dispatcher, - test_platform: platform, - text_system, - fn_name, - on_quit: Rc::new(RefCell::new(Vec::default())), - } - } - - /// Skip all drawing operations for the duration of this test. - pub fn skip_drawing(&mut self) { - self.app.borrow_mut().mode = GpuiMode::Test { skip_drawing: true }; - } - - /// Create a single TestAppContext, for non-multi-client tests - pub fn single() -> Self { - let dispatcher = TestDispatcher::new(0); - Self::build(dispatcher, None) - } - - /// The name of the test function that created this `TestAppContext` - pub fn test_function_name(&self) -> Option<&'static str> { - self.fn_name - } - - /// Checks whether there have been any new path prompts received by the platform. - pub fn did_prompt_for_new_path(&self) -> bool { - self.test_platform.did_prompt_for_new_path() - } - - /// returns a new `TestAppContext` re-using the same executors to interleave tasks. - pub fn new_app(&self) -> TestAppContext { - Self::build(self.dispatcher.clone(), self.fn_name) - } - - /// Called by the test helper to end the test. - /// public so the macro can call it. - pub fn quit(&self) { - self.on_quit.borrow_mut().drain(..).for_each(|f| f()); - self.app.borrow_mut().shutdown(); - } - - /// Register cleanup to run when the test ends. - pub fn on_quit(&mut self, f: impl FnOnce() + 'static) { - self.on_quit.borrow_mut().push(Box::new(f)); - } - - /// Schedules all windows to be redrawn on the next effect cycle. - pub fn refresh(&mut self) -> Result<()> { - let mut app = self.app.borrow_mut(); - app.refresh_windows(); - Ok(()) - } - - /// Returns an executor (for running tasks in the background) - pub fn executor(&self) -> BackgroundExecutor { - self.background_executor.clone() - } - - /// Returns an executor (for running tasks on the main thread) - pub fn foreground_executor(&self) -> &ForegroundExecutor { - &self.foreground_executor - } - - /// Gives you an `&mut App` for the duration of the closure - pub fn update(&self, f: impl FnOnce(&mut App) -> R) -> R { - let mut cx = self.app.borrow_mut(); - cx.update(f) - } - - /// Gives you an `&App` for the duration of the closure - pub fn read(&self, f: impl FnOnce(&App) -> R) -> R { - let cx = self.app.borrow(); - f(&cx) - } - - /// Adds a new window. The Window will always be backed by a `TestWindow` which - /// can be retrieved with `self.test_window(handle)` - pub fn add_window(&mut self, build_window: F) -> WindowHandle - where - F: FnOnce(&mut Window, &mut Context) -> V, - V: 'static + Render, - { - let mut cx = self.app.borrow_mut(); - - // Some tests rely on the window size matching the bounds of the test display - let bounds = Bounds::maximized(None, &cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |window, cx| cx.new(|cx| build_window(window, cx)), - ) - .unwrap() - } - - /// Opens a new window with a specific size. - /// - /// Unlike `add_window` which uses maximized bounds, this allows controlling - /// the window dimensions, which is important for layout-sensitive tests. - pub fn open_window( - &mut self, - window_size: Size, - build_window: F, - ) -> WindowHandle - where - F: FnOnce(&mut Window, &mut Context) -> V, - V: 'static + Render, - { - let mut cx = self.app.borrow_mut(); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(Bounds { - origin: Point::default(), - size: window_size, - })), - ..Default::default() - }, - |window, cx| cx.new(|cx| build_window(window, cx)), - ) - .unwrap() - } - - /// Adds a new window with no content. - pub fn add_empty_window(&mut self) -> &mut VisualTestContext { - let mut cx = self.app.borrow_mut(); - let bounds = Bounds::maximized(None, &cx); - let window = cx - .open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| Empty), - ) - .unwrap(); - drop(cx); - let cx = VisualTestContext::from_window(*window.deref(), self).into_mut(); - cx.run_until_parked(); - cx - } - - /// Adds a new window, and returns its root view and a `VisualTestContext` which can be used - /// as a `Window` and `App` for the rest of the test. Typically you would shadow this context with - /// the returned one. `let (view, cx) = cx.add_window_view(...);` - pub fn add_window_view( - &mut self, - build_root_view: F, - ) -> (Entity, &mut VisualTestContext) - where - F: FnOnce(&mut Window, &mut Context) -> V, - V: 'static + Render, - { - let mut cx = self.app.borrow_mut(); - let bounds = Bounds::maximized(None, &cx); - let window = cx - .open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |window, cx| cx.new(|cx| build_root_view(window, cx)), - ) - .unwrap(); - drop(cx); - let view = window.root(self).unwrap(); - let cx = VisualTestContext::from_window(*window.deref(), self).into_mut(); - cx.run_until_parked(); - - // it might be nice to try and cleanup these at the end of each test. - (view, cx) - } - - /// returns the TextSystem - pub fn text_system(&self) -> &Arc { - &self.text_system - } - - /// Simulates writing to the platform clipboard - pub fn write_to_clipboard(&self, item: ClipboardItem) { - self.test_platform.write_to_clipboard(item) - } - - /// Simulates reading from the platform clipboard. - /// This will return the most recent value from `write_to_clipboard`. - pub fn read_from_clipboard(&self) -> Option { - self.test_platform.read_from_clipboard() - } - - /// Simulates choosing a File in the platform's "Open" dialog. - pub fn simulate_new_path_selection( - &self, - select_path: impl FnOnce(&std::path::Path) -> Option, - ) { - self.test_platform.simulate_new_path_selection(select_path); - } - - /// Simulates responding to a `prompt_for_paths` ("Open") dialog. - pub fn simulate_path_prompt_response( - &self, - select_paths: impl FnOnce(&crate::PathPromptOptions) -> Option>, - ) { - self.test_platform - .simulate_path_prompt_response(select_paths); - } - - /// Returns true if there's a path selection dialog pending. - pub fn did_prompt_for_paths(&self) -> bool { - self.test_platform.did_prompt_for_paths() - } - - /// Simulates clicking a button in an platform-level alert dialog. - #[track_caller] - pub fn simulate_prompt_answer(&self, button: &str) { - self.test_platform.simulate_prompt_answer(button); - } - - /// Returns true if there's an alert dialog open. - pub fn has_pending_prompt(&self) -> bool { - self.test_platform.has_pending_prompt() - } - - /// Returns true if there's an alert dialog open. - pub fn pending_prompt(&self) -> Option<(String, String)> { - self.test_platform.pending_prompt() - } - - /// All the urls that have been opened with cx.open_url() during this test. - pub fn opened_url(&self) -> Option { - self.test_platform.opened_url.borrow().clone() - } - - /// Returns the application identity configured during this test. - pub fn app_identity(&self) -> Option<(SharedString, SharedString)> { - self.test_platform.app_identity() - } - - /// Returns all system notifications shown during this test, in order. - pub fn shown_system_notifications(&self) -> Vec { - self.test_platform.shown_system_notifications() - } - - /// Returns the system notifications currently delivered by the test platform. - pub fn delivered_system_notifications(&self) -> Vec { - self.test_platform.delivered_system_notifications() - } - - /// Returns the tags of all system notifications dismissed during this test, in order. - pub fn dismissed_system_notifications(&self) -> Vec { - self.test_platform.dismissed_system_notifications() - } - - /// Simulates the user activating a system notification. - pub fn simulate_system_notification_response(&self, response: SystemNotificationResponse) { - self.test_platform - .simulate_system_notification_response(response); - } - - /// Simulates the user resizing the window to the new size. - pub fn simulate_window_resize(&self, window_handle: AnyWindowHandle, size: Size) { - self.test_window(window_handle).simulate_resize(size); - } - - /// Returns true if there's an alert dialog open. - pub fn expect_restart(&self) -> oneshot::Receiver<(Option, Vec)> { - let (tx, rx) = futures::channel::oneshot::channel(); - self.test_platform.expect_restart.borrow_mut().replace(tx); - rx - } - - /// Causes the given sources to be returned if the application queries for screen - /// capture sources. - pub fn set_screen_capture_sources(&self, sources: Vec) { - self.test_platform.set_screen_capture_sources(sources); - } - - /// Returns all windows open in the test. - pub fn windows(&self) -> Vec { - self.app.borrow().windows() - } - - /// Run the given task on the main thread. - #[track_caller] - pub fn spawn(&self, f: impl FnOnce(AsyncApp) -> Fut) -> Task - where - Fut: Future + 'static, - R: 'static, - { - self.foreground_executor.spawn(f(self.to_async())) - } - - /// true if the given global is defined - pub fn has_global(&self) -> bool { - let app = self.app.borrow(); - app.has_global::() - } - - /// runs the given closure with a reference to the global - /// panics if `has_global` would return false. - pub fn read_global(&self, read: impl FnOnce(&G, &App) -> R) -> R { - let app = self.app.borrow(); - read(app.global(), &app) - } - - /// runs the given closure with a reference to the global (if set) - pub fn try_read_global(&self, read: impl FnOnce(&G, &App) -> R) -> Option { - let lock = self.app.borrow(); - Some(read(lock.try_global()?, &lock)) - } - - /// sets the global in this context. - pub fn set_global(&mut self, global: G) { - let mut lock = self.app.borrow_mut(); - lock.update(|cx| cx.set_global(global)) - } - - /// updates the global in this context. (panics if `has_global` would return false) - pub fn update_global(&mut self, update: impl FnOnce(&mut G, &mut App) -> R) -> R { - let mut lock = self.app.borrow_mut(); - lock.update(|cx| cx.update_global(update)) - } - - /// Returns an `AsyncApp` which can be used to run tasks that expect to be on a background - /// thread on the current thread in tests. - pub fn to_async(&self) -> AsyncApp { - AsyncApp { - app: Rc::downgrade(&self.app), - background_executor: self.background_executor.clone(), - foreground_executor: self.foreground_executor.clone(), - } - } - - /// Wait until there are no more pending tasks. - pub fn run_until_parked(&self) { - self.dispatcher.run_until_parked(); - } - - /// Simulate dispatching an action to the currently focused node in the window. - pub fn dispatch_action(&mut self, window: AnyWindowHandle, action: A) - where - A: Action, - { - window - .update(self, |_, window, cx| { - window.dispatch_action(action.boxed_clone(), cx) - }) - .unwrap(); - - self.background_executor.run_until_parked() - } - - /// simulate_keystrokes takes a space-separated list of keys to type. - /// cx.simulate_keystrokes("cmd-shift-p b k s p enter") - /// in Zed, this will run backspace on the current editor through the command palette. - /// This will also run the background executor until it's parked. - pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) { - for keystroke in keystrokes - .split(' ') - .map(Keystroke::parse) - .map(Result::unwrap) - { - self.dispatch_keystroke(window, keystroke); - } - - self.background_executor.run_until_parked() - } - - /// simulate_input takes a string of text to type. - /// cx.simulate_input("abc") - /// will type abc into your current editor - /// This will also run the background executor until it's parked. - pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) { - for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) { - self.dispatch_keystroke(window, keystroke); - } - - self.background_executor.run_until_parked() - } - - /// dispatches a single Keystroke (see also `simulate_keystrokes` and `simulate_input`) - pub fn dispatch_keystroke(&mut self, window: AnyWindowHandle, keystroke: Keystroke) { - self.update_window(window, |_, window, cx| { - window.dispatch_keystroke(keystroke, cx) - }) - .unwrap(); - } - - /// Returns the `TestWindow` backing the given handle. - pub(crate) fn test_window(&self, window: AnyWindowHandle) -> TestWindow { - self.app - .borrow_mut() - .windows - .get_mut(window.id) - .unwrap() - .as_deref_mut() - .unwrap() - .platform_window - .as_test() - .unwrap() - .clone() - } - - /// Returns a stream of notifications whenever the Entity is updated. - pub fn notifications( - &mut self, - entity: &Entity, - ) -> impl Stream + use { - let (tx, rx) = futures::channel::mpsc::unbounded(); - self.update(|cx| { - cx.observe(entity, { - let tx = tx.clone(); - move |_, _| { - let _ = tx.unbounded_send(()); - } - }) - .detach(); - cx.observe_release(entity, move |_, _| tx.close_channel()) - .detach() - }); - rx - } - - /// Returns a stream of events emitted by the given Entity. - pub fn events>( - &mut self, - entity: &Entity, - ) -> futures::channel::mpsc::UnboundedReceiver - where - Evt: 'static + Clone, - { - let (tx, rx) = futures::channel::mpsc::unbounded(); - entity - .update(self, |_, cx: &mut Context| { - cx.subscribe(entity, move |_entity, _handle, event, _cx| { - let _ = tx.unbounded_send(event.clone()); - }) - }) - .detach(); - rx - } - - /// Runs until the given condition becomes true. (Prefer `run_until_parked` if you - /// don't need to jump in at a specific time). - pub async fn condition( - &mut self, - entity: &Entity, - mut predicate: impl FnMut(&mut T, &mut Context) -> bool, - ) { - let timer = self.executor().timer(Duration::from_secs(3)); - let mut notifications = self.notifications(entity); - - use futures::FutureExt as _; - use futures_concurrency::future::Race as _; - - ( - async { - loop { - if entity.update(self, &mut predicate) { - return Ok(()); - } - - if notifications.next().await.is_none() { - bail!("entity dropped") - } - } - }, - timer.map(|_| Err(anyhow!("condition timed out"))), - ) - .race() - .await - .unwrap(); - } - - /// Set a name for this App. - #[cfg(any(test, feature = "test-support"))] - pub fn set_name(&mut self, name: &'static str) { - self.update(|cx| cx.name = Some(name)) - } -} - -impl Entity { - /// Block until the next event is emitted by the entity, then return it. - pub fn next_event(&self, cx: &mut TestAppContext) -> impl Future - where - Event: Send + Clone + 'static, - T: EventEmitter, - { - let (tx, mut rx) = oneshot::channel(); - let mut tx = Some(tx); - let subscription = self.update(cx, |_, cx| { - cx.subscribe(self, move |_, _, event, _| { - if let Some(tx) = tx.take() { - _ = tx.send(event.clone()); - } - }) - }); - - async move { - let event = rx.await.expect("no event emitted"); - drop(subscription); - event - } - } -} - -impl Entity { - /// Returns a future that resolves when the view is next updated. - pub fn next_notification( - &self, - advance_clock_by: Duration, - cx: &TestAppContext, - ) -> impl Future { - use postage::prelude::{Sink as _, Stream as _}; - - let (mut tx, mut rx) = postage::mpsc::channel(1); - let subscription = cx.app.borrow_mut().observe(self, move |_, _| { - tx.try_send(()).ok(); - }); - - cx.executor().advance_clock(advance_clock_by); - - async move { - rx.recv() - .await - .expect("entity dropped while test was waiting for its next notification"); - drop(subscription); - } - } -} - -impl Entity { - /// Returns a future that resolves when the condition becomes true. - pub fn condition( - &self, - cx: &TestAppContext, - mut predicate: impl FnMut(&V, &App) -> bool, - ) -> impl Future - where - Evt: 'static, - V: EventEmitter, - { - use postage::prelude::{Sink as _, Stream as _}; - - let (tx, mut rx) = postage::mpsc::channel(1024); - - let mut cx = cx.app.borrow_mut(); - let subscriptions = ( - cx.observe(self, { - let mut tx = tx.clone(); - move |_, _| { - tx.blocking_send(()).ok(); - } - }), - cx.subscribe(self, { - let mut tx = tx; - move |_, _: &Evt, _| { - tx.blocking_send(()).ok(); - } - }), - ); - - let cx = cx.this.upgrade().unwrap(); - let handle = self.downgrade(); - - async move { - loop { - { - let cx = cx.borrow(); - let cx = &*cx; - if predicate( - handle - .upgrade() - .expect("view dropped with pending condition") - .read(cx), - cx, - ) { - break; - } - } - - rx.recv() - .await - .expect("view dropped with pending condition"); - } - drop(subscriptions); - } - } -} - -use derive_more::{Deref, DerefMut}; - -use super::{Context, Entity}; -#[derive(Deref, DerefMut, Clone)] -/// A VisualTestContext is the test-equivalent of a `Window` and `App`. It allows you to -/// run window-specific test code. It can be dereferenced to a `TextAppContext`. -pub struct VisualTestContext { - #[deref] - #[deref_mut] - /// cx is the original TestAppContext (you can more easily access this using Deref) - pub cx: TestAppContext, - window: AnyWindowHandle, -} - -impl VisualTestContext { - /// Provides a `Window` and `App` for the duration of the closure. - pub fn update(&mut self, f: impl FnOnce(&mut Window, &mut App) -> R) -> R { - self.cx - .update_window(self.window, |_, window, cx| f(window, cx)) - .unwrap() - } - - /// Creates a new VisualTestContext. You would typically shadow the passed in - /// TestAppContext with this, as this is typically more useful. - /// `let cx = VisualTestContext::from_window(window, cx);` - pub fn from_window(window: AnyWindowHandle, cx: &TestAppContext) -> Self { - Self { - cx: cx.clone(), - window, - } - } - - /// Wait until there are no more pending tasks. - pub fn run_until_parked(&self) { - self.cx.background_executor.run_until_parked(); - } - - /// Dispatch the action to the currently focused node. - pub fn dispatch_action(&mut self, action: A) - where - A: Action, - { - self.cx.dispatch_action(self.window, action) - } - - /// Read the title off the window (set by `Window#set_window_title`) - pub fn window_title(&mut self) -> Option { - self.cx.test_window(self.window).0.lock().title.clone() - } - - /// Read the document path off the window (set by `Window#set_document_path`) - pub fn document_path(&mut self) -> Option { - self.cx - .test_window(self.window) - .0 - .lock() - .document_path - .clone() - } - - /// Simulate a sequence of keystrokes `cx.simulate_keystrokes("cmd-p escape")` - /// Automatically runs until parked. - pub fn simulate_keystrokes(&mut self, keystrokes: &str) { - self.cx.simulate_keystrokes(self.window, keystrokes) - } - - /// Simulate typing text `cx.simulate_input("hello")` - /// Automatically runs until parked. - pub fn simulate_input(&mut self, input: &str) { - self.cx.simulate_input(self.window, input) - } - - /// Simulate a mouse move event to the given point - pub fn simulate_mouse_move( - &mut self, - position: Point, - button: impl Into>, - modifiers: Modifiers, - ) { - self.simulate_event(MouseMoveEvent { - position, - modifiers, - pressed_button: button.into(), - }) - } - - /// Simulate a mouse down event to the given point - pub fn simulate_mouse_down( - &mut self, - position: Point, - button: MouseButton, - modifiers: Modifiers, - ) { - self.simulate_event(MouseDownEvent { - position, - modifiers, - button, - click_count: 1, - first_mouse: false, - }) - } - - /// Simulate a mouse up event to the given point - pub fn simulate_mouse_up( - &mut self, - position: Point, - button: MouseButton, - modifiers: Modifiers, - ) { - self.simulate_event(MouseUpEvent { - position, - modifiers, - button, - click_count: 1, - }) - } - - /// Simulate a primary mouse click at the given point - pub fn simulate_click(&mut self, position: Point, modifiers: Modifiers) { - self.simulate_event(MouseDownEvent { - position, - modifiers, - button: MouseButton::Left, - click_count: 1, - first_mouse: false, - }); - self.simulate_event(MouseUpEvent { - position, - modifiers, - button: MouseButton::Left, - click_count: 1, - }); - } - - /// Simulate a modifiers changed event - pub fn simulate_modifiers_change(&mut self, modifiers: Modifiers) { - self.simulate_event(ModifiersChangedEvent { - modifiers, - capslock: Capslock { on: false }, - }) - } - - /// Simulate a capslock changed event - pub fn simulate_capslock_change(&mut self, on: bool) { - self.simulate_event(ModifiersChangedEvent { - modifiers: Modifiers::none(), - capslock: Capslock { on }, - }) - } - - /// Simulates the user resizing the window to the new size. - pub fn simulate_resize(&self, size: Size) { - self.simulate_window_resize(self.window, size) - } - - /// debug_bounds returns the bounds of the element with the given selector. - pub fn debug_bounds(&mut self, selector: &'static str) -> Option> { - self.update(|window, _| window.rendered_frame.debug_bounds.get(selector).copied()) - } - - /// Draw an element to the window. Useful for simulating events or actions - pub fn draw( - &mut self, - origin: Point, - space: impl Into>, - f: impl FnOnce(&mut Window, &mut App) -> E, - ) -> (E::RequestLayoutState, E::PrepaintState) - where - E: Element, - { - self.update(|window, cx| { - let arena_scope = ElementArenaScope::enter(&cx.element_arena); - - window.invalidator.set_phase(DrawPhase::Prepaint); - let mut element = Drawable::new(f(window, cx)); - element.layout_as_root(space.into(), window, cx); - window.with_absolute_element_offset(origin, |window| element.prepaint(window, cx)); - - window.invalidator.set_phase(DrawPhase::Paint); - let (request_layout_state, prepaint_state) = element.paint(window, cx); - - window.invalidator.set_phase(DrawPhase::None); - window.refresh(); - - drop(element); - arena_scope.exit(&cx.element_arena).clear(cx); - - (request_layout_state, prepaint_state) - }) - } - - /// Simulate an event from the platform, e.g. a ScrollWheelEvent - /// Make sure you've called [VisualTestContext::draw] first! - pub fn simulate_event(&mut self, event: E) { - self.test_window(self.window) - .simulate_input(event.to_platform_input()); - self.background_executor.run_until_parked(); - } - - /// Simulates the user blurring the window. - pub fn deactivate_window(&mut self) { - if Some(self.window) == self.test_platform.active_window() { - self.test_platform.set_active_window(None) - } - self.background_executor.run_until_parked(); - } - - /// Simulates the user closing the window. - /// Returns true if the window was closed. - pub fn simulate_close(&mut self) -> bool { - let handler = self - .cx - .update_window(self.window, |_, window, _| { - window - .platform_window - .as_test() - .unwrap() - .0 - .lock() - .should_close_handler - .take() - }) - .unwrap(); - if let Some(mut handler) = handler { - let should_close = handler(); - self.cx - .update_window(self.window, |_, window, _| { - window.platform_window.on_should_close(handler); - }) - .unwrap(); - should_close - } else { - false - } - } - - /// Get an &mut VisualTestContext (which is mostly what you need to pass to other methods). - /// This method internally retains the VisualTestContext until the end of the test. - pub fn into_mut(self) -> &'static mut Self { - let ptr = Box::into_raw(Box::new(self)); - // safety: on_quit will be called after the test has finished. - // the executor will ensure that all tasks related to the test have stopped. - // so there is no way for cx to be accessed after on_quit is called. - // todo: This is unsound under stacked borrows (also tree borrows probably?) - // the mutable reference invalidates `ptr` which is later used in the closure - let cx = unsafe { &mut *ptr }; - cx.on_quit(move || unsafe { - drop(Box::from_raw(ptr)); - }); - cx - } -} - -impl AppContext for VisualTestContext { - fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity { - self.window - .update(&mut self.cx, |_, _, cx| cx.new(build_entity)) - .expect("window was unexpectedly closed") - } - - fn reserve_entity(&mut self) -> crate::Reservation { - self.cx.reserve_entity() - } - - fn insert_entity( - &mut self, - reservation: crate::Reservation, - build_entity: impl FnOnce(&mut Context) -> T, - ) -> Entity { - self.window - .update(&mut self.cx, |_, _, cx| { - cx.insert_entity(reservation, build_entity) - }) - .expect("window was unexpectedly closed") - } - - fn update_entity( - &mut self, - handle: &Entity, - update: impl FnOnce(&mut T, &mut Context) -> R, - ) -> R - where - T: 'static, - { - self.cx.update_entity(handle, update) - } - - fn as_mut<'a, T>(&'a mut self, handle: &Entity) -> super::GpuiBorrow<'a, T> - where - T: 'static, - { - self.cx.as_mut(handle) - } - - fn read_entity(&self, handle: &Entity, read: impl FnOnce(&T, &App) -> R) -> R - where - T: 'static, - { - self.cx.read_entity(handle, read) - } - - fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result - where - F: FnOnce(AnyView, &mut Window, &mut App) -> T, - { - self.cx.update_window(window, f) - } - - fn with_window( - &mut self, - entity_id: EntityId, - f: impl FnOnce(&mut Window, &mut App) -> R, - ) -> Option { - self.cx.with_window(entity_id, f) - } - - fn read_window( - &self, - window: &WindowHandle, - read: impl FnOnce(Entity, &App) -> R, - ) -> Result - where - T: 'static, - { - self.cx.read_window(window, read) - } - - fn background_spawn(&self, future: impl Future + Send + 'static) -> Task - where - R: Send + 'static, - { - self.cx.background_spawn(future) - } - - fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R - where - G: Global, - { - self.cx.read_global(callback) - } -} - -impl VisualContext for VisualTestContext { - type Result = T; - - /// Get the underlying window handle underlying this context. - fn window_handle(&self) -> AnyWindowHandle { - self.window - } - - fn new_window_entity( - &mut self, - build_entity: impl FnOnce(&mut Window, &mut Context) -> T, - ) -> Entity { - self.window - .update(&mut self.cx, |_, window, cx| { - cx.new(|cx| build_entity(window, cx)) - }) - .expect("window was unexpectedly closed") - } - - fn update_window_entity( - &mut self, - view: &Entity, - update: impl FnOnce(&mut V, &mut Window, &mut Context) -> R, - ) -> R { - let view = view.clone(); - self.cx - .app - .borrow_mut() - .with_window(view.entity_id(), |window, app| { - view.update(app, |v, cx| update(v, window, cx)) - }) - .expect("entity has no current window; use `update` instead of `update_in`") - } - - fn replace_root_view( - &mut self, - build_view: impl FnOnce(&mut Window, &mut Context) -> V, - ) -> Entity - where - V: 'static + Render, - { - self.window - .update(&mut self.cx, |_, window, cx| { - window.replace_root(cx, build_view) - }) - .expect("window was unexpectedly closed") - } - - fn focus(&mut self, view: &Entity) { - self.window - .update(&mut self.cx, |_, window, cx| { - view.read(cx).focus_handle(cx).focus(window, cx) - }) - .expect("window was unexpectedly closed") - } -} - -impl AnyWindowHandle { - /// Creates the given view in this window. - pub fn build_entity( - &self, - cx: &mut TestAppContext, - build_view: impl FnOnce(&mut Window, &mut Context) -> V, - ) -> Entity { - self.update(cx, |_, window, cx| cx.new(|cx| build_view(window, cx))) - .unwrap() - } -} - -#[cfg(test)] -mod tests { - use crate::{ - PathPromptOptions, SystemNotification, SystemNotificationAction, - SystemNotificationResponse, TestAppContext, - }; - use std::cell::RefCell; - use std::path::PathBuf; - use std::rc::Rc; - - #[gpui::test] - async fn test_system_notifications_require_identity_and_replace_matching_tags( - cx: &mut TestAppContext, - ) { - cx.update(|cx| { - cx.show_system_notification(SystemNotification { - tag: "thread-1".into(), - title: "Task started".into(), - body: "Running tests".into(), - actions: Vec::new(), - }); - }); - assert!(cx.shown_system_notifications().is_empty()); - assert!(cx.delivered_system_notifications().is_empty()); - - cx.update(|cx| { - cx.set_app_identity("com.example.tasks", "Tasks"); - cx.show_system_notification(SystemNotification { - tag: "thread-1".into(), - title: "Task started".into(), - body: "Running tests".into(), - actions: Vec::new(), - }); - cx.show_system_notification(SystemNotification { - tag: "thread-1".into(), - title: "Task finished".into(), - body: "All tests passed".into(), - actions: vec![SystemNotificationAction { - id: "open".into(), - label: "Open".into(), - }], - }); - }); - - assert_eq!( - cx.app_identity(), - Some(("com.example.tasks".into(), "Tasks".into())) - ); - assert_eq!(cx.shown_system_notifications().len(), 2); - assert_eq!( - cx.delivered_system_notifications(), - [SystemNotification { - tag: "thread-1".into(), - title: "Task finished".into(), - body: "All tests passed".into(), - actions: vec![SystemNotificationAction { - id: "open".into(), - label: "Open".into(), - }], - }] - ); - - cx.update(|cx| cx.dismiss_system_notification("thread-1")); - assert!(cx.delivered_system_notifications().is_empty()); - assert_eq!(cx.dismissed_system_notifications(), ["thread-1"]); - } - - #[gpui::test] - async fn test_system_notification_body_and_action_responses(cx: &mut TestAppContext) { - let responses = Rc::new(RefCell::new(Vec::new())); - cx.update(|cx| { - cx.on_system_notification_response({ - let responses = responses.clone(); - move |response, _cx| responses.borrow_mut().push(response) - }); - }); - - cx.simulate_system_notification_response(SystemNotificationResponse { - tag: "thread-1".into(), - action_id: None, - }); - cx.simulate_system_notification_response(SystemNotificationResponse { - tag: "thread-1".into(), - action_id: Some("default".into()), - }); - - assert_eq!( - responses.borrow().as_slice(), - &[ - SystemNotificationResponse { - tag: "thread-1".into(), - action_id: None, - }, - SystemNotificationResponse { - tag: "thread-1".into(), - action_id: Some("default".into()), - }, - ] - ); - } - - #[gpui::test] - async fn test_system_notification_response_handler_can_be_replaced(cx: &mut TestAppContext) { - let first_responses = Rc::new(RefCell::new(Vec::new())); - let second_responses = Rc::new(RefCell::new(Vec::new())); - cx.update(|cx| { - cx.on_system_notification_response({ - let first_responses = first_responses.clone(); - move |response, _cx| first_responses.borrow_mut().push(response) - }); - cx.on_system_notification_response({ - let second_responses = second_responses.clone(); - move |response, _cx| second_responses.borrow_mut().push(response) - }); - }); - - let response = SystemNotificationResponse { - tag: "thread-1".into(), - action_id: None, - }; - cx.simulate_system_notification_response(response.clone()); - - assert!(first_responses.borrow().is_empty()); - assert_eq!(second_responses.borrow().as_slice(), &[response]); - } - - #[gpui::test] - async fn test_system_notification_response_handler_can_reenter_app(cx: &mut TestAppContext) { - cx.update(|cx| { - cx.set_app_identity("com.example.tasks", "Tasks"); - cx.show_system_notification(SystemNotification { - tag: "thread-1".into(), - title: "Task finished".into(), - body: "All tests passed".into(), - actions: Vec::new(), - }); - cx.on_system_notification_response(|response, cx| { - cx.dismiss_system_notification(&response.tag); - }); - }); - - cx.simulate_system_notification_response(SystemNotificationResponse { - tag: "thread-1".into(), - action_id: None, - }); - - assert!(cx.delivered_system_notifications().is_empty()); - assert_eq!(cx.dismissed_system_notifications(), ["thread-1"]); - } - - #[gpui::test] - async fn test_simulate_path_prompt_response(cx: &mut TestAppContext) { - assert!(!cx.did_prompt_for_paths()); - - let receiver = cx.update(|cx| { - cx.prompt_for_paths(PathPromptOptions { - files: false, - directories: true, - multiple: true, - prompt: None, - }) - }); - assert!(cx.did_prompt_for_paths()); - - let selected = vec![PathBuf::from("/a"), PathBuf::from("/b")]; - cx.simulate_path_prompt_response({ - let selected = selected.clone(); - move |options| { - assert!(options.multiple); - Some(selected) - } - }); - assert!(!cx.did_prompt_for_paths()); - - let response = receiver.await.unwrap().unwrap(); - assert_eq!(response, Some(selected)); - } - - #[gpui::test] - async fn test_simulate_path_prompt_cancellation(cx: &mut TestAppContext) { - let receiver = cx.update(|cx| { - cx.prompt_for_paths(PathPromptOptions { - files: true, - directories: false, - multiple: false, - prompt: None, - }) - }); - - cx.simulate_path_prompt_response(|_options| None); - - let response = receiver.await.unwrap().unwrap(); - assert_eq!(response, None); - } -} diff --git a/crates/gpui_pre/src/app/visual_test_context.rs b/crates/gpui_pre/src/app/visual_test_context.rs deleted file mode 100644 index b54802a..0000000 --- a/crates/gpui_pre/src/app/visual_test_context.rs +++ /dev/null @@ -1,484 +0,0 @@ -use crate::{ - Action, AnyView, AnyWindowHandle, App, AppCell, AppContext, AssetSource, BackgroundExecutor, - Bounds, ClipboardItem, Context, Entity, EntityId, ForegroundExecutor, Global, InputEvent, - Keystroke, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, - Platform, Point, Render, Result, Size, Task, TestDispatcher, TextSystem, VisualTestPlatform, - Window, WindowBounds, WindowHandle, WindowOptions, app::GpuiMode, -}; -use anyhow::anyhow; -use image::RgbaImage; -use std::{future::Future, rc::Rc, sync::Arc, time::Duration}; - -/// A test context that uses real macOS rendering instead of mocked rendering. -/// This is used for visual tests that need to capture actual screenshots. -/// -/// Unlike `TestAppContext` which uses `TestPlatform` with mocked rendering, -/// `VisualTestAppContext` uses the real `MacPlatform` to produce actual rendered output. -/// -/// Windows created through this context are positioned off-screen (at coordinates like -10000, -10000) -/// so they are invisible to the user but still fully rendered by the compositor. -#[derive(Clone)] -pub struct VisualTestAppContext { - /// The underlying app cell - pub app: Rc, - /// The background executor for running async tasks - pub background_executor: BackgroundExecutor, - /// The foreground executor for running tasks on the main thread - pub foreground_executor: ForegroundExecutor, - /// The test dispatcher for deterministic task scheduling - dispatcher: TestDispatcher, - platform: Rc, - text_system: Arc, -} - -impl VisualTestAppContext { - /// Creates a new `VisualTestAppContext` with real macOS platform rendering - /// but deterministic task scheduling via TestDispatcher. - /// - /// This provides: - /// - Real Metal/compositor rendering for accurate screenshots - /// - Deterministic task scheduling via TestDispatcher - /// - Controllable time via `advance_clock` - /// - /// Note: This uses a no-op asset source, so SVG icons won't render. - /// Use `with_asset_source` to provide real assets for icon rendering. - pub fn new(platform: Rc) -> Self { - Self::with_asset_source(platform, Arc::new(())) - } - - /// Creates a new `VisualTestAppContext` with a custom asset source. - /// - /// Use this when you need SVG icons to render properly in visual tests. - /// Pass the real `Assets` struct to enable icon rendering. - pub fn with_asset_source( - platform: Rc, - asset_source: Arc, - ) -> Self { - // Use a seeded RNG for deterministic behavior - let seed = std::env::var("SEED") - .ok() - .and_then(|s| s.parse().ok()) - .unwrap_or(0); - - // Create a visual test platform that combines real Mac rendering - // with controllable TestDispatcher for deterministic task scheduling - let platform = Rc::new(VisualTestPlatform::new(platform, seed)); - - // Get the dispatcher and executors from the platform - let dispatcher = platform.dispatcher().clone(); - let background_executor = platform.background_executor(); - let foreground_executor = platform.foreground_executor(); - - let text_system = Arc::new(TextSystem::new(platform.text_system())); - - let http_client = http_client::FakeHttpClient::with_404_response(); - - let mut app = App::new_app(platform.clone(), asset_source, http_client); - app.borrow_mut().mode = GpuiMode::test(); - - Self { - app, - background_executor, - foreground_executor, - dispatcher, - platform, - text_system, - } - } - - /// Opens a window positioned off-screen for invisible rendering. - /// - /// The window is positioned at (-10000, -10000) so it's not visible on any display, - /// but it's still fully rendered by the compositor and can be captured via ScreenCaptureKit. - /// - /// # Arguments - /// * `size` - The size of the window to create - /// * `build_root` - A closure that builds the root view for the window - pub fn open_offscreen_window( - &mut self, - size: Size, - build_root: impl FnOnce(&mut Window, &mut App) -> Entity, - ) -> Result> { - use crate::{point, px}; - - let bounds = Bounds { - origin: point(px(-10000.0), px(-10000.0)), - size, - }; - - let mut cx = self.app.borrow_mut(); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - focus: false, - show: true, - ..Default::default() - }, - build_root, - ) - } - - /// Opens an off-screen window with default size (1280x800). - pub fn open_offscreen_window_default( - &mut self, - build_root: impl FnOnce(&mut Window, &mut App) -> Entity, - ) -> Result> { - use crate::{px, size}; - self.open_offscreen_window(size(px(1280.0), px(800.0)), build_root) - } - - /// Returns whether screen capture is supported on this platform. - pub fn is_screen_capture_supported(&self) -> bool { - self.platform.is_screen_capture_supported() - } - - /// Returns the text system used by this context. - pub fn text_system(&self) -> &Arc { - &self.text_system - } - - /// Returns the background executor. - pub fn executor(&self) -> BackgroundExecutor { - self.background_executor.clone() - } - - /// Returns the foreground executor. - pub fn foreground_executor(&self) -> ForegroundExecutor { - self.foreground_executor.clone() - } - - /// Runs all pending foreground and background tasks until there's nothing left to do. - /// This is essential for processing async operations like tooltip timers. - pub fn run_until_parked(&self) { - self.dispatcher.run_until_parked(); - } - - /// Advances the simulated clock by the given duration and processes any tasks - /// that become ready. This is essential for testing time-based behaviors like - /// tooltip delays. - pub fn advance_clock(&self, duration: Duration) { - self.dispatcher.advance_clock(duration); - } - - /// Updates the app state. - pub fn update(&mut self, f: impl FnOnce(&mut App) -> R) -> R { - let mut app = self.app.borrow_mut(); - f(&mut app) - } - - /// Reads from the app state. - pub fn read(&self, f: impl FnOnce(&App) -> R) -> R { - let app = self.app.borrow(); - f(&app) - } - - /// Updates a window. - pub fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result - where - F: FnOnce(AnyView, &mut Window, &mut App) -> T, - { - let mut lock = self.app.borrow_mut(); - lock.update_window(window, f) - } - - /// Spawns a task on the foreground executor. - pub fn spawn(&self, f: F) -> Task - where - F: Future + 'static, - R: 'static, - { - self.foreground_executor.spawn(f) - } - - /// Checks if a global of type G exists. - pub fn has_global(&self) -> bool { - let app = self.app.borrow(); - app.has_global::() - } - - /// Reads a global value. - pub fn read_global(&self, f: impl FnOnce(&G, &App) -> R) -> R { - let app = self.app.borrow(); - f(app.global::(), &app) - } - - /// Sets a global value. - pub fn set_global(&mut self, global: G) { - let mut app = self.app.borrow_mut(); - app.set_global(global); - } - - /// Updates a global value. - pub fn update_global(&mut self, f: impl FnOnce(&mut G, &mut App) -> R) -> R { - let mut lock = self.app.borrow_mut(); - lock.update(|cx| { - let mut global = cx.lease_global::(); - let result = f(&mut global, cx); - cx.end_global_lease(global); - result - }) - } - - /// Simulates a sequence of keystrokes on the given window. - /// - /// Keystrokes are specified as a space-separated string, e.g., "cmd-p escape". - pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) { - for keystroke_text in keystrokes.split_whitespace() { - let keystroke = Keystroke::parse(keystroke_text) - .unwrap_or_else(|_| panic!("Invalid keystroke: {}", keystroke_text)); - self.dispatch_keystroke(window, keystroke); - } - self.run_until_parked(); - } - - /// Dispatches a single keystroke to a window. - pub fn dispatch_keystroke(&mut self, window: AnyWindowHandle, keystroke: Keystroke) { - self.update_window(window, |_, window, cx| { - window.dispatch_keystroke(keystroke, cx); - }) - .ok(); - } - - /// Simulates typing text input on the given window. - pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) { - for char in input.chars() { - let key = char.to_string(); - let keystroke = Keystroke { - modifiers: Modifiers::default(), - key: key.clone(), - key_char: Some(key), - }; - self.dispatch_keystroke(window, keystroke); - } - self.run_until_parked(); - } - - /// Simulates a mouse move event. - pub fn simulate_mouse_move( - &mut self, - window: AnyWindowHandle, - position: Point, - button: impl Into>, - modifiers: Modifiers, - ) { - self.simulate_event( - window, - MouseMoveEvent { - position, - modifiers, - pressed_button: button.into(), - }, - ); - } - - /// Simulates a mouse down event. - pub fn simulate_mouse_down( - &mut self, - window: AnyWindowHandle, - position: Point, - button: MouseButton, - modifiers: Modifiers, - ) { - self.simulate_event( - window, - MouseDownEvent { - position, - modifiers, - button, - click_count: 1, - first_mouse: false, - }, - ); - } - - /// Simulates a mouse up event. - pub fn simulate_mouse_up( - &mut self, - window: AnyWindowHandle, - position: Point, - button: MouseButton, - modifiers: Modifiers, - ) { - self.simulate_event( - window, - MouseUpEvent { - position, - modifiers, - button, - click_count: 1, - }, - ); - } - - /// Simulates a click (mouse down followed by mouse up). - pub fn simulate_click( - &mut self, - window: AnyWindowHandle, - position: Point, - modifiers: Modifiers, - ) { - self.simulate_mouse_down(window, position, MouseButton::Left, modifiers); - self.simulate_mouse_up(window, position, MouseButton::Left, modifiers); - } - - /// Simulates an input event on the given window. - pub fn simulate_event(&mut self, window: AnyWindowHandle, event: E) { - self.update_window(window, |_, window, cx| { - window.dispatch_event(event.to_platform_input(), cx); - }) - .ok(); - self.run_until_parked(); - } - - /// Dispatches an action to the given window. - pub fn dispatch_action(&mut self, window: AnyWindowHandle, action: impl Action) { - self.update_window(window, |_, window, cx| { - window.dispatch_action(action.boxed_clone(), cx); - }) - .ok(); - self.run_until_parked(); - } - - /// Writes to the clipboard. - pub fn write_to_clipboard(&self, item: ClipboardItem) { - self.platform.write_to_clipboard(item); - } - - /// Reads from the clipboard. - pub fn read_from_clipboard(&self) -> Option { - self.platform.read_from_clipboard() - } - - /// Waits for a condition to become true, with a timeout. - pub async fn wait_for( - &mut self, - entity: &Entity, - predicate: impl Fn(&T) -> bool, - timeout: Duration, - ) -> Result<()> { - let start = web_time::Instant::now(); - loop { - { - let app = self.app.borrow(); - if predicate(entity.read(&app)) { - return Ok(()); - } - } - - if start.elapsed() > timeout { - return Err(anyhow!("Timed out waiting for condition")); - } - - self.run_until_parked(); - self.background_executor - .timer(Duration::from_millis(10)) - .await; - } - } - - /// Captures a screenshot of the specified window using direct texture capture. - /// - /// This renders the scene to a Metal texture and reads the pixels directly, - /// which does not require the window to be visible on screen. - #[cfg(any(test, feature = "test-support"))] - pub fn capture_screenshot(&mut self, window: AnyWindowHandle) -> Result { - self.update_window(window, |_, window, _cx| window.render_to_image())? - } - - /// Waits for animations to complete by waiting a couple of frames. - pub async fn wait_for_animations(&self) { - self.background_executor - .timer(Duration::from_millis(32)) - .await; - self.run_until_parked(); - } -} - -impl AppContext for VisualTestAppContext { - fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity { - let mut app = self.app.borrow_mut(); - app.new(build_entity) - } - - fn reserve_entity(&mut self) -> crate::Reservation { - let mut app = self.app.borrow_mut(); - app.reserve_entity() - } - - fn insert_entity( - &mut self, - reservation: crate::Reservation, - build_entity: impl FnOnce(&mut Context) -> T, - ) -> Entity { - let mut app = self.app.borrow_mut(); - app.insert_entity(reservation, build_entity) - } - - fn update_entity( - &mut self, - handle: &Entity, - update: impl FnOnce(&mut T, &mut Context) -> R, - ) -> R { - let mut app = self.app.borrow_mut(); - app.update_entity(handle, update) - } - - fn as_mut<'a, T>(&'a mut self, _: &Entity) -> crate::GpuiBorrow<'a, T> - where - T: 'static, - { - panic!("Cannot use as_mut with a visual test app context. Try calling update() first") - } - - fn read_entity(&self, handle: &Entity, read: impl FnOnce(&T, &App) -> R) -> R - where - T: 'static, - { - let app = self.app.borrow(); - app.read_entity(handle, read) - } - - fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result - where - F: FnOnce(AnyView, &mut Window, &mut App) -> T, - { - let mut lock = self.app.borrow_mut(); - lock.update_window(window, f) - } - - fn with_window( - &mut self, - entity_id: EntityId, - f: impl FnOnce(&mut Window, &mut App) -> R, - ) -> Option { - let mut lock = self.app.borrow_mut(); - lock.with_window(entity_id, f) - } - - fn read_window( - &self, - window: &WindowHandle, - read: impl FnOnce(Entity, &App) -> R, - ) -> Result - where - T: 'static, - { - let app = self.app.borrow(); - app.read_window(window, read) - } - - fn background_spawn(&self, future: impl Future + Send + 'static) -> Task - where - R: Send + 'static, - { - self.background_executor.spawn(future) - } - - fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R - where - G: Global, - { - let app = self.app.borrow(); - callback(app.global::(), &app) - } -} diff --git a/crates/gpui_pre/src/arena.rs b/crates/gpui_pre/src/arena.rs deleted file mode 100644 index 2ba7a2f..0000000 --- a/crates/gpui_pre/src/arena.rs +++ /dev/null @@ -1,396 +0,0 @@ -use std::{ - alloc::{self, handle_alloc_error}, - cell::Cell, - num::NonZeroUsize, - ops::{Deref, DerefMut}, - ptr::{self, NonNull}, - rc::Rc, -}; - -struct ArenaElement { - value: *mut u8, - drop: unsafe fn(*mut u8), -} - -impl Drop for ArenaElement { - #[inline(always)] - fn drop(&mut self) { - unsafe { (self.drop)(self.value) }; - } -} - -struct Chunk { - start: *mut u8, - end: *mut u8, - offset: *mut u8, -} - -impl Drop for Chunk { - fn drop(&mut self) { - unsafe { - let chunk_size = self.end.offset_from_unsigned(self.start); - // SAFETY: This succeeded during allocation. - let layout = alloc::Layout::from_size_align_unchecked(chunk_size, 1); - alloc::dealloc(self.start, layout); - } - } -} - -impl Chunk { - fn new(chunk_size: NonZeroUsize) -> Self { - // this only fails if chunk_size is unreasonably huge - let layout = alloc::Layout::from_size_align(chunk_size.get(), 1).unwrap(); - let start = unsafe { alloc::alloc(layout) }; - if start.is_null() { - handle_alloc_error(layout); - } - let end = unsafe { start.add(chunk_size.get()) }; - Self { - start, - end, - offset: start, - } - } - - fn allocate(&mut self, layout: alloc::Layout) -> Option> { - // Compute the allocation bounds in integer address space so that the - // bounds check happens before any pointer offsetting. Offsetting a - // pointer past the end of its allocation is undefined behavior even if - // the result is never dereferenced (as happens on the chunk-spill - // path), so `ptr::add` cannot be used until we know the result stays - // in bounds. `checked_add` also handles the documented case where - // `align_offset` returns `usize::MAX`. - let base = self.offset.addr(); - let aligned_addr = base.checked_add(self.offset.align_offset(layout.align()))?; - let next_addr = aligned_addr.checked_add(layout.size())?; - - if next_addr <= self.end.addr() { - let aligned = self.offset.with_addr(aligned_addr); - self.offset = self.offset.with_addr(next_addr); - NonNull::new(aligned) - } else { - None - } - } - - fn reset(&mut self) { - self.offset = self.start; - } -} - -pub struct Arena { - chunks: Vec, - elements: Vec, - valid: Rc>, - current_chunk_index: usize, - chunk_size: NonZeroUsize, - scope_depth: usize, -} - -impl Drop for Arena { - fn drop(&mut self) { - self.force_clear(); - } -} - -impl Arena { - pub fn new(chunk_size: usize) -> Self { - let chunk_size = NonZeroUsize::try_from(chunk_size).unwrap(); - Self { - chunks: vec![Chunk::new(chunk_size)], - elements: Vec::new(), - valid: Rc::new(Cell::new(true)), - current_chunk_index: 0, - chunk_size, - scope_depth: 0, - } - } - - pub fn capacity(&self) -> usize { - self.chunks.len() * self.chunk_size.get() - } - - /// Marks the start of a scope (e.g. a window draw) whose allocations must stay - /// live until the scope ends, even if `clear` is called by a nested scope in - /// the meantime. - pub fn begin_scope(&mut self) { - self.scope_depth += 1; - } - - /// Ends the innermost scope started with `begin_scope`. - /// - /// Panics if no scope is active: an unbalanced `end_scope` would let `clear` - /// run while an enclosing scope still references arena memory, which is - /// exactly the use-after-free this bookkeeping exists to prevent, so failing - /// loudly here is preferable. - pub fn end_scope(&mut self) { - self.scope_depth = self - .scope_depth - .checked_sub(1) - .expect("Arena::end_scope called without a matching begin_scope"); - } - - /// Drops all allocations and resets the arena, unless a scope is still active. - /// - /// When a draw triggers a nested draw (e.g. re-entrant window procedure - /// invocations on Windows, or opening a window from within a draw), the nested - /// draw's clear must not free memory the outer draw still references, so it is - /// deferred: the outer draw's own clear will drop both draws' allocations. - pub fn clear(&mut self) { - if self.scope_depth == 0 { - self.force_clear(); - } else { - log::debug!( - "deferring arena clear; {} enclosing scope(s) still active", - self.scope_depth - ); - } - } - - fn force_clear(&mut self) { - self.valid.set(false); - self.valid = Rc::new(Cell::new(true)); - self.elements.clear(); - for chunk_index in 0..=self.current_chunk_index { - self.chunks[chunk_index].reset(); - } - self.current_chunk_index = 0; - } - - #[inline(always)] - pub fn alloc(&mut self, f: impl FnOnce() -> T) -> ArenaBox { - #[inline(always)] - unsafe fn inner_writer(ptr: *mut T, f: F) - where - F: FnOnce() -> T, - { - unsafe { ptr::write(ptr, f()) }; - } - - unsafe fn drop(ptr: *mut u8) { - unsafe { std::ptr::drop_in_place(ptr.cast::()) }; - } - - let layout = alloc::Layout::new::(); - let mut current_chunk = &mut self.chunks[self.current_chunk_index]; - let ptr = if let Some(ptr) = current_chunk.allocate(layout) { - ptr.as_ptr() - } else { - self.current_chunk_index += 1; - if self.current_chunk_index >= self.chunks.len() { - self.chunks.push(Chunk::new(self.chunk_size)); - assert_eq!(self.current_chunk_index, self.chunks.len() - 1); - log::trace!( - "increased element arena capacity to {}kb", - self.capacity() / 1024, - ); - } - current_chunk = &mut self.chunks[self.current_chunk_index]; - if let Some(ptr) = current_chunk.allocate(layout) { - ptr.as_ptr() - } else { - panic!( - "Arena chunk_size of {} is too small to allocate {} bytes", - self.chunk_size, - layout.size() - ); - } - }; - - unsafe { inner_writer(ptr.cast(), f) }; - self.elements.push(ArenaElement { - value: ptr, - drop: drop::, - }); - - ArenaBox { - ptr: ptr.cast(), - valid: self.valid.clone(), - } - } -} - -pub struct ArenaBox { - ptr: *mut T, - valid: Rc>, -} - -impl ArenaBox { - #[inline(always)] - pub fn map(mut self, f: impl FnOnce(&mut T) -> &mut U) -> ArenaBox { - ArenaBox { - ptr: f(&mut self), - valid: self.valid, - } - } - - #[track_caller] - fn validate(&self) { - assert!( - self.valid.get(), - "attempted to dereference an ArenaRef after its Arena was cleared" - ); - } -} - -impl Deref for ArenaBox { - type Target = T; - - #[inline(always)] - fn deref(&self) -> &Self::Target { - self.validate(); - unsafe { &*self.ptr } - } -} - -impl DerefMut for ArenaBox { - #[inline(always)] - fn deref_mut(&mut self) -> &mut Self::Target { - self.validate(); - unsafe { &mut *self.ptr } - } -} - -#[cfg(test)] -mod tests { - use std::{cell::Cell, rc::Rc}; - - use super::*; - - #[test] - fn test_arena() { - let mut arena = Arena::new(1024); - let a = arena.alloc(|| 1u64); - let b = arena.alloc(|| 2u32); - let c = arena.alloc(|| 3u16); - let d = arena.alloc(|| 4u8); - assert_eq!(*a, 1); - assert_eq!(*b, 2); - assert_eq!(*c, 3); - assert_eq!(*d, 4); - - arena.clear(); - let a = arena.alloc(|| 5u64); - let b = arena.alloc(|| 6u32); - let c = arena.alloc(|| 7u16); - let d = arena.alloc(|| 8u8); - assert_eq!(*a, 5); - assert_eq!(*b, 6); - assert_eq!(*c, 7); - assert_eq!(*d, 8); - - // Ensure drop gets called. - let dropped = Rc::new(Cell::new(false)); - struct DropGuard(Rc>); - impl Drop for DropGuard { - fn drop(&mut self) { - self.0.set(true); - } - } - arena.alloc(|| DropGuard(dropped.clone())); - arena.clear(); - assert!(dropped.get()); - } - - #[test] - fn test_arena_grow() { - let mut arena = Arena::new(8); - arena.alloc(|| 1u64); - arena.alloc(|| 2u64); - - assert_eq!(arena.capacity(), 16); - - arena.alloc(|| 3u32); - arena.alloc(|| 4u32); - - assert_eq!(arena.capacity(), 24); - } - - #[test] - fn test_arena_alignment() { - let mut arena = Arena::new(256); - let x1 = arena.alloc(|| 1u8); - let x2 = arena.alloc(|| 2u16); - let x3 = arena.alloc(|| 3u32); - let x4 = arena.alloc(|| 4u64); - let x5 = arena.alloc(|| 5u64); - - assert_eq!(*x1, 1); - assert_eq!(*x2, 2); - assert_eq!(*x3, 3); - assert_eq!(*x4, 4); - assert_eq!(*x5, 5); - - assert_eq!(x1.ptr.align_offset(std::mem::align_of_val(&*x1)), 0); - assert_eq!(x2.ptr.align_offset(std::mem::align_of_val(&*x2)), 0); - } - - #[test] - #[should_panic(expected = "attempted to dereference an ArenaRef after its Arena was cleared")] - fn test_arena_use_after_clear() { - let mut arena = Arena::new(16); - let value = arena.alloc(|| 1u64); - - arena.clear(); - let _read_value = *value; - } - - #[test] - fn test_clear_deferred_while_scope_active() { - struct DropCounter(Rc>); - impl Drop for DropCounter { - fn drop(&mut self) { - self.0.set(self.0.get() + 1); - } - } - - let drops = Rc::new(Cell::new(0)); - let mut arena = Arena::new(1024); - - // Outer draw starts and allocates. - arena.begin_scope(); - let outer = arena.alloc(|| 42u64); - arena.alloc({ - let drops = drops.clone(); - || DropCounter(drops) - }); - - // Nested draw runs to completion and requests a clear. - arena.begin_scope(); - let inner = arena.alloc(|| 7u64); - arena.alloc({ - let drops = drops.clone(); - || DropCounter(drops) - }); - arena.end_scope(); - arena.clear(); - - // The clear must be deferred: the outer draw's allocations are still live. - assert_eq!(*outer, 42); - assert_eq!(*inner, 7); - assert_eq!(drops.get(), 0); - - // Once the outer draw finishes, its clear drops both draws' allocations. - arena.end_scope(); - arena.clear(); - assert_eq!(drops.get(), 2); - } - - #[test] - fn test_clear_without_scope_is_immediate() { - let mut arena = Arena::new(1024); - let value = arena.alloc(|| 1u64); - assert_eq!(*value, 1); - arena.clear(); - assert!(!value.valid.get()); - } - - #[test] - #[should_panic(expected = "Arena::end_scope called without a matching begin_scope")] - fn test_unbalanced_end_scope_panics() { - let mut arena = Arena::new(1024); - arena.begin_scope(); - arena.end_scope(); - arena.end_scope(); - } -} diff --git a/crates/gpui_pre/src/asset_cache.rs b/crates/gpui_pre/src/asset_cache.rs deleted file mode 100644 index bd12715..0000000 --- a/crates/gpui_pre/src/asset_cache.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::{App, SharedString, SharedUri}; -use futures::{Future, TryFutureExt}; - -use std::fmt::Debug; -use std::hash::{BuildHasher, Hash}; -use std::marker::PhantomData; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -/// An enum representing -#[derive(Debug, PartialEq, Eq, Hash, Clone)] -pub enum Resource { - /// This resource is at a given URI - Uri(SharedUri), - /// This resource is at a given path in the file system - Path(Arc), - /// This resource is embedded in the application binary - Embedded(SharedString), -} - -impl From for Resource { - fn from(value: SharedUri) -> Self { - Self::Uri(value) - } -} - -impl From for Resource { - fn from(value: PathBuf) -> Self { - Self::Path(value.into()) - } -} - -impl From> for Resource { - fn from(value: Arc) -> Self { - Self::Path(value) - } -} - -/// A trait for asynchronous asset loading. -pub trait Asset: 'static { - /// The source of the asset. - type Source: Clone + Hash + Send; - - /// The loaded asset - type Output: Clone + Send; - - /// Load the asset asynchronously - fn load( - source: Self::Source, - cx: &mut App, - ) -> impl Future + Send + 'static; -} - -/// An asset Loader which logs the [`Err`] variant of a [`Result`] during loading -pub enum AssetLogger { - #[doc(hidden)] - _Phantom(PhantomData, &'static dyn crate::seal::Sealed), -} - -impl Asset for AssetLogger -where - T: Asset>, - R: Clone + Send, - E: Clone + Send + Debug, -{ - type Source = T::Source; - - type Output = T::Output; - - fn load( - source: Self::Source, - cx: &mut App, - ) -> impl Future + Send + 'static { - let load = T::load(source, cx); - load.inspect_err(|e| log::error!("Failed to load asset: {:?}", e)) - } -} - -/// Use a quick, non-cryptographically secure hash function to get an identifier from data -pub fn hash(data: &T) -> u64 { - collections::FxBuildHasher.hash_one(data) -} diff --git a/crates/gpui_pre/src/assets.rs b/crates/gpui_pre/src/assets.rs deleted file mode 100644 index cb8b47e..0000000 --- a/crates/gpui_pre/src/assets.rs +++ /dev/null @@ -1,133 +0,0 @@ -use crate::{DevicePixels, Pixels, Result, SharedString, Size, size}; -use smallvec::SmallVec; - -use image::{Delay, Frame}; -use std::{ - borrow::Cow, - fmt, - hash::Hash, - sync::atomic::{AtomicUsize, Ordering::SeqCst}, -}; - -/// A source of assets for this app to use. -pub trait AssetSource: 'static + Send + Sync { - /// Load the given asset from the source path. - fn load(&self, path: &str) -> Result>>; - - /// List the assets at the given path. - fn list(&self, path: &str) -> Result>; -} - -impl AssetSource for () { - fn load(&self, _path: &str) -> Result>> { - Ok(None) - } - - fn list(&self, _path: &str) -> Result> { - Ok(vec![]) - } -} - -/// A unique identifier for the image cache -#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct ImageId(pub usize); - -#[derive(PartialEq, Eq, Hash, Clone)] -#[expect(missing_docs)] -pub struct RenderImageParams { - pub image_id: ImageId, - pub frame_index: usize, -} - -/// A cached and processed image, in BGRA format -pub struct RenderImage { - /// The ID associated with this image - pub id: ImageId, - /// The scale factor of this image on render. - pub(crate) scale_factor: f32, - data: SmallVec<[Frame; 1]>, -} - -impl PartialEq for RenderImage { - fn eq(&self, other: &Self) -> bool { - self.id == other.id - } -} - -impl Eq for RenderImage {} - -impl RenderImage { - /// Create a new image from the given data. - pub fn new(data: impl Into>) -> Self { - static NEXT_ID: AtomicUsize = AtomicUsize::new(0); - - Self { - id: ImageId(NEXT_ID.fetch_add(1, SeqCst)), - scale_factor: 1.0, - data: data.into(), - } - } - - /// Convert this image into a byte slice. - pub fn as_bytes(&self, frame_index: usize) -> Option<&[u8]> { - self.data - .get(frame_index) - .map(|frame| frame.buffer().as_raw().as_slice()) - } - - /// Get the size of this image, in pixels. - pub fn size(&self, frame_index: usize) -> Size { - self.data - .get(frame_index) - .map(|frame| { - let (width, height) = frame.buffer().dimensions(); - size(width.into(), height.into()) - }) - .unwrap_or_default() - } - - /// Get the size of this image, in pixels for display, adjusted for the scale factor. - pub(crate) fn render_size(&self, frame_index: usize) -> Size { - self.size(frame_index) - .map(|v| (v.0 as f32 / self.scale_factor).into()) - } - - /// Get the delay of this frame from the previous - pub fn delay(&self, frame_index: usize) -> Delay { - self.data - .get(frame_index) - .map(|frame| frame.delay()) - .unwrap_or(Delay::from_numer_denom_ms(100, 1)) - } - - /// Get the number of frames for this image. - pub fn frame_count(&self) -> usize { - self.data.len() - } -} - -impl fmt::Debug for RenderImage { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("ImageData") - .field("id", &self.id) - .field("size", &self.data.first().map(|f| f.buffer().dimensions())) - .finish() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use smallvec::SmallVec; - - #[test] - fn empty_render_image_does_not_panic() { - let image = RenderImage::new(SmallVec::new()); - assert_eq!(image.frame_count(), 0); - assert_eq!(image.size(0), Size::default()); - assert_eq!(image.as_bytes(0), None); - assert_eq!(image.render_size(0), Size::default()); - assert_eq!(image.delay(0), Delay::from_numer_denom_ms(100, 1)); - let _ = format!("{image:?}"); - } -} diff --git a/crates/gpui_pre/src/bounds_tree.rs b/crates/gpui_pre/src/bounds_tree.rs deleted file mode 100644 index e95e324..0000000 --- a/crates/gpui_pre/src/bounds_tree.rs +++ /dev/null @@ -1,472 +0,0 @@ -use crate::{Bounds, Half}; -use std::{ - cmp, - fmt::Debug, - ops::{Add, Sub}, - ptr::NonNull, -}; - -/// Maximum children per internal node (R-tree style branching factor). -/// Higher values = shorter tree = fewer cache misses, but more work per node. -const MAX_CHILDREN: usize = 12; - -/// A spatial tree optimized for finding maximum ordering among intersecting bounds. -/// -/// This is an R-tree variant specifically designed for the use case of assigning -/// z-order to overlapping UI elements. Key optimizations: -/// - Tracks the leaf with global max ordering for O(1) fast-path queries -/// - Uses higher branching factor (4) for lower tree height -/// - Aggressive pruning during search based on max_order metadata -#[derive(Debug)] -pub(crate) struct BoundsTree -where - U: Clone + Debug + Default + PartialEq, -{ - /// All nodes stored contiguously for cache efficiency. - nodes: Vec>, - /// Index of the root node, if any. - root: Option, - /// Index of the leaf with the highest ordering (for fast-path lookups). - max_leaf: Option, - /// Reusable stack for tree traversal during insertion. - insert_path: Vec, - /// Reusable stack for search operations. - search_stack: Vec>>, -} - -/// A node in the bounds tree. -#[derive(Debug, Clone)] -struct Node -where - U: Clone + Debug + Default + PartialEq, -{ - /// Bounding box containing this node and all descendants. - bounds: Bounds, - /// Maximum ordering value in this subtree. - max_order: u32, - /// Node-specific data. - kind: NodeKind, -} - -#[derive(Debug, Clone)] -enum NodeKind { - /// Leaf node containing actual bounds data. - Leaf { - /// The ordering assigned to this bounds. - order: u32, - }, - /// Internal node with children. - Internal { - /// Indices of child nodes (2 to MAX_CHILDREN). - children: NodeChildren, - }, -} - -/// Fixed-size array for child indices, avoiding heap allocation. -#[derive(Debug, Clone)] -struct NodeChildren { - // Keeps an invariant where the max order child is always at the end - indices: [usize; MAX_CHILDREN], - len: u8, -} - -impl NodeChildren { - fn new() -> Self { - Self { - indices: [0; MAX_CHILDREN], - len: 0, - } - } - - fn push(&mut self, index: usize) { - debug_assert!((self.len as usize) < MAX_CHILDREN); - self.indices[self.len as usize] = index; - self.len += 1; - } - - fn len(&self) -> usize { - self.len as usize - } - - fn as_slice(&self) -> &[usize] { - &self.indices[..self.len as usize] - } -} - -impl BoundsTree -where - U: Clone - + Debug - + PartialEq - + PartialOrd - + Add - + Sub - + Half - + Default, -{ - /// Clears all nodes from the tree. - pub fn clear(&mut self) { - self.nodes.clear(); - self.root = None; - self.max_leaf = None; - self.insert_path.clear(); - self.search_stack.clear(); - } - - /// Inserts bounds into the tree and returns its assigned ordering. - /// - /// The ordering is one greater than the maximum ordering of any - /// existing bounds that intersect with the new bounds. - pub fn insert(&mut self, new_bounds: Bounds) -> u32 { - // Find maximum ordering among intersecting bounds - let max_intersecting = self.find_max_ordering(&new_bounds); - let ordering = max_intersecting + 1; - - // Insert the new leaf - let new_leaf_idx = self.insert_leaf(new_bounds, ordering); - - // Update max_leaf tracking - self.max_leaf = match self.max_leaf { - None => Some(new_leaf_idx), - Some(old_idx) if self.nodes[old_idx].max_order < ordering => Some(new_leaf_idx), - some => some, - }; - - ordering - } - - /// Finds the maximum ordering among all bounds that intersect with the query. - fn find_max_ordering(&mut self, query: &Bounds) -> u32 { - let Some(root_idx) = self.root else { - return 0; - }; - - // Fast path: check if the max-ordering leaf intersects - if let Some(max_idx) = self.max_leaf { - let max_node = &self.nodes[max_idx]; - if query.intersects(&max_node.bounds) { - return max_node.max_order; - } - } - - // Slow path: search the tree - self.search_stack.clear(); - self.search_stack.push(NonNull::from(&self.nodes[root_idx])); - - let mut max_found = 0u32; - - while let Some(node) = self.search_stack.pop() { - // SAFETY: `node` is guaranteed to be valid as the `nodes` stack is unmodified in this function - // and the `search_stack` only contains pointers from this function call. - let node = unsafe { node.as_ref() }; - - // Pruning: skip if this subtree can't improve our result - if node.max_order <= max_found { - continue; - } - - // Spatial pruning: skip if bounds don't intersect - if !query.intersects(&node.bounds) { - continue; - } - - match &node.kind { - NodeKind::Leaf { order } => { - max_found = cmp::max(max_found, *order); - } - NodeKind::Internal { children } => { - // Children are maintained with highest max_order at the end. - // Push in forward order to highest (last) is popped first. - self.search_stack.extend( - children - .as_slice() - .iter() - .map(|&child_idx| &self.nodes[child_idx]) - .filter(|node| node.max_order > max_found) - .map(NonNull::from), - ); - } - } - } - - max_found - } - - /// Inserts a leaf node with the given bounds and ordering. - /// Returns the index of the new leaf. - fn insert_leaf(&mut self, bounds: Bounds, order: u32) -> usize { - let new_leaf_idx = self.nodes.len(); - self.nodes.push(Node { - bounds: bounds.clone(), - max_order: order, - kind: NodeKind::Leaf { order }, - }); - - let Some(root_idx) = self.root else { - // Tree is empty, new leaf becomes root - self.root = Some(new_leaf_idx); - return new_leaf_idx; - }; - - // If root is a leaf, create internal node with both - if matches!(self.nodes[root_idx].kind, NodeKind::Leaf { .. }) { - let root_bounds = self.nodes[root_idx].bounds.clone(); - let root_order = self.nodes[root_idx].max_order; - - let mut children = NodeChildren::new(); - // Max end invariant - if order > root_order { - children.push(root_idx); - children.push(new_leaf_idx); - } else { - children.push(new_leaf_idx); - children.push(root_idx); - } - - let new_root_idx = self.nodes.len(); - self.nodes.push(Node { - bounds: root_bounds.union(&bounds), - max_order: cmp::max(root_order, order), - kind: NodeKind::Internal { children }, - }); - self.root = Some(new_root_idx); - return new_leaf_idx; - } - - // Descend to find the best internal node to insert into - self.insert_path.clear(); - let mut current_idx = root_idx; - - loop { - let current = &self.nodes[current_idx]; - let NodeKind::Internal { children } = ¤t.kind else { - unreachable!("Should only traverse internal nodes"); - }; - - self.insert_path.push(current_idx); - - // Find the best child to descend into - let mut best_child_idx = children.as_slice()[0]; - let mut best_child_pos = 0; - let mut best_cost = bounds - .union(&self.nodes[best_child_idx].bounds) - .half_perimeter(); - - for (pos, &child_idx) in children.as_slice().iter().enumerate().skip(1) { - let cost = bounds.union(&self.nodes[child_idx].bounds).half_perimeter(); - if cost < best_cost { - best_cost = cost; - best_child_idx = child_idx; - best_child_pos = pos; - } - } - - // Check if best child is a leaf or internal - if matches!(self.nodes[best_child_idx].kind, NodeKind::Leaf { .. }) { - // Best child is a leaf. Check if current node has room for another child. - if children.len() < MAX_CHILDREN { - // Add new leaf directly to this node - let node = &mut self.nodes[current_idx]; - - if let NodeKind::Internal { children } = &mut node.kind { - children.push(new_leaf_idx); - // Swap new leaf only if it has the highest max_order - if order <= node.max_order { - let last = children.len() - 1; - children.indices.swap(last - 1, last); - } - } - - node.bounds = node.bounds.union(&bounds); - node.max_order = cmp::max(node.max_order, order); - break; - } else { - // Node is full, create new internal with [best_leaf, new_leaf] - let sibling_bounds = self.nodes[best_child_idx].bounds.clone(); - let sibling_order = self.nodes[best_child_idx].max_order; - - let mut new_children = NodeChildren::new(); - // Max end invariant - if order > sibling_order { - new_children.push(best_child_idx); - new_children.push(new_leaf_idx); - } else { - new_children.push(new_leaf_idx); - new_children.push(best_child_idx); - } - - let new_internal_idx = self.nodes.len(); - let new_internal_max = cmp::max(sibling_order, order); - self.nodes.push(Node { - bounds: sibling_bounds.union(&bounds), - max_order: new_internal_max, - kind: NodeKind::Internal { - children: new_children, - }, - }); - - // Replace the leaf with the new internal in parent - let parent = &mut self.nodes[current_idx]; - if let NodeKind::Internal { children } = &mut parent.kind { - let children_len = children.len(); - - children.indices[best_child_pos] = new_internal_idx; - - // If new internal has highest max_order, swap it to the end - // to maintain sorting invariant - if new_internal_max > parent.max_order { - children.indices.swap(best_child_pos, children_len - 1); - } - } - break; - } - } else { - // Best child is internal, continue descent - current_idx = best_child_idx; - } - } - - // Propagate bounds and max_order updates up the tree - let mut updated_child_idx = None; - for &node_idx in self.insert_path.iter().rev() { - let node = &mut self.nodes[node_idx]; - node.bounds = node.bounds.union(&bounds); - - if node.max_order < order { - node.max_order = order; - - // Swap updated child to end (skip first iteration since the invariant is already handled by previous cases) - if let Some(child_idx) = updated_child_idx { - if let NodeKind::Internal { children } = &mut node.kind { - if let Some(pos) = children.as_slice().iter().position(|&c| c == child_idx) - { - let last = children.len() - 1; - if pos != last { - children.indices.swap(pos, last); - } - } - } - } - } - - updated_child_idx = Some(node_idx); - } - - new_leaf_idx - } -} - -impl Default for BoundsTree -where - U: Clone + Debug + Default + PartialEq, -{ - fn default() -> Self { - BoundsTree { - nodes: Vec::new(), - root: None, - max_leaf: None, - insert_path: Vec::new(), - search_stack: Vec::new(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{Bounds, Point, Size}; - use rand::{Rng, SeedableRng}; - - #[test] - fn test_insert() { - let mut tree = BoundsTree::::default(); - let bounds1 = Bounds { - origin: Point { x: 0.0, y: 0.0 }, - size: Size { - width: 10.0, - height: 10.0, - }, - }; - let bounds2 = Bounds { - origin: Point { x: 5.0, y: 5.0 }, - size: Size { - width: 10.0, - height: 10.0, - }, - }; - let bounds3 = Bounds { - origin: Point { x: 10.0, y: 10.0 }, - size: Size { - width: 10.0, - height: 10.0, - }, - }; - - // Insert the bounds into the tree and verify the order is correct - assert_eq!(tree.insert(bounds1), 1); - assert_eq!(tree.insert(bounds2), 2); - assert_eq!(tree.insert(bounds3), 3); - - // Insert non-overlapping bounds and verify they can reuse orders - let bounds4 = Bounds { - origin: Point { x: 20.0, y: 20.0 }, - size: Size { - width: 10.0, - height: 10.0, - }, - }; - let bounds5 = Bounds { - origin: Point { x: 40.0, y: 40.0 }, - size: Size { - width: 10.0, - height: 10.0, - }, - }; - let bounds6 = Bounds { - origin: Point { x: 25.0, y: 25.0 }, - size: Size { - width: 10.0, - height: 10.0, - }, - }; - assert_eq!(tree.insert(bounds4), 1); // bounds4 does not overlap with bounds1, bounds2, or bounds3 - assert_eq!(tree.insert(bounds5), 1); // bounds5 does not overlap with any other bounds - assert_eq!(tree.insert(bounds6), 2); // bounds6 overlaps with bounds4, so it should have a different order - } - - #[test] - fn test_random_iterations() { - let max_bounds = 100; - for seed in 1..=1000 { - // let seed = 44; - let mut tree = BoundsTree::default(); - let mut rng = rand::rngs::StdRng::seed_from_u64(seed as u64); - let mut expected_quads: Vec<(Bounds, u32)> = Vec::new(); - - // Insert a random number of random AABBs into the tree. - let num_bounds = rng.random_range(1..=max_bounds); - for _ in 0..num_bounds { - let min_x: f32 = rng.random_range(-100.0..100.0); - let min_y: f32 = rng.random_range(-100.0..100.0); - let width: f32 = rng.random_range(0.0..50.0); - let height: f32 = rng.random_range(0.0..50.0); - let bounds = Bounds { - origin: Point { x: min_x, y: min_y }, - size: Size { width, height }, - }; - - let expected_ordering = expected_quads - .iter() - .filter_map(|quad| quad.0.intersects(&bounds).then_some(quad.1)) - .max() - .unwrap_or(0) - + 1; - expected_quads.push((bounds, expected_ordering)); - - // Insert the AABB into the tree and collect intersections. - let actual_ordering = tree.insert(bounds); - assert_eq!(actual_ordering, expected_ordering); - } - } - } -} diff --git a/crates/gpui_pre/src/clip.rs b/crates/gpui_pre/src/clip.rs deleted file mode 100644 index 4bb79d9..0000000 --- a/crates/gpui_pre/src/clip.rs +++ /dev/null @@ -1,188 +0,0 @@ -//! Rounded clipping keeps each ancestor's geometry separate from culling bounds. - -use crate::{px, Bounds, ContentMask, Corners, Pixels, Point, ScaledPixels}; -use smallvec::SmallVec; -use std::fmt::Debug; - -/// One immutable rounded rectangle in a scene's clip chain. -/// Separate horizontal and vertical radii preserve corners inset by unequal borders. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -#[repr(C)] -pub struct RoundedClip { - /// Original geometry, never replaced by an intersection's bounding box. - pub bounds: Bounds

, - /// Horizontal corner radii. - pub radii_x: Corners

, - /// Vertical corner radii. - pub radii_y: Corners

, - /// One-based index of the next clip in the scene; zero ends the chain. - pub parent: u32, - /// Explicit GPU record padding. - pub padding: u32, -} - -impl RoundedClip { - /// Convert logical clip geometry to device coordinates. - pub fn scale(&self, scale: f32) -> RoundedClip { - RoundedClip { - bounds: self.bounds.scale(scale), - radii_x: self.radii_x.scale(scale), - radii_y: self.radii_y.scale(scale), - parent: 0, - padding: 0, - } - } - - /// Exact point membership in the rounded rectangle. - pub fn contains(&self, point: Point) -> bool { - if self.bounds.is_empty() || !self.bounds.contains(&point) { - return false; - } - let x = f32::from(point.x - self.bounds.left()); - let y = f32::from(point.y - self.bounds.top()); - let right = f32::from(self.bounds.right() - point.x); - let bottom = f32::from(self.bounds.bottom() - point.y); - [ - (x, y, self.radii_x.top_left, self.radii_y.top_left), - (right, y, self.radii_x.top_right, self.radii_y.top_right), - ( - right, - bottom, - self.radii_x.bottom_right, - self.radii_y.bottom_right, - ), - ( - x, - bottom, - self.radii_x.bottom_left, - self.radii_y.bottom_left, - ), - ] - .into_iter() - .all(|(x, y, rx, ry)| { - let (rx, ry) = (f32::from(rx), f32::from(ry)); - if rx <= 0.0 || ry <= 0.0 || x >= rx || y >= ry { - return true; - } - let dx = (x - rx) / rx; - let dy = (y - ry) / ry; - dx * dx + dy * dy <= 1.0 - }) - } -} - -/// The exact intersection of rectangular and rounded ancestor clips. -/// -/// `bounds` is only a culling rectangle. Rounded curves keep the bounds and -/// radii of their owning element, even when a narrow descendant intersects them. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct ClipRegion { - /// Rectangular intersection used for culling and layout visibility. - pub bounds: Bounds, - /// Rounded constraints still relevant within `bounds`. - pub rounded_clips: SmallVec<[RoundedClip; 1]>, -} - -impl From> for ClipRegion { - fn from(mask: ContentMask) -> Self { - let radii = mask - .corner_radii - .clamp_radii_for_quad_size(mask.bounds.size); - Self::rounded(mask.bounds, radii, radii) - } -} - -impl ClipRegion { - /// Construct a clip with elliptical corners. The caller normalizes radii - /// against the original element before deriving any border inset. - pub fn rounded( - bounds: Bounds, - radii_x: Corners, - radii_y: Corners, - ) -> Self { - let mut region = Self { - bounds, - rounded_clips: SmallVec::new(), - }; - if radii_x.max() > px(0.) && radii_y.max() > px(0.) && !bounds.is_empty() { - region.rounded_clips.push(RoundedClip { - bounds, - radii_x, - radii_y, - parent: 0, - padding: 0, - }); - } - region - } - - /// Intersect without approximating or relocating either region's curves. - pub fn intersect(&self, other: &Self) -> Self { - let bounds = self.bounds.intersect(&other.bounds); - let mut result = Self { - bounds, - rounded_clips: SmallVec::new(), - }; - if bounds.is_empty() { - return result; - } - for clip in self.rounded_clips.iter().chain(&other.rounded_clips) { - // Logical containment cannot prune a curve: the GPU's conservative - // culling rectangle can expand across its antialiased edge. - if result.rounded_clips.contains(clip) { - continue; - } - result.rounded_clips.push(*clip); - } - result - } - - /// Exact logical point membership, independent of rasterization scale. - pub fn contains(&self, point: Point) -> bool { - !self.bounds.is_empty() - && self.bounds.contains(&point) - && self.rounded_clips.iter().all(|clip| clip.contains(point)) - } -} - -/// Concrete clip record exported to the Metal shader bindings. -#[allow(non_camel_case_types)] -pub type RoundedClip_ScaledPixels = RoundedClip; - -#[cfg(test)] -mod tests { - use super::*; - use crate::point; - - #[test] - fn nested_clips_preserve_both_original_shapes() { - let outer: ClipRegion = ContentMask { - bounds: Bounds::from_corners(point(px(0.), px(0.)), point(px(100.), px(100.))), - corner_radii: Corners::all(px(16.)), - ..Default::default() - } - .into(); - for (left, top, right, bottom) in - [(4., 4., 96., 96.), (0., 0., 10., 100.), (7., 1., 94., 80.)] - { - let inner: ClipRegion = ContentMask { - bounds: Bounds::from_corners( - point(px(left), px(top)), - point(px(right), px(bottom)), - ), - ..Default::default() - } - .into(); - let intersection = outer.intersect(&inner); - for x in 0..100 { - for y in 0..100 { - let point = point(px(x as f32 + 0.5), px(y as f32 + 0.5)); - assert_eq!( - intersection.contains(point), - outer.contains(point) && inner.contains(point) - ); - } - } - } - } -} diff --git a/crates/gpui_pre/src/color.rs b/crates/gpui_pre/src/color.rs deleted file mode 100644 index 3bd893f..0000000 --- a/crates/gpui_pre/src/color.rs +++ /dev/null @@ -1,1070 +0,0 @@ -use anyhow::{Context as _, bail}; -use schemars::{JsonSchema, json_schema}; -use serde::{ - Deserialize, Deserializer, Serialize, Serializer, - de::{self, Visitor}, -}; -use std::borrow::Cow; -use std::{ - fmt::{self, Display, Formatter}, - hash::{Hash, Hasher}, -}; - -/// Convert an RGB hex color code number to a color type -pub fn rgb(hex: u32) -> Rgba { - let [_, r, g, b] = hex.to_be_bytes().map(|b| (b as f32) / 255.0); - Rgba { r, g, b, a: 1.0 } -} - -/// Convert an RGBA hex color code number to [`Rgba`] -pub fn rgba(hex: u32) -> Rgba { - let [r, g, b, a] = hex.to_be_bytes().map(|b| (b as f32) / 255.0); - Rgba { r, g, b, a } -} - -/// Swap from RGBA with premultiplied alpha to BGRA -pub fn swap_rgba_pa_to_bgra(color: &mut [u8]) { - color.swap(0, 2); - if color[3] > 0 { - let a = color[3] as f32 / 255.; - color[0] = (color[0] as f32 / a) as u8; - color[1] = (color[1] as f32 / a) as u8; - color[2] = (color[2] as f32 / a) as u8; - } -} - -/// An RGBA color -#[derive(PartialEq, Clone, Copy, Default)] -#[repr(C)] -pub struct Rgba { - /// The red component of the color, in the range 0.0 to 1.0 - pub r: f32, - /// The green component of the color, in the range 0.0 to 1.0 - pub g: f32, - /// The blue component of the color, in the range 0.0 to 1.0 - pub b: f32, - /// The alpha component of the color, in the range 0.0 to 1.0 - pub a: f32, -} - -impl fmt::Debug for Rgba { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "rgba({:#010x})", u32::from(*self)) - } -} - -impl Rgba { - /// Create a new [`Rgba`] color by blending this and another color together - pub fn blend(&self, other: Rgba) -> Self { - if other.a >= 1.0 { - other - } else if other.a <= 0.0 { - *self - } else { - Rgba { - r: (self.r * (1.0 - other.a)) + (other.r * other.a), - g: (self.g * (1.0 - other.a)) + (other.g * other.a), - b: (self.b * (1.0 - other.a)) + (other.b * other.a), - a: self.a, - } - } - } - - /// Returns a new RGBA color with the same red, green and blue channels, but - /// with a new alpha value. - /// - /// Example: - /// ``` - /// use gpui::rgba; - /// let color = rgba(0xFF0000FF); - /// let faded = color.alpha(0.25); - /// assert_eq!(faded.a, 0.25); - /// ``` - /// - /// This will return a red color with 25% opacity. - /// - /// Example: - /// ``` - /// use gpui::rgba; - /// let color = rgba(0x3399FFCC); - /// let transparent = color.alpha(0.0); - /// assert_eq!(transparent.a, 0.0); - /// ``` - /// - /// This will return the same blue color, fully transparent. - pub fn alpha(&self, a: f32) -> Self { - Rgba { - r: self.r, - g: self.g, - b: self.b, - a: a.clamp(0., 1.), - } - } - - /// Returns a new RGBA color with the same red, green, and blue channels, - /// but with the alpha channel multiplied by the given factor. - /// - /// Example: - /// ``` - /// use gpui::rgba; - /// let color = rgba(0xFF0000FF); // Fully opaque red - /// let faded = color.opacity(0.5); - /// assert_eq!(faded.a, 0.5); - /// ``` - /// - /// This will return a red color with 50% opacity. - /// - /// Example: - /// ``` - /// use gpui::rgba; - /// let color = rgba(0x3399FFCC); // A light blue with 80% opacity - /// let faded = color.opacity(0.5); - /// assert!((faded.a - 0.4).abs() < 1e-6); - /// ``` - /// - /// This will return the same blue color scaled down to 40% opacity. - pub fn opacity(&self, factor: f32) -> Self { - Rgba { - r: self.r, - g: self.g, - b: self.b, - a: self.a * factor.clamp(0., 1.), - } - } -} - -impl From for u32 { - fn from(rgba: Rgba) -> Self { - let r = (rgba.r * 255.0) as u32; - let g = (rgba.g * 255.0) as u32; - let b = (rgba.b * 255.0) as u32; - let a = (rgba.a * 255.0) as u32; - (r << 24) | (g << 16) | (b << 8) | a - } -} - -struct RgbaVisitor; - -impl Visitor<'_> for RgbaVisitor { - type Value = Rgba; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a string in the format #rrggbb or #rrggbbaa") - } - - fn visit_str(self, value: &str) -> Result { - Rgba::try_from(value).map_err(E::custom) - } -} - -impl JsonSchema for Rgba { - fn schema_name() -> Cow<'static, str> { - "Rgba".into() - } - - fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema { - json_schema!({ - "type": "string", - "pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$" - }) - } -} - -impl<'de> Deserialize<'de> for Rgba { - fn deserialize>(deserializer: D) -> Result { - deserializer.deserialize_str(RgbaVisitor) - } -} - -impl Serialize for Rgba { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let r = (self.r * 255.0).round() as u8; - let g = (self.g * 255.0).round() as u8; - let b = (self.b * 255.0).round() as u8; - let a = (self.a * 255.0).round() as u8; - - let s = format!("#{r:02x}{g:02x}{b:02x}{a:02x}"); - serializer.serialize_str(&s) - } -} - -impl From for Rgba { - fn from(color: Hsla) -> Self { - let h = color.h; - let s = color.s; - let l = color.l; - - let c = (1.0 - (2.0 * l - 1.0).abs()) * s; - let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs()); - let m = l - c / 2.0; - let cm = c + m; - let xm = x + m; - - let (r, g, b) = match (h * 6.0).floor() as i32 { - 0 | 6 => (cm, xm, m), - 1 => (xm, cm, m), - 2 => (m, cm, xm), - 3 => (m, xm, cm), - 4 => (xm, m, cm), - _ => (cm, m, xm), - }; - - Rgba { - r: r.clamp(0., 1.), - g: g.clamp(0., 1.), - b: b.clamp(0., 1.), - a: color.a, - } - } -} - -impl TryFrom<&'_ str> for Rgba { - type Error = anyhow::Error; - - fn try_from(value: &'_ str) -> Result { - const RGB: usize = "rgb".len(); - const RGBA: usize = "rgba".len(); - const RRGGBB: usize = "rrggbb".len(); - const RRGGBBAA: usize = "rrggbbaa".len(); - - const EXPECTED_FORMATS: &str = "Expected #rgb, #rgba, #rrggbb, or #rrggbbaa"; - const INVALID_UNICODE: &str = "invalid unicode characters in color"; - - let Some(("", hex)) = value.trim().split_once('#') else { - bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}"); - }; - - let (r, g, b, a) = match hex.len() { - RGB | RGBA => { - let r = u8::from_str_radix( - hex.get(0..1).with_context(|| { - format!("{INVALID_UNICODE}: r component of #rgb/#rgba for value: '{value}'") - })?, - 16, - )?; - let g = u8::from_str_radix( - hex.get(1..2).with_context(|| { - format!("{INVALID_UNICODE}: g component of #rgb/#rgba for value: '{value}'") - })?, - 16, - )?; - let b = u8::from_str_radix( - hex.get(2..3).with_context(|| { - format!("{INVALID_UNICODE}: b component of #rgb/#rgba for value: '{value}'") - })?, - 16, - )?; - let a = if hex.len() == RGBA { - u8::from_str_radix( - hex.get(3..4).with_context(|| { - format!("{INVALID_UNICODE}: a component of #rgba for value: '{value}'") - })?, - 16, - )? - } else { - 0xf - }; - - /// Duplicates a given hex digit. - /// E.g., `0xf` -> `0xff`. - const fn duplicate(value: u8) -> u8 { - (value << 4) | value - } - - (duplicate(r), duplicate(g), duplicate(b), duplicate(a)) - } - RRGGBB | RRGGBBAA => { - let r = u8::from_str_radix( - hex.get(0..2).with_context(|| { - format!( - "{}: r component of #rrggbb/#rrggbbaa for value: '{}'", - INVALID_UNICODE, value - ) - })?, - 16, - )?; - let g = u8::from_str_radix( - hex.get(2..4).with_context(|| { - format!( - "{INVALID_UNICODE}: g component of #rrggbb/#rrggbbaa for value: '{value}'" - ) - })?, - 16, - )?; - let b = u8::from_str_radix( - hex.get(4..6).with_context(|| { - format!( - "{INVALID_UNICODE}: b component of #rrggbb/#rrggbbaa for value: '{value}'" - ) - })?, - 16, - )?; - let a = if hex.len() == RRGGBBAA { - u8::from_str_radix( - hex.get(6..8).with_context(|| { - format!( - "{INVALID_UNICODE}: a component of #rrggbbaa for value: '{value}'" - ) - })?, - 16, - )? - } else { - 0xff - }; - (r, g, b, a) - } - _ => bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}"), - }; - - Ok(Rgba { - r: r as f32 / 255., - g: g as f32 / 255., - b: b as f32 / 255., - a: a as f32 / 255., - }) - } -} - -/// An HSLA color -#[derive(Default, Copy, Clone, Debug)] -#[repr(C)] -pub struct Hsla { - /// Hue, in a range from 0 to 1 - pub h: f32, - - /// Saturation, in a range from 0 to 1 - pub s: f32, - - /// Lightness, in a range from 0 to 1 - pub l: f32, - - /// Alpha, in a range from 0 to 1 - pub a: f32, -} - -#[cfg(feature = "proptest")] -mod property { - use super::Hsla; - use proptest::prelude::*; - - impl Hsla { - /// Proptest [`Strategy`] that produces opaque colors (i.e. alpha = 1). - /// - /// For truly arbitrary colors, use the [`Arbitrary`] implementation. - pub fn opaque_strategy() -> impl Strategy { - (0.0f32..=1.0, 0.0f32..=1.0, 0.0f32..=1.0).prop_map(|(h, s, l)| Hsla { h, s, l, a: 1. }) - } - } - - impl Arbitrary for Hsla { - type Strategy = BoxedStrategy; - type Parameters = (); - - fn arbitrary_with((): Self::Parameters) -> Self::Strategy { - (0.0f32..=1.0, 0.0f32..=1.0, 0.0f32..=1.0, 0.0f32..=1.0) - .prop_map(|(h, s, l, a)| Hsla { h, s, l, a }) - .boxed() - } - } -} - -impl PartialEq for Hsla { - fn eq(&self, other: &Self) -> bool { - self.h - .total_cmp(&other.h) - .then(self.s.total_cmp(&other.s)) - .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a))) - .is_eq() - } -} - -impl PartialOrd for Hsla { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Hsla { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.h - .total_cmp(&other.h) - .then(self.s.total_cmp(&other.s)) - .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a))) - } -} - -impl Eq for Hsla {} - -impl Hash for Hsla { - fn hash(&self, state: &mut H) { - state.write_u32(u32::from_be_bytes(self.h.to_be_bytes())); - state.write_u32(u32::from_be_bytes(self.s.to_be_bytes())); - state.write_u32(u32::from_be_bytes(self.l.to_be_bytes())); - state.write_u32(u32::from_be_bytes(self.a.to_be_bytes())); - } -} - -impl Display for Hsla { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "hsla({:.2}, {:.2}%, {:.2}%, {:.2})", - self.h * 360., - self.s * 100., - self.l * 100., - self.a - ) - } -} - -/// Construct an [`Hsla`] object from plain values -pub const fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla { - Hsla { - h: h.clamp(0., 1.), - s: s.clamp(0., 1.), - l: l.clamp(0., 1.), - a: a.clamp(0., 1.), - } -} - -/// Pure black in [`Hsla`] -pub const fn black() -> Hsla { - Hsla { - h: 0., - s: 0., - l: 0., - a: 1., - } -} - -/// Transparent black in [`Hsla`] -pub const fn transparent_black() -> Hsla { - Hsla { - h: 0., - s: 0., - l: 0., - a: 0., - } -} - -/// Transparent white in [`Hsla`] -pub const fn transparent_white() -> Hsla { - Hsla { - h: 0., - s: 0., - l: 1., - a: 0., - } -} - -/// Opaque grey in [`Hsla`], values will be clamped to the range [0, 1] -pub const fn opaque_grey(lightness: f32, opacity: f32) -> Hsla { - Hsla { - h: 0., - s: 0., - l: lightness.clamp(0., 1.), - a: opacity.clamp(0., 1.), - } -} - -/// Pure white in [`Hsla`] -pub const fn white() -> Hsla { - Hsla { - h: 0., - s: 0., - l: 1., - a: 1., - } -} - -/// The color red in [`Hsla`] -pub const fn red() -> Hsla { - Hsla { - h: 0., - s: 1., - l: 0.5, - a: 1., - } -} - -/// The color blue in [`Hsla`] -pub const fn blue() -> Hsla { - Hsla { - h: 0.6666666667, - s: 1., - l: 0.5, - a: 1., - } -} - -/// The color green in [`Hsla`] -pub const fn green() -> Hsla { - Hsla { - h: 0.3333333333, - s: 1., - l: 0.25, - a: 1., - } -} - -/// The color yellow in [`Hsla`] -pub const fn yellow() -> Hsla { - Hsla { - h: 0.1666666667, - s: 1., - l: 0.5, - a: 1., - } -} - -impl Hsla { - /// Converts this HSLA color to an RGBA color. - pub fn to_rgb(self) -> Rgba { - self.into() - } - - /// The color red - pub const fn red() -> Self { - red() - } - - /// The color green - pub const fn green() -> Self { - green() - } - - /// The color blue - pub const fn blue() -> Self { - blue() - } - - /// The color black - pub const fn black() -> Self { - black() - } - - /// The color white - pub const fn white() -> Self { - white() - } - - /// The color transparent black - pub const fn transparent_black() -> Self { - transparent_black() - } - - /// Returns true if the HSLA color is fully transparent, false otherwise. - pub fn is_transparent(&self) -> bool { - self.a == 0.0 - } - - /// Returns true if the HSLA color is fully opaque, false otherwise. - pub fn is_opaque(&self) -> bool { - self.a == 1.0 - } - - /// Blends `other` on top of `self` based on `other`'s alpha value. The resulting color is a combination of `self`'s and `other`'s colors. - /// - /// If `other`'s alpha value is 1.0 or greater, `other` color is fully opaque, thus `other` is returned as the output color. - /// If `other`'s alpha value is 0.0 or less, `other` color is fully transparent, thus `self` is returned as the output color. - /// Else, the output color is calculated as a blend of `self` and `other` based on their weighted alpha values. - /// - /// Assumptions: - /// - Alpha values are contained in the range [0, 1], with 1 as fully opaque and 0 as fully transparent. - /// - The relative contributions of `self` and `other` is based on `self`'s alpha value (`self.a`) and `other`'s alpha value (`other.a`), `self` contributing `self.a * (1.0 - other.a)` and `other` contributing its own alpha value. - /// - RGB color components are contained in the range [0, 1]. - /// - If `self` and `other` colors are out of the valid range, the blend operation's output and behavior is undefined. - pub fn blend(self, other: Hsla) -> Hsla { - let alpha = other.a; - - if alpha >= 1.0 { - other - } else if alpha <= 0.0 { - self - } else { - let converted_self = Rgba::from(self); - let converted_other = Rgba::from(other); - let blended_rgb = converted_self.blend(converted_other); - Hsla::from(blended_rgb) - } - } - - /// Returns a new HSLA color with the same hue, and lightness, but with no saturation. - pub fn grayscale(&self) -> Self { - Hsla { - h: self.h, - s: 0., - l: self.l, - a: self.a, - } - } - - /// Fade out the color by a given factor. This factor should be between 0.0 and 1.0. - /// Where 0.0 will leave the color unchanged, and 1.0 will completely fade out the color. - pub fn fade_out(&mut self, factor: f32) { - self.a *= 1.0 - factor.clamp(0., 1.); - } - - /// Multiplies the alpha value of the color by a given factor - /// and returns a new HSLA color. - /// - /// Useful for transforming colors with dynamic opacity, - /// like a color from an external source. - /// - /// Example: - /// ``` - /// let color = gpui::red(); - /// let faded_color = color.opacity(0.5); - /// assert_eq!(faded_color.a, 0.5); - /// ``` - /// - /// This will return a red color with half the opacity. - /// - /// Example: - /// ``` - /// use gpui::hsla; - /// let color = hsla(0.7, 1.0, 0.5, 0.7); // A saturated blue - /// let faded_color = color.opacity(0.16); - /// assert!((faded_color.a - 0.112).abs() < 1e-6); - /// ``` - /// - /// This will return a blue color with around ~10% opacity, - /// suitable for an element's hover or selected state. - /// - pub fn opacity(&self, factor: f32) -> Self { - Hsla { - h: self.h, - s: self.s, - l: self.l, - a: self.a * factor.clamp(0., 1.), - } - } - - /// Returns a new HSLA color with the same hue, saturation, - /// and lightness, but with a new alpha value. - /// - /// Example: - /// ``` - /// let color = gpui::red(); - /// let red_color = color.alpha(0.25); - /// assert_eq!(red_color.a, 0.25); - /// ``` - /// - /// This will return a red color with 25% opacity. - /// - /// Example: - /// ``` - /// use gpui::hsla; - /// let color = hsla(0.7, 1.0, 0.5, 0.7); // A saturated blue - /// let faded_color = color.alpha(0.25); - /// assert_eq!(faded_color.a, 0.25); - /// ``` - /// - /// This will return a blue color with 25% opacity. - pub fn alpha(&self, a: f32) -> Self { - Hsla { - h: self.h, - s: self.s, - l: self.l, - a: a.clamp(0., 1.), - } - } -} - -impl From for Hsla { - fn from(color: Rgba) -> Self { - let r = color.r; - let g = color.g; - let b = color.b; - - let max = r.max(g.max(b)); - let min = r.min(g.min(b)); - let delta = max - min; - - let l = (max + min) / 2.0; - let s = if l == 0.0 || l == 1.0 { - 0.0 - } else if l < 0.5 { - delta / (2.0 * l) - } else { - delta / (2.0 - 2.0 * l) - }; - - let h = if delta == 0.0 { - 0.0 - } else if max == r { - ((g - b) / delta).rem_euclid(6.0) / 6.0 - } else if max == g { - ((b - r) / delta + 2.0) / 6.0 - } else { - ((r - g) / delta + 4.0) / 6.0 - }; - - Hsla { - h, - s, - l, - a: color.a, - } - } -} - -impl JsonSchema for Hsla { - fn schema_name() -> Cow<'static, str> { - Rgba::schema_name() - } - - fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { - Rgba::json_schema(generator) - } -} - -impl Serialize for Hsla { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - Rgba::from(*self).serialize(serializer) - } -} - -impl<'de> Deserialize<'de> for Hsla { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - Ok(Rgba::deserialize(deserializer)?.into()) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)] -#[repr(C)] -pub(crate) enum BackgroundTag { - Solid = 0, - LinearGradient = 1, - PatternSlash = 2, - Checkerboard = 3, -} - -/// A color space for color interpolation. -/// -/// References: -/// - -/// - -#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize, JsonSchema)] -#[repr(C)] -pub enum ColorSpace { - #[default] - /// The sRGB color space. - Srgb = 0, - /// The Oklab color space. - Oklab = 1, -} - -impl Display for ColorSpace { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self { - ColorSpace::Srgb => write!(f, "sRGB"), - ColorSpace::Oklab => write!(f, "Oklab"), - } - } -} - -/// A background color, which can be either a solid color or a linear gradient. -#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)] -#[repr(C)] -pub struct Background { - pub(crate) tag: BackgroundTag, - pub(crate) color_space: ColorSpace, - pub(crate) solid: Hsla, - pub(crate) gradient_angle_or_pattern_height: f32, - pub(crate) colors: [LinearColorStop; 2], - /// Padding for alignment for repr(C) layout. - pad: u32, -} - -impl std::fmt::Debug for Background { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self.tag { - BackgroundTag::Solid => write!(f, "Solid({:?})", self.solid), - BackgroundTag::LinearGradient => write!( - f, - "LinearGradient({}, {:?}, {:?})", - self.gradient_angle_or_pattern_height, self.colors[0], self.colors[1] - ), - BackgroundTag::PatternSlash => write!( - f, - "PatternSlash({:?}, {})", - self.solid, self.gradient_angle_or_pattern_height - ), - BackgroundTag::Checkerboard => write!( - f, - "Checkerboard({:?}, {})", - self.solid, self.gradient_angle_or_pattern_height - ), - } - } -} - -impl Eq for Background {} -impl Default for Background { - fn default() -> Self { - Self { - tag: BackgroundTag::Solid, - solid: Hsla::default(), - color_space: ColorSpace::default(), - gradient_angle_or_pattern_height: 0.0, - colors: [LinearColorStop::default(), LinearColorStop::default()], - pad: 0, - } - } -} - -/// Creates a hash pattern background -pub fn pattern_slash(color: impl Into, width: f32, interval: f32) -> Background { - let width_scaled = (width * 255.0) as u32; - let interval_scaled = (interval * 255.0) as u32; - let height = ((width_scaled * 0xFFFF) + interval_scaled) as f32; - - Background { - tag: BackgroundTag::PatternSlash, - solid: color.into(), - gradient_angle_or_pattern_height: height, - ..Default::default() - } -} - -/// Creates a checkerboard pattern background -pub fn checkerboard(color: impl Into, size: f32) -> Background { - Background { - tag: BackgroundTag::Checkerboard, - solid: color.into(), - gradient_angle_or_pattern_height: size, - ..Default::default() - } -} - -/// Creates a solid background color. -pub fn solid_background(color: impl Into) -> Background { - Background { - solid: color.into(), - ..Default::default() - } -} - -/// Creates a LinearGradient background color. -/// -/// The gradient line's angle of direction. A value of `0.` is equivalent to top; increasing values rotate clockwise from there. -/// -/// The `angle` is in degrees value in the range 0.0 to 360.0. -/// -/// -pub fn linear_gradient( - angle: f32, - from: impl Into, - to: impl Into, -) -> Background { - Background { - tag: BackgroundTag::LinearGradient, - gradient_angle_or_pattern_height: angle, - colors: [from.into(), to.into()], - ..Default::default() - } -} - -/// A color stop in a linear gradient. -/// -/// -#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, JsonSchema)] -#[repr(C)] -pub struct LinearColorStop { - /// The color of the color stop. - pub color: Hsla, - /// The percentage of the gradient, in the range 0.0 to 1.0. - pub percentage: f32, -} - -/// Creates a new linear color stop. -/// -/// The percentage of the gradient, in the range 0.0 to 1.0. -pub fn linear_color_stop(color: impl Into, percentage: f32) -> LinearColorStop { - LinearColorStop { - color: color.into(), - percentage, - } -} - -impl LinearColorStop { - /// Returns a new color stop with the same color, but with a modified alpha value. - pub fn opacity(&self, factor: f32) -> Self { - Self { - percentage: self.percentage, - color: self.color.opacity(factor), - } - } -} - -impl Background { - /// Returns the solid color if this is a solid background, None otherwise. - pub fn as_solid(&self) -> Option { - if self.tag == BackgroundTag::Solid { - Some(self.solid) - } else { - None - } - } - - /// Use specified color space for color interpolation. - /// - /// - pub fn color_space(mut self, color_space: ColorSpace) -> Self { - self.color_space = color_space; - self - } - - /// Returns a new background color with the same hue, saturation, and lightness, but with a modified alpha value. - pub fn opacity(&self, factor: f32) -> Self { - let mut background = *self; - background.solid = background.solid.opacity(factor); - background.colors = [ - self.colors[0].opacity(factor), - self.colors[1].opacity(factor), - ]; - background - } - - /// Returns whether the background color is transparent. - pub fn is_transparent(&self) -> bool { - match self.tag { - BackgroundTag::Solid => self.solid.is_transparent(), - BackgroundTag::LinearGradient => self.colors.iter().all(|c| c.color.is_transparent()), - BackgroundTag::PatternSlash => self.solid.is_transparent(), - BackgroundTag::Checkerboard => self.solid.is_transparent(), - } - } -} - -impl From for Background { - fn from(value: Hsla) -> Self { - Background { - tag: BackgroundTag::Solid, - solid: value, - ..Default::default() - } - } -} - -impl From for Background { - fn from(value: Rgba) -> Self { - Background { - tag: BackgroundTag::Solid, - solid: Hsla::from(value), - ..Default::default() - } - } -} - -#[cfg(test)] -mod tests { - use serde_json::json; - - use super::*; - - #[test] - fn test_deserialize_three_value_hex_to_rgba() { - let actual: Rgba = serde_json::from_value(json!("#f09")).unwrap(); - - assert_eq!(actual, rgba(0xff0099ff)) - } - - #[test] - fn test_deserialize_four_value_hex_to_rgba() { - let actual: Rgba = serde_json::from_value(json!("#f09f")).unwrap(); - - assert_eq!(actual, rgba(0xff0099ff)) - } - - #[test] - fn test_deserialize_six_value_hex_to_rgba() { - let actual: Rgba = serde_json::from_value(json!("#ff0099")).unwrap(); - - assert_eq!(actual, rgba(0xff0099ff)) - } - - #[test] - fn test_deserialize_eight_value_hex_to_rgba() { - let actual: Rgba = serde_json::from_value(json!("#ff0099ff")).unwrap(); - - assert_eq!(actual, rgba(0xff0099ff)) - } - - #[test] - fn test_deserialize_eight_value_hex_with_padding_to_rgba() { - let actual: Rgba = serde_json::from_value(json!(" #f5f5f5ff ")).unwrap(); - - assert_eq!(actual, rgba(0xf5f5f5ff)) - } - - #[test] - fn test_deserialize_eight_value_hex_with_mixed_case_to_rgba() { - let actual: Rgba = serde_json::from_value(json!("#DeAdbEeF")).unwrap(); - - assert_eq!(actual, rgba(0xdeadbeef)) - } - - #[test] - fn test_background_solid() { - let color = Hsla::from(rgba(0xff0099ff)); - let mut background = Background::from(color); - assert_eq!(background.tag, BackgroundTag::Solid); - assert_eq!(background.solid, color); - - assert_eq!(background.opacity(0.5).solid, color.opacity(0.5)); - assert!(!background.is_transparent()); - background.solid = hsla(0.0, 0.0, 0.0, 0.0); - assert!(background.is_transparent()); - } - - #[test] - fn test_background_linear_gradient() { - let from = linear_color_stop(rgba(0xff0099ff), 0.0); - let to = linear_color_stop(rgba(0x00ff99ff), 1.0); - let background = linear_gradient(90.0, from, to); - assert_eq!(background.tag, BackgroundTag::LinearGradient); - assert_eq!(background.colors[0], from); - assert_eq!(background.colors[1], to); - - assert_eq!(background.opacity(0.5).colors[0], from.opacity(0.5)); - assert_eq!(background.opacity(0.5).colors[1], to.opacity(0.5)); - assert!(!background.is_transparent()); - assert!(background.opacity(0.0).is_transparent()); - } - - #[test] - fn test_rgba_alpha() { - let color = Rgba { - r: 0.2, - g: 0.6, - b: 1.0, - a: 0.8, - }; - - assert_eq!(color.alpha(0.25).a, 0.25); - assert_eq!(color.alpha(1.5).a, 1.0); - } - - #[test] - fn test_rgba_opacity() { - let color = Rgba { - r: 0.2, - g: 0.6, - b: 1.0, - a: 0.8, - }; - assert!((color.opacity(0.5).a - 0.4).abs() < 1e-6); - assert_eq!(color.opacity(2.0).a, 0.8); - } -} diff --git a/crates/gpui_pre/src/colors.rs b/crates/gpui_pre/src/colors.rs deleted file mode 100644 index ef11ef5..0000000 --- a/crates/gpui_pre/src/colors.rs +++ /dev/null @@ -1,122 +0,0 @@ -use crate::{App, Global, Rgba, Window, WindowAppearance, rgb}; -use std::ops::Deref; -use std::sync::Arc; - -/// The default set of colors for gpui. -/// -/// These are used for styling base components, examples and more. -#[derive(Clone, Debug)] -pub struct Colors { - /// Text color - pub text: Rgba, - /// Selected text color - pub selected_text: Rgba, - /// Background color - pub background: Rgba, - /// Disabled color - pub disabled: Rgba, - /// Selected color - pub selected: Rgba, - /// Border color - pub border: Rgba, - /// Separator color - pub separator: Rgba, - /// Container color - pub container: Rgba, -} - -impl Default for Colors { - fn default() -> Self { - Self::light() - } -} - -impl Colors { - /// Returns the default colors for the given window appearance. - pub fn for_appearance(window: &Window) -> Self { - match window.appearance() { - WindowAppearance::Light | WindowAppearance::VibrantLight => Self::light(), - WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::dark(), - } - } - - /// Returns the default dark colors. - pub fn dark() -> Self { - Self { - text: rgb(0xffffff), - selected_text: rgb(0xffffff), - disabled: rgb(0x565656), - selected: rgb(0x2457ca), - background: rgb(0x222222), - border: rgb(0x000000), - separator: rgb(0xd9d9d9), - container: rgb(0x262626), - } - } - - /// Returns the default light colors. - pub fn light() -> Self { - Self { - text: rgb(0x252525), - selected_text: rgb(0xffffff), - background: rgb(0xffffff), - disabled: rgb(0xb0b0b0), - selected: rgb(0x2a63d9), - border: rgb(0xd9d9d9), - separator: rgb(0xe6e6e6), - container: rgb(0xf4f5f5), - } - } - - /// Get [Colors] from the global state - pub fn get_global(cx: &App) -> &Arc { - &cx.global::().0 - } -} - -/// Get [Colors] from the global state -#[derive(Clone, Debug)] -pub struct GlobalColors(pub Arc); - -impl Deref for GlobalColors { - type Target = Arc; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Global for GlobalColors {} - -/// Implement this trait to allow global [Colors] access via `cx.default_colors()`. -pub trait DefaultColors { - /// Returns the default [`Colors`] - fn default_colors(&self) -> &Arc; -} - -impl DefaultColors for App { - fn default_colors(&self) -> &Arc { - &self.global::().0 - } -} - -/// The appearance of the base GPUI colors, used to style GPUI elements -/// -/// Varies based on the system's current [`WindowAppearance`]. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub enum DefaultAppearance { - /// Use the set of colors for light appearances. - #[default] - Light, - /// Use the set of colors for dark appearances. - Dark, -} - -impl From for DefaultAppearance { - fn from(appearance: WindowAppearance) -> Self { - match appearance { - WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light, - WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark, - } - } -} diff --git a/crates/gpui_pre/src/debug_overlay.rs b/crates/gpui_pre/src/debug_overlay.rs deleted file mode 100644 index 9157bcf..0000000 --- a/crates/gpui_pre/src/debug_overlay.rs +++ /dev/null @@ -1,429 +0,0 @@ -//! A developer overlay that paints frame-time statistics directly into the -//! scene, bypassing layout, text, and view invalidation entirely (to avoid -//! infinitely triggering new frames). - -use crate::{ - BorderStyle, Bounds, ContentMask, Corners, Edges, Hsla, Pixels, Quad, ScaledPixels, Scene, - Size, point, rgba, size, transparent_black, -}; -use std::{collections::VecDeque, time::Duration}; - -#[allow(missing_docs)] -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -pub enum DebugFrameOverlayMode { - #[default] - Hidden, - Minimal, - Full, -} - -impl DebugFrameOverlayMode { - /// Returns the next mode in the Hidden → FrameTime → Detailed cycle. - pub fn next(self) -> Self { - match self { - Self::Hidden => Self::Minimal, - Self::Minimal => Self::Full, - Self::Full => Self::Hidden, - } - } -} - -/// The number of most recent draw durations retained for percentile statistics. -const MAX_SAMPLES: usize = 1000; - -const GLYPH_WIDTH: usize = 5; -const GLYPH_HEIGHT: usize = 7; -/// Glyph advance and line advance, in font cells. -const CHAR_ADVANCE: f32 = (GLYPH_WIDTH + 1) as f32; -const LINE_ADVANCE: f32 = (GLYPH_HEIGHT + 2) as f32; -/// Padding between the panel edge and the text, in font cells. -const PANEL_PADDING: f32 = 2.0; -/// Margin between the panel and the window corner, in font cells. -const PANEL_MARGIN: f32 = 4.0; -/// Side of one square font cell, in logical pixels. -const CELL_SIZE: f32 = 2.0; - -fn text_color() -> Hsla { - rgba(0x33ff33ff).into() -} - -fn panel_color() -> Hsla { - rgba(0x000000aa).into() -} - -pub(crate) struct DebugFrameOverlay { - mode: DebugFrameOverlayMode, - draw_durations: VecDeque, - total_frame_count: u64, -} - -impl DebugFrameOverlay { - pub(crate) fn new() -> Self { - Self { - mode: DebugFrameOverlayMode::default(), - draw_durations: VecDeque::new(), - total_frame_count: 0, - } - } - - pub(crate) fn mode(&self) -> DebugFrameOverlayMode { - self.mode - } - - pub(crate) fn set_mode(&mut self, mode: DebugFrameOverlayMode) { - self.mode = mode; - } - - /// Clears the draw-duration samples, restarting the percentile window. - /// The total frame count is left untouched. - pub(crate) fn reset_stats(&mut self) { - self.draw_durations.clear(); - } - - pub(crate) fn is_enabled(&self) -> bool { - self.mode != DebugFrameOverlayMode::Hidden - } - - pub(crate) fn record_frame(&mut self, draw_duration: Duration) { - self.total_frame_count += 1; - if self.draw_durations.len() >= MAX_SAMPLES { - self.draw_durations.pop_front(); - } - self.draw_durations.push_back(draw_duration); - } - - pub(crate) fn paint(&self, scene: &mut Scene, viewport_size: Size, scale_factor: f32) { - if !self.is_enabled() { - return; - } - - let lines = self.lines(); - let max_line_chars = lines.iter().map(|line| line.len()).max().unwrap_or(0); - // Ensure at least one physical pixel per cell so the text stays legible - // at fractional downscale factors. - let cell = (CELL_SIZE * scale_factor).max(1.0); - - let panel_width = cell * (max_line_chars as f32 * CHAR_ADVANCE + 2.0 * PANEL_PADDING); - let panel_height = cell * (lines.len() as f32 * LINE_ADVANCE + 2.0 * PANEL_PADDING); - let viewport = viewport_size.scale(scale_factor); - let panel_left = viewport.width.0 - panel_width - cell * PANEL_MARGIN; - let panel_top = cell * PANEL_MARGIN; - - let content_mask = ContentMask { - bounds: Bounds { - origin: point(ScaledPixels(0.), ScaledPixels(0.)), - size: viewport, - }, - ..Default::default() - }; - - scene.insert_primitive(solid_quad( - scaled_bounds(panel_left, panel_top, panel_width, panel_height), - &content_mask, - panel_color(), - )); - - let text_color = text_color(); - for (line_index, line) in lines.iter().enumerate() { - let line_top = panel_top + cell * (PANEL_PADDING + line_index as f32 * LINE_ADVANCE); - for (char_index, character) in line.chars().enumerate() { - let Some(rows) = glyph(character) else { - continue; - }; - let glyph_left = - panel_left + cell * (PANEL_PADDING + char_index as f32 * CHAR_ADVANCE); - for (row_index, row) in rows.iter().enumerate() { - let row_top = line_top + cell * row_index as f32; - // Merge horizontal runs of lit cells into single quads. - let mut column = 0; - while column < GLYPH_WIDTH { - if row & (1 << (GLYPH_WIDTH - 1 - column)) == 0 { - column += 1; - continue; - } - let run_start = column; - while column < GLYPH_WIDTH && row & (1 << (GLYPH_WIDTH - 1 - column)) != 0 { - column += 1; - } - scene.insert_primitive(solid_quad( - scaled_bounds( - glyph_left + cell * run_start as f32, - row_top, - cell * (column - run_start) as f32, - cell, - ), - &content_mask, - text_color, - )); - } - } - } - } - } - - fn lines(&self) -> Vec { - let current = self.draw_durations.back().copied(); - match self.mode { - DebugFrameOverlayMode::Hidden => Vec::new(), - DebugFrameOverlayMode::Minimal => vec![format_ms(current)], - DebugFrameOverlayMode::Full => { - let mut sorted: Vec = self.draw_durations.iter().copied().collect(); - sorted.sort_unstable(); - let percentile = |numerator: usize| { - (!sorted.is_empty()).then(|| sorted[(sorted.len() - 1) * numerator / 100]) - }; - // Past five digits the count would break the column - // alignment, so it saturates instead. - let frame_count = if self.total_frame_count > 99_999 { - "LOTS".to_string() - } else { - self.total_frame_count.to_string() - }; - // Labels are padded to a uniform width so the fixed-width - // durations start in the same column on every line. - vec![ - format!("CUR {}", format_ms(current)), - format!("1% {}", format_ms(percentile(99))), - format!("10% {}", format_ms(percentile(90))), - format!("MAX {}", format_ms(sorted.last().copied())), - format!("FRAMES {frame_count:>5}"), - ] - } - } - } -} - -/// Formats as `abc.d MS`, right-aligned in room for three integer digits and -/// one decimal (padded with spaces, not zeroes), so stacked readouts align. -fn format_ms(duration: Option) -> String { - match duration { - Some(duration) => { - let ms = duration.as_secs_f32() * 1000.0; - format!("{ms:>5.1} MS") - } - None => " -- MS".into(), - } -} - -fn scaled_bounds(left: f32, top: f32, width: f32, height: f32) -> Bounds { - Bounds { - origin: point(ScaledPixels(left), ScaledPixels(top)), - size: size(ScaledPixels(width), ScaledPixels(height)), - } -} - -fn solid_quad( - bounds: Bounds, - content_mask: &ContentMask, - color: Hsla, -) -> Quad { - Quad { - order: 0, - border_style: BorderStyle::Solid, - bounds, - content_mask: *content_mask, - background: color.into(), - border_color: transparent_black(), - corner_radii: Corners::default(), - border_widths: Edges::default(), - } -} - -/// Returns the 5x7 bitmap for the given character, one `u8` of column bits per -/// row with the most significant of the 5 bits leftmost. Only the characters -/// used by the overlay's readouts are defined. -fn glyph(character: char) -> Option<[u8; GLYPH_HEIGHT]> { - Some(match character { - '0' => [ - 0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110, - ], - '1' => [ - 0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110, - ], - '2' => [ - 0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b01000, 0b11111, - ], - '3' => [ - 0b11111, 0b00010, 0b00100, 0b00010, 0b00001, 0b10001, 0b01110, - ], - '4' => [ - 0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010, - ], - '5' => [ - 0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110, - ], - '6' => [ - 0b00110, 0b01000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110, - ], - '7' => [ - 0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000, - ], - '8' => [ - 0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110, - ], - '9' => [ - 0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b01100, - ], - '.' => [ - 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b01100, 0b01100, - ], - '-' => [ - 0b00000, 0b00000, 0b00000, 0b11111, 0b00000, 0b00000, 0b00000, - ], - '%' => [ - 0b11001, 0b11001, 0b00010, 0b00100, 0b01000, 0b10011, 0b10011, - ], - 'A' => [ - 0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001, - ], - 'C' => [ - 0b01110, 0b10001, 0b10000, 0b10000, 0b10000, 0b10001, 0b01110, - ], - 'E' => [ - 0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111, - ], - 'F' => [ - 0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000, - ], - 'L' => [ - 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111, - ], - 'M' => [ - 0b10001, 0b11011, 0b10101, 0b10101, 0b10001, 0b10001, 0b10001, - ], - 'N' => [ - 0b10001, 0b11001, 0b10101, 0b10011, 0b10001, 0b10001, 0b10001, - ], - 'O' => [ - 0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110, - ], - 'R' => [ - 0b11110, 0b10001, 0b10001, 0b11110, 0b10100, 0b10010, 0b10001, - ], - 'S' => [ - 0b01111, 0b10000, 0b10000, 0b01110, 0b00001, 0b00001, 0b11110, - ], - 'T' => [ - 0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, - ], - 'U' => [ - 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110, - ], - 'X' => [ - 0b10001, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001, 0b10001, - ], - _ => return None, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Every character the readouts can produce must have a glyph, or it - /// would silently render as blank space. - #[test] - fn all_rendered_characters_have_glyphs() { - let mut overlay = DebugFrameOverlay::new(); - overlay.set_mode(DebugFrameOverlayMode::Full); - - let mut lines = Vec::new(); - for duration in [ - Duration::ZERO, - Duration::from_micros(1), - Duration::from_micros(8_333), - Duration::from_millis(123), - Duration::from_secs(2), - ] { - overlay.record_frame(duration); - lines.extend(overlay.lines()); - } - // Counts beyond five digits render as "LOTS". - overlay.total_frame_count = 100_000; - lines.extend(overlay.lines()); - - // An enabled overlay with no samples yet renders placeholders. - let mut empty = DebugFrameOverlay::new(); - empty.set_mode(DebugFrameOverlayMode::Full); - lines.extend(empty.lines()); - - for line in lines { - for character in line.chars() { - assert!( - character == ' ' || glyph(character).is_some(), - "no glyph for {character:?} in line {line:?}" - ); - } - } - } - - #[test] - fn percentile_lows_are_reported_as_times() { - let mut overlay = DebugFrameOverlay::new(); - overlay.set_mode(DebugFrameOverlayMode::Full); - for milliseconds in 1..=100 { - overlay.record_frame(Duration::from_millis(milliseconds)); - } - let lines = overlay.lines(); - assert_eq!(lines[0], "CUR 100.0 MS"); - assert_eq!(lines[1], "1% 99.0 MS"); - assert_eq!(lines[2], "10% 90.0 MS"); - assert_eq!(lines[3], "MAX 100.0 MS"); - assert_eq!(lines[4], "FRAMES 100"); - } - - #[test] - fn reset_clears_durations_but_keeps_frame_count() { - let mut overlay = DebugFrameOverlay::new(); - overlay.set_mode(DebugFrameOverlayMode::Full); - for _ in 0..10 { - overlay.record_frame(Duration::from_millis(10)); - } - overlay.reset_stats(); - let lines = overlay.lines(); - assert_eq!(lines[0], "CUR -- MS"); - assert_eq!(lines[3], "MAX -- MS"); - assert_eq!(lines[4], "FRAMES 10"); - overlay.record_frame(Duration::from_millis(20)); - let lines = overlay.lines(); - assert_eq!(lines[0], "CUR 20.0 MS"); - assert_eq!(lines[4], "FRAMES 11"); - } - - #[test] - fn frame_count_accumulates_across_mode_changes() { - let mut overlay = DebugFrameOverlay::new(); - for _ in 0..3 { - overlay.record_frame(Duration::from_millis(10)); - } - overlay.set_mode(DebugFrameOverlayMode::Minimal); - assert_eq!(overlay.lines(), vec![" 10.0 MS".to_string()]); - overlay.set_mode(DebugFrameOverlayMode::Full); - overlay.record_frame(Duration::from_millis(10)); - assert_eq!(overlay.lines()[4], "FRAMES 4"); - overlay.set_mode(DebugFrameOverlayMode::Hidden); - overlay.record_frame(Duration::from_millis(10)); - overlay.set_mode(DebugFrameOverlayMode::Full); - assert_eq!(overlay.lines()[4], "FRAMES 5"); - } - - #[test] - fn frame_count_is_right_aligned_and_saturates() { - let mut overlay = DebugFrameOverlay::new(); - overlay.set_mode(DebugFrameOverlayMode::Full); - overlay.record_frame(Duration::from_millis(10)); - assert_eq!(overlay.lines()[4], "FRAMES 1"); - overlay.total_frame_count = 99_999; - assert_eq!(overlay.lines()[4], "FRAMES 99999"); - overlay.total_frame_count = 100_000; - assert_eq!(overlay.lines()[4], "FRAMES LOTS"); - } - - #[test] - fn toggling_on_shows_previous_frame_immediately() { - let mut overlay = DebugFrameOverlay::new(); - overlay.record_frame(Duration::from_millis(10)); - overlay.set_mode(DebugFrameOverlayMode::Minimal); - assert_eq!(overlay.lines(), vec![" 10.0 MS".to_string()]); - } -} diff --git a/crates/gpui_pre/src/element.rs b/crates/gpui_pre/src/element.rs deleted file mode 100644 index c817492..0000000 --- a/crates/gpui_pre/src/element.rs +++ /dev/null @@ -1,792 +0,0 @@ -//! Elements are the workhorses of GPUI. They are responsible for laying out and painting all of -//! the contents of a window. Elements form a tree and are laid out according to the web layout -//! standards as implemented by [taffy](https://github.com/DioxusLabs/taffy). Most of the time, -//! you won't need to interact with this module or these APIs directly. Elements provide their -//! own APIs and GPUI, or other element implementation, uses the APIs in this module to convert -//! that element tree into the pixels you see on the screen. -//! -//! # Element Basics -//! -//! Elements are constructed by calling [`Render::render()`] on the root view of the window, -//! which recursively constructs the element tree from the current state of the application,. -//! These elements are then laid out by Taffy, and painted to the screen according to their own -//! implementation of [`Element::paint()`]. Before the start of the next frame, the entire element -//! tree and any callbacks they have registered with GPUI are dropped and the process repeats. -//! -//! But some state is too simple and voluminous to store in every view that needs it, e.g. -//! whether a hover has been started or not. For this, GPUI provides the [`Element::PrepaintState`], associated type. -//! -//! # Implementing your own elements -//! -//! Elements are intended to be the low level, imperative API to GPUI. They are responsible for upholding, -//! or breaking, GPUI's features as they deem necessary. As an example, most GPUI elements are expected -//! to stay in the bounds that their parent element gives them. But with [`Window::with_content_mask`], -//! you can ignore this restriction and paint anywhere inside of the window's bounds. This is useful for overlays -//! and popups and anything else that shows up 'on top' of other elements. -//! With great power, comes great responsibility. -//! -//! However, most of the time, you won't need to implement your own elements. GPUI provides a number of -//! elements that should cover most common use cases out of the box and it's recommended that you use those -//! to construct `components`, using the [`RenderOnce`] trait and the `#[derive(IntoElement)]` macro. Only implement -//! elements when you need to take manual control of the layout and painting process, such as when using -//! your own custom layout algorithm or rendering a code editor. - -use crate::{ - A11ySubtreeBuilder, App, ArenaBox, AvailableSpace, Bounds, Context, DispatchNodeId, ElementId, - FocusHandle, InspectorElementId, LayoutId, Pixels, Point, Size, Style, Window, - util::FluentBuilder, window::with_element_arena, -}; -use derive_more::{Deref, DerefMut}; -use std::{ - any::Any, - fmt::{self, Debug, Display}, - mem, panic, - sync::Arc, -}; - -/// Implemented by types that participate in laying out and painting the contents of a window. -/// Elements form a tree and are laid out according to web-based layout rules, as implemented by Taffy. -/// You can create custom elements by implementing this trait, see the module-level documentation -/// for more details. -pub trait Element: 'static + IntoElement { - /// The type of state returned from [`Element::request_layout`]. A mutable reference to this state is subsequently - /// provided to [`Element::prepaint`] and [`Element::paint`]. - type RequestLayoutState: 'static; - - /// The type of state returned from [`Element::prepaint`]. A mutable reference to this state is subsequently - /// provided to [`Element::paint`]. - type PrepaintState: 'static; - - /// If this element has a unique identifier, return it here. This is used to track elements across frames, and - /// will cause a GlobalElementId to be passed to the request_layout, prepaint, and paint methods. - /// - /// The global id can in turn be used to access state that's connected to an element with the same id across - /// frames. This id must be unique among children of the first containing element with an id. - fn id(&self) -> Option; - - /// Source location where this element was constructed, used to disambiguate elements in the - /// inspector and navigate to their source code. - fn source_location(&self) -> Option<&'static panic::Location<'static>>; - - /// Before an element can be painted, we need to know where it's going to be and how big it is. - /// Use this method to request a layout from Taffy and initialize the element's state. - fn request_layout( - &mut self, - id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState); - - /// After laying out an element, we need to commit its bounds to the current frame for hitbox - /// purposes. The state argument is the same state that was returned from [`Element::request_layout()`]. - fn prepaint( - &mut self, - id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState; - - /// Once layout has been completed, this method will be called to paint the element to the screen. - /// The state argument is the same state that was returned from [`Element::request_layout()`]. - fn paint( - &mut self, - id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - request_layout: &mut Self::RequestLayoutState, - prepaint: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ); - - /// Returns the accessible role for this element, if any. - /// Elements that return `None` are not included in the accessibility tree. - /// - /// Note: inclusion in accessibility tree requires non-`None` [`id`][Element::id]. - /// - /// See the [accessibility guide](crate::_accessibility) for an overview. - fn a11y_role(&self) -> Option { - None - } - - /// Write accessibility properties to the given node. - /// Called only when `a11y_role()` returns `Some`. - /// - /// See the [accessibility guide](crate::_accessibility) for an overview. - fn write_a11y_info(&self, _node: &mut accesskit::Node) {} - - /// Add synthetic child nodes to an [`Element`] that has an - /// [`.id()`][Element::id] and a [`.role()`][Element::a11y_role]. - /// - /// Some elements may want to inject accessibility nodes that do not - /// correspond to any GPUI element. For example, a custom text field element - /// may want to inject synthetic child nodes for the text content. - /// - /// See [Synthetic children](crate::_accessibility#synthetic-children) in - /// the accessibility guide for more detail. - fn a11y_synthetic_children( - &mut self, - _prepaint: &mut Self::PrepaintState, - _builder: &mut A11ySubtreeBuilder, - ) { - } - - /// Convert this element into a dynamically-typed [`AnyElement`]. - fn into_any(self) -> AnyElement { - AnyElement::new(self) - } -} - -/// Implemented by any type that can be converted into an element. -pub trait IntoElement: Sized { - /// The specific type of element into which the implementing type is converted. - /// Useful for converting other types into elements automatically, like Strings - type Element: Element; - - /// Convert self into a type that implements [`Element`]. - fn into_element(self) -> Self::Element; - - /// Convert self into a dynamically-typed [`AnyElement`]. - fn into_any_element(self) -> AnyElement { - self.into_element().into_any() - } -} - -impl FluentBuilder for T {} - -/// An object that can be drawn to the screen. This is the trait that distinguishes "views" from -/// other entities. Views are `Entity`'s which `impl Render` and drawn to the screen. -pub trait Render: 'static + Sized { - /// Render this view into an element tree. - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement; -} - -impl Render for Empty { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - Empty - } -} - -/// You can derive [`IntoElement`] on any type that implements this trait. -/// It is used to construct reusable `components` out of plain data. Think of -/// components as a recipe for a certain pattern of elements. RenderOnce allows -/// you to invoke this pattern, without breaking the fluent builder pattern of -/// the element APIs. -pub trait RenderOnce: 'static { - /// Render this component into an element tree. Note that this method - /// takes ownership of self, as compared to [`Render::render()`] method - /// which takes a mutable reference. - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement; -} - -/// This is a helper trait to provide a uniform interface for constructing elements that -/// can accept any number of any kind of child elements -pub trait ParentElement { - /// Extend this element's children with the given child elements. - fn extend(&mut self, elements: impl IntoIterator); - - /// Add a single child element to this element. - fn child(mut self, child: impl IntoElement) -> Self - where - Self: Sized, - { - self.extend(std::iter::once(child.into_element().into_any())); - self - } - - /// Add multiple child elements to this element. - fn children(mut self, children: impl IntoIterator) -> Self - where - Self: Sized, - { - self.extend(children.into_iter().map(|child| child.into_any_element())); - self - } -} - -/// A globally unique identifier for an element, used to track state across frames. -#[derive(Deref, DerefMut, Clone, Default, Debug, Eq, PartialEq, Hash)] -pub struct GlobalElementId(pub(crate) Arc<[ElementId]>); - -impl Display for GlobalElementId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for (i, element_id) in self.0.iter().enumerate() { - if i > 0 { - write!(f, ".")?; - } - write!(f, "{}", element_id)?; - } - Ok(()) - } -} - -impl GlobalElementId { - pub(crate) fn accesskit_node_id(&self) -> accesskit::NodeId { - use std::hash::{Hash, Hasher}; - let mut hasher = std::hash::DefaultHasher::default(); - self.hash(&mut hasher); - accesskit::NodeId(hasher.finish()) - } -} - -trait ElementObject { - fn inner_element(&mut self) -> &mut dyn Any; - - fn request_layout(&mut self, window: &mut Window, cx: &mut App) -> LayoutId; - - fn prepaint(&mut self, window: &mut Window, cx: &mut App); - - fn paint(&mut self, window: &mut Window, cx: &mut App); - - fn layout_as_root( - &mut self, - available_space: Size, - window: &mut Window, - cx: &mut App, - ) -> Size; -} - -/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window. -pub struct Drawable { - /// The drawn element. - pub element: E, - phase: ElementDrawPhase, -} - -#[derive(Default)] -enum ElementDrawPhase { - #[default] - Start, - RequestLayout { - layout_id: LayoutId, - global_id: Option, - inspector_id: Option, - request_layout: RequestLayoutState, - }, - LayoutComputed { - layout_id: LayoutId, - global_id: Option, - inspector_id: Option, - available_space: Size, - request_layout: RequestLayoutState, - }, - Prepaint { - node_id: DispatchNodeId, - global_id: Option, - inspector_id: Option, - bounds: Bounds, - request_layout: RequestLayoutState, - prepaint: PrepaintState, - }, - Painted, -} - -/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window. -impl Drawable { - pub(crate) fn new(element: E) -> Self { - Drawable { - element, - phase: ElementDrawPhase::Start, - } - } - - fn request_layout(&mut self, window: &mut Window, cx: &mut App) -> LayoutId { - match mem::take(&mut self.phase) { - ElementDrawPhase::Start => { - let global_id = self.element.id().map(|element_id| { - window.element_id_stack.push(element_id); - GlobalElementId(Arc::from(&*window.element_id_stack)) - }); - - let inspector_id; - #[cfg(any(feature = "inspector", debug_assertions))] - { - inspector_id = self.element.source_location().map(|source| { - let path = crate::InspectorElementPath { - global_id: GlobalElementId(Arc::from(&*window.element_id_stack)), - source_location: source, - }; - window.build_inspector_element_id(path) - }); - } - #[cfg(not(any(feature = "inspector", debug_assertions)))] - { - inspector_id = None; - } - - let (layout_id, request_layout) = self.element.request_layout( - global_id.as_ref(), - inspector_id.as_ref(), - window, - cx, - ); - - if global_id.is_some() { - window.element_id_stack.pop(); - } - - self.phase = ElementDrawPhase::RequestLayout { - layout_id, - global_id, - inspector_id, - request_layout, - }; - layout_id - } - _ => panic!("must call request_layout only once"), - } - } - - pub(crate) fn prepaint(&mut self, window: &mut Window, cx: &mut App) { - match mem::take(&mut self.phase) { - ElementDrawPhase::RequestLayout { - layout_id, - global_id, - inspector_id, - mut request_layout, - } - | ElementDrawPhase::LayoutComputed { - layout_id, - global_id, - inspector_id, - mut request_layout, - .. - } => { - if let Some(element_id) = self.element.id() { - window.element_id_stack.push(element_id); - debug_assert_eq!(&*global_id.as_ref().unwrap().0, &*window.element_id_stack); - } - - let bounds = window.layout_bounds(layout_id); - let mut pushed_a11y_node = false; - if window.a11y.is_active() { - if let Some(global_id) = global_id.as_ref() { - if let Some(role) = self.element.a11y_role() { - let node_id = global_id.accesskit_node_id(); - let mut node = accesskit::Node::new(role); - let scale = window.scale_factor(); - node.set_bounds(accesskit::Rect { - x0: (bounds.origin.x.0 * scale) as f64, - y0: (bounds.origin.y.0 * scale) as f64, - x1: ((bounds.origin.x.0 + bounds.size.width.0) * scale) as f64, - y1: ((bounds.origin.y.0 + bounds.size.height.0) * scale) as f64, - }); - self.element.write_a11y_info(&mut node); - window.a11y.node_bounds.insert(node_id, bounds); - pushed_a11y_node = window.a11y.nodes.push(node_id, node); - #[cfg(debug_assertions)] - if pushed_a11y_node { - let view = window - .a11y - .view_type_names - .get(&window.current_view()) - .copied(); - let source_location = self.element.source_location(); - window.a11y.nodes.record_node_info( - node_id, - crate::window::a11y::debug::NodeDebugInfo { - synthetic: false, - view, - element_id: global_id.0.last().map(|id| format!("{id:?}")), - source_location, - }, - ); - } - } - } - } - - let node_id = window.next_frame.dispatch_tree.push_node(); - let mut prepaint = self.element.prepaint( - global_id.as_ref(), - inspector_id.as_ref(), - bounds, - &mut request_layout, - window, - cx, - ); - window.next_frame.dispatch_tree.pop_node(); - - if pushed_a11y_node { - if let Some(global_id) = global_id.as_ref() { - #[cfg(debug_assertions)] - let creator = crate::window::a11y::debug::NodeCreator { - view: window - .a11y - .view_type_names - .get(&window.current_view()) - .copied(), - element_id: global_id.0.last().map(|id| format!("{id:?}")), - source_location: self.element.source_location(), - }; - let mut builder = A11ySubtreeBuilder::new( - global_id.accesskit_node_id(), - &mut window.a11y.nodes, - ); - #[cfg(debug_assertions)] - { - builder = builder.with_creator(creator); - } - self.element - .a11y_synthetic_children(&mut prepaint, &mut builder); - } - window.a11y.nodes.pop(); - } - - if global_id.is_some() { - window.element_id_stack.pop(); - } - - self.phase = ElementDrawPhase::Prepaint { - node_id, - global_id, - inspector_id, - bounds, - request_layout, - prepaint, - }; - } - _ => panic!("must call request_layout before prepaint"), - } - } - - pub(crate) fn paint( - &mut self, - window: &mut Window, - cx: &mut App, - ) -> (E::RequestLayoutState, E::PrepaintState) { - match mem::take(&mut self.phase) { - ElementDrawPhase::Prepaint { - node_id, - global_id, - inspector_id, - bounds, - mut request_layout, - mut prepaint, - .. - } => { - if let Some(element_id) = self.element.id() { - window.element_id_stack.push(element_id); - debug_assert_eq!(&*global_id.as_ref().unwrap().0, &*window.element_id_stack); - } - - window.next_frame.dispatch_tree.set_active_node(node_id); - self.element.paint( - global_id.as_ref(), - inspector_id.as_ref(), - bounds, - &mut request_layout, - &mut prepaint, - window, - cx, - ); - - if global_id.is_some() { - window.element_id_stack.pop(); - } - - self.phase = ElementDrawPhase::Painted; - (request_layout, prepaint) - } - _ => panic!("must call prepaint before paint"), - } - } - - pub(crate) fn layout_as_root( - &mut self, - available_space: Size, - window: &mut Window, - cx: &mut App, - ) -> Size { - if matches!(&self.phase, ElementDrawPhase::Start) { - self.request_layout(window, cx); - } - - let layout_id = match mem::take(&mut self.phase) { - ElementDrawPhase::RequestLayout { - layout_id, - global_id, - inspector_id, - request_layout, - } => { - window.compute_layout(layout_id, available_space, cx); - self.phase = ElementDrawPhase::LayoutComputed { - layout_id, - global_id, - inspector_id, - available_space, - request_layout, - }; - layout_id - } - ElementDrawPhase::LayoutComputed { - layout_id, - global_id, - inspector_id, - available_space: prev_available_space, - request_layout, - } => { - if available_space != prev_available_space { - window.compute_layout(layout_id, available_space, cx); - } - self.phase = ElementDrawPhase::LayoutComputed { - layout_id, - global_id, - inspector_id, - available_space, - request_layout, - }; - layout_id - } - _ => panic!("cannot measure after painting"), - }; - - window.layout_bounds(layout_id).size - } -} - -impl ElementObject for Drawable -where - E: Element, - E::RequestLayoutState: 'static, -{ - fn inner_element(&mut self) -> &mut dyn Any { - &mut self.element - } - - #[inline] - fn request_layout(&mut self, window: &mut Window, cx: &mut App) -> LayoutId { - Drawable::request_layout(self, window, cx) - } - - #[inline] - fn prepaint(&mut self, window: &mut Window, cx: &mut App) { - Drawable::prepaint(self, window, cx); - } - - #[inline] - fn paint(&mut self, window: &mut Window, cx: &mut App) { - Drawable::paint(self, window, cx); - } - - #[inline] - fn layout_as_root( - &mut self, - available_space: Size, - window: &mut Window, - cx: &mut App, - ) -> Size { - Drawable::layout_as_root(self, available_space, window, cx) - } -} - -/// A dynamically typed element that can be used to store any element type. -pub struct AnyElement(ArenaBox); - -impl AnyElement { - pub(crate) fn new(element: E) -> Self - where - E: 'static + Element, - E::RequestLayoutState: Any, - { - let element = with_element_arena(|arena| arena.alloc(|| Drawable::new(element))) - .map(|element| element as &mut dyn ElementObject); - AnyElement(element) - } - - /// Attempt to downcast a reference to the boxed element to a specific type. - pub fn downcast_mut(&mut self) -> Option<&mut T> { - self.0.inner_element().downcast_mut::() - } - - /// Request the layout ID of the element stored in this `AnyElement`. - /// Used for laying out child elements in a parent element. - pub fn request_layout(&mut self, window: &mut Window, cx: &mut App) -> LayoutId { - self.0.request_layout(window, cx) - } - - /// Prepares the element to be painted by storing its bounds, giving it a chance to draw hitboxes and - /// request autoscroll before the final paint pass is confirmed. - pub fn prepaint(&mut self, window: &mut Window, cx: &mut App) -> Option { - let focus_assigned = window.next_frame.focus.is_some(); - - self.0.prepaint(window, cx); - - if !focus_assigned && let Some(focus_id) = window.next_frame.focus { - return FocusHandle::for_id(focus_id, &cx.focus_handles); - } - - None - } - - /// Paints the element stored in this `AnyElement`. - pub fn paint(&mut self, window: &mut Window, cx: &mut App) { - self.0.paint(window, cx); - } - - /// Performs layout for this element within the given available space and returns its size. - pub fn layout_as_root( - &mut self, - available_space: Size, - window: &mut Window, - cx: &mut App, - ) -> Size { - self.0.layout_as_root(available_space, window, cx) - } - - /// Prepaints this element at the given absolute origin. - /// If any element in the subtree beneath this element is focused, its FocusHandle is returned. - pub fn prepaint_at( - &mut self, - origin: Point, - window: &mut Window, - cx: &mut App, - ) -> Option { - window.with_absolute_element_offset(origin, |window| self.prepaint(window, cx)) - } - - /// Performs layout on this element in the available space, then prepaints it at the given absolute origin. - /// If any element in the subtree beneath this element is focused, its FocusHandle is returned. - pub fn prepaint_as_root( - &mut self, - origin: Point, - available_space: Size, - window: &mut Window, - cx: &mut App, - ) -> Option { - self.layout_as_root(available_space, window, cx); - window.with_absolute_element_offset(origin, |window| self.prepaint(window, cx)) - } -} - -impl Element for AnyElement { - type RequestLayoutState = (); - type PrepaintState = (); - - fn id(&self) -> Option { - None - } - - fn source_location(&self) -> Option<&'static panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - let layout_id = self.request_layout(window, cx); - (layout_id, ()) - } - - fn prepaint( - &mut self, - _: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _: Bounds, - _: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) { - self.prepaint(window, cx); - } - - fn paint( - &mut self, - _: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _: Bounds, - _: &mut Self::RequestLayoutState, - _: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - self.paint(window, cx); - } -} - -impl IntoElement for AnyElement { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } - - fn into_any_element(self) -> AnyElement { - self - } -} - -/// The empty element, which renders nothing. -pub struct Empty; - -impl IntoElement for Empty { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -impl Element for Empty { - type RequestLayoutState = (); - type PrepaintState = (); - - fn id(&self) -> Option { - None - } - - fn source_location(&self) -> Option<&'static panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - ( - window.request_layout( - Style { - display: crate::Display::None, - ..Default::default() - }, - None, - cx, - ), - (), - ) - } - - fn prepaint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _bounds: Bounds, - _state: &mut Self::RequestLayoutState, - _window: &mut Window, - _cx: &mut App, - ) { - } - - fn paint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - _prepaint: &mut Self::PrepaintState, - _window: &mut Window, - _cx: &mut App, - ) { - } -} diff --git a/crates/gpui_pre/src/elements/anchored.rs b/crates/gpui_pre/src/elements/anchored.rs deleted file mode 100644 index 2926006..0000000 --- a/crates/gpui_pre/src/elements/anchored.rs +++ /dev/null @@ -1,398 +0,0 @@ -use smallvec::SmallVec; - -use crate::{ - Anchor, AnyElement, App, Axis, Bounds, Display, Edges, Element, GlobalElementId, - InspectorElementId, IntoElement, LayoutId, ParentElement, Pixels, Point, Position, Size, Style, - Window, point, px, -}; - -/// The state that the anchored element element uses to track its children. -pub struct AnchoredState { - child_layout_ids: SmallVec<[LayoutId; 4]>, -} - -/// An anchored element that can be used to display UI that -/// will avoid overflowing the window bounds. -pub struct Anchored { - children: SmallVec<[AnyElement; 2]>, - anchor: Anchor, - fit_mode: AnchoredFitMode, - anchor_position: Option>, - position_mode: AnchoredPositionMode, - offset: Option>, -} - -/// anchored gives you an element that will avoid overflowing the window bounds. -/// Its children should have no margin to avoid measurement issues. -pub fn anchored() -> Anchored { - Anchored { - children: SmallVec::new(), - anchor: Anchor::TopLeft, - fit_mode: AnchoredFitMode::SwitchAnchor, - anchor_position: None, - position_mode: AnchoredPositionMode::Window, - offset: None, - } -} - -impl Anchored { - /// Sets which corner of the anchored element should be anchored to the current position. - pub fn anchor(mut self, anchor: Anchor) -> Self { - self.anchor = anchor; - self - } - - /// Sets the position in window coordinates - /// (otherwise the location the anchored element is rendered is used) - pub fn position(mut self, anchor: Point) -> Self { - self.anchor_position = Some(anchor); - self - } - - /// Offset the final position by this amount. - /// Useful when you want to anchor to an element but offset from it, such as in PopoverMenu. - pub fn offset(mut self, offset: Point) -> Self { - self.offset = Some(offset); - self - } - - /// Sets the position mode for this anchored element. Local will have this - /// interpret its [`Anchored::position`] as relative to the parent element. - /// While Window will have it interpret the position as relative to the window. - pub fn position_mode(mut self, mode: AnchoredPositionMode) -> Self { - self.position_mode = mode; - self - } - - /// Snap to window edge instead of switching anchor corner when an overflow would occur. - pub fn snap_to_window(mut self) -> Self { - self.fit_mode = AnchoredFitMode::SnapToWindow; - self - } - - /// Snap to window edge and leave some margins. - pub fn snap_to_window_with_margin(mut self, edges: impl Into>) -> Self { - self.fit_mode = AnchoredFitMode::SnapToWindowWithMargin(edges.into()); - self - } -} - -impl ParentElement for Anchored { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl Element for Anchored { - type RequestLayoutState = AnchoredState; - type PrepaintState = (); - - fn id(&self) -> Option { - None - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (crate::LayoutId, Self::RequestLayoutState) { - let child_layout_ids = self - .children - .iter_mut() - .map(|child| child.request_layout(window, cx)) - .collect::>(); - - let anchored_style = Style { - position: Position::Absolute, - display: Display::Flex, - ..Style::default() - }; - - let layout_id = window.request_layout(anchored_style, child_layout_ids.iter().copied(), cx); - - (layout_id, AnchoredState { child_layout_ids }) - } - - fn prepaint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) { - if request_layout.child_layout_ids.is_empty() { - return; - } - - let children_bounds = request_layout - .child_layout_ids - .iter() - .map(|id| window.layout_bounds(*id)) - .reduce(|acc, bounds| acc.union(&bounds)) - .unwrap(); - - let (origin, mut desired) = self.position_mode.get_position_and_bounds( - self.anchor_position, - self.anchor, - children_bounds.size, - bounds, - self.offset, - ); - - let limits = Bounds { - origin: Point::default(), - size: window.viewport_size(), - }; - - if self.fit_mode == AnchoredFitMode::SwitchAnchor { - let mut anchor = self.anchor; - - if desired.left() < limits.left() || desired.right() > limits.right() { - let switched = Bounds::from_anchor_and_size( - anchor.other_side_along(Axis::Horizontal), - origin, - children_bounds.size, - ); - if !(switched.left() < limits.left() || switched.right() > limits.right()) { - anchor = anchor.other_side_along(Axis::Horizontal); - desired = switched - } - } - - if desired.top() < limits.top() || desired.bottom() > limits.bottom() { - let switched = Bounds::from_anchor_and_size( - anchor.other_side_along(Axis::Vertical), - origin, - children_bounds.size, - ); - if !(switched.top() < limits.top() || switched.bottom() > limits.bottom()) { - desired = switched; - } - } - } - - let client_inset = window.client_inset.unwrap_or(px(0.)); - let edges = match self.fit_mode { - AnchoredFitMode::SnapToWindowWithMargin(edges) => edges, - _ => Edges::default(), - } - .map(|edge| *edge + client_inset); - - // Snap the horizontal edges of the anchored element to the horizontal edges of the window if - // its horizontal bounds overflow, aligning to the left if it is wider than the limits. - if desired.right() > limits.right() { - desired.origin.x -= desired.right() - limits.right() + edges.right; - } - if desired.left() < limits.left() { - desired.origin.x = limits.origin.x + edges.left; - } - - // Snap the vertical edges of the anchored element to the vertical edges of the window if - // its vertical bounds overflow, aligning to the top if it is taller than the limits. - if desired.bottom() > limits.bottom() { - desired.origin.y -= desired.bottom() - limits.bottom() + edges.bottom; - } - if desired.top() < limits.top() { - desired.origin.y = limits.origin.y + edges.top; - } - - let offset = desired.origin - bounds.origin; - let offset = point(offset.x.round(), offset.y.round()); - - window.with_element_offset(offset, |window| { - for child in &mut self.children { - child.prepaint(window, cx); - } - }) - } - - fn paint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _bounds: crate::Bounds, - _request_layout: &mut Self::RequestLayoutState, - _prepaint: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - for child in &mut self.children { - child.paint(window, cx); - } - } -} - -impl IntoElement for Anchored { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -/// Which algorithm to use when fitting the anchored element to be inside the window. -#[derive(Copy, Clone, PartialEq)] -pub enum AnchoredFitMode { - /// Snap the anchored element to the window edge. - SnapToWindow, - /// Snap to window edge and leave some margins. - SnapToWindowWithMargin(Edges), - /// Switch which corner anchor this anchored element is attached to. - SwitchAnchor, -} - -/// Which algorithm to use when positioning the anchored element. -#[derive(Copy, Clone, PartialEq)] -pub enum AnchoredPositionMode { - /// Position the anchored element relative to the window. - Window, - /// Position the anchored element relative to its parent. - Local, -} - -impl AnchoredPositionMode { - fn get_position_and_bounds( - &self, - anchor_position: Option>, - anchor: Anchor, - size: Size, - bounds: Bounds, - offset: Option>, - ) -> (Point, Bounds) { - let offset = offset.unwrap_or_default(); - - match self { - AnchoredPositionMode::Window => { - let anchor_position = anchor_position.unwrap_or(bounds.origin); - let bounds = Bounds::from_anchor_and_size(anchor, anchor_position + offset, size); - (anchor_position, bounds) - } - AnchoredPositionMode::Local => { - let anchor_position = anchor_position.unwrap_or_default(); - let bounds = Bounds::from_anchor_and_size( - anchor, - bounds.origin + anchor_position + offset, - size, - ); - (anchor_position, bounds) - } - } - } -} - -#[cfg(test)] -mod tests { - use crate::{ - Context, Pixels, PlatformInput, Point, TestAppContext, Window, deferred, div, point, - prelude::*, px, size, - }; - - struct AnchoredTestView { - position: Point, - } - - impl Render for AnchoredTestView { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full().child( - div() - .id("scroll-container") - .overflow_y_scroll() - .size_full() - .child(div().h(px(2000.)).w_full()) - .child( - deferred( - super::anchored() - .snap_to_window() - .position(self.position) - .child( - div() - .id("menu") - .debug_selector(|| "MENU".into()) - .w(px(200.)) - .h(px(300.)), - ), - ) - .with_priority(1), - ), - ) - } - } - - #[gpui::test] - fn test_anchored_position_without_scroll(cx: &mut TestAppContext) { - let window = cx.open_window(size(px(800.), px(600.)), |_, _| AnchoredTestView { - position: point(px(100.), px(100.)), - }); - - cx.run_until_parked(); - - let menu_bounds = window - .update(cx, |_, window, _| { - window.rendered_frame.debug_bounds.get("MENU").copied() - }) - .unwrap() - .expect("MENU debug bounds not found"); - - assert_eq!(menu_bounds.origin, point(px(100.), px(100.))); - assert_eq!(menu_bounds.size, size(px(200.), px(300.))); - } - - #[gpui::test] - fn test_anchored_position_when_scrolled(cx: &mut TestAppContext) { - let window = cx.open_window(size(px(800.), px(600.)), |_, _| AnchoredTestView { - position: point(px(100.), px(100.)), - }); - - cx.run_until_parked(); - - window - .update(cx, |_, window, cx| { - let event = gpui::ScrollWheelEvent { - position: point(px(400.), px(300.)), - delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-1000.))), - ..Default::default() - }; - window.dispatch_event(PlatformInput::ScrollWheel(event), cx); - }) - .unwrap(); - - cx.run_until_parked(); - - let menu_bounds = window - .update(cx, |_, window, _| { - window.rendered_frame.debug_bounds.get("MENU").copied() - }) - .unwrap() - .expect("MENU debug bounds not found"); - - assert_eq!(menu_bounds.origin, point(px(100.), px(100.))); - assert_eq!(menu_bounds.size, size(px(200.), px(300.))); - } - - #[gpui::test] - fn test_anchored_snaps_to_window(cx: &mut TestAppContext) { - let window = cx.open_window(size(px(800.), px(600.)), |_, _| AnchoredTestView { - position: point(px(100.), px(500.)), - }); - - cx.run_until_parked(); - - let menu_bounds = window - .update(cx, |_, window, _| { - window.rendered_frame.debug_bounds.get("MENU").copied() - }) - .unwrap() - .expect("MENU debug bounds not found"); - - assert_eq!(menu_bounds.origin, point(px(100.), px(300.))); - assert_eq!(menu_bounds.size, size(px(200.), px(300.))); - } -} diff --git a/crates/gpui_pre/src/elements/animation.rs b/crates/gpui_pre/src/elements/animation.rs deleted file mode 100644 index e076893..0000000 --- a/crates/gpui_pre/src/elements/animation.rs +++ /dev/null @@ -1,1028 +0,0 @@ -use scheduler::Instant; -use std::{cell::Cell, rc::Rc, time::Duration}; - -use crate::{ - AnyElement, App, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, - ParentElement, SpringAnimation, SpringConfig, SpringPlayback, SpringState, SpringTarget, - Window, -}; - -pub use easing::*; -use smallvec::SmallVec; - -/// An animation that can be applied to an element. -#[derive(Clone)] -pub struct Animation { - /// The amount of time for which this animation should run - pub duration: Duration, - /// Whether to repeat this animation when it finishes - pub oneshot: bool, - /// Whether to derive the phase from a shared clock. See [`Animation::repeat_synced`]. - pub synced: bool, - /// A function that maps normalized time to an animated value. - /// The result may exceed 0..1 for easing functions that overshoot. - pub easing: Rc f32>, - /// The maximum number of times per second this animation re-renders. - /// When `None`, the animation re-renders on every frame. - pub max_fps: Option, -} - -impl Animation { - /// Create a new animation with the given duration. - /// By default the animation will only run once and will use a linear easing function. - pub fn new(duration: Duration) -> Self { - Self { - duration, - oneshot: true, - synced: false, - easing: Rc::new(linear), - max_fps: None, - } - } - - /// Set the animation to loop when it finishes. - pub fn repeat(mut self) -> Self { - self.oneshot = false; - self - } - - /// Set the animation to loop when it finishes, phase-locked to a clock shared by the whole [`App`]. - pub fn repeat_synced(mut self) -> Self { - self.oneshot = false; - self.synced = true; - self - } - - /// Sets the easing function used to map normalized time to an animated value. - /// - /// The output is not clamped, allowing physical easing functions such as - /// springs to overshoot. - pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self { - self.easing = Rc::new(easing); - self - } - - /// Limit how often this animation re-renders. Instead of re-rendering on - /// every frame, the animation schedules its next render `1 / max_fps` - /// seconds after the current one. Values that are not finite and positive - /// are ignored. - pub fn with_max_fps(mut self, max_fps: f32) -> Self { - self.max_fps = Some(max_fps); - self - } -} - -/// An extension trait for adding the animation wrapper to both Elements and Components -/// -/// Animations rendered through this trait automatically respect -/// [`App::reduce_motion`](crate::App::reduce_motion): when it is set, -/// the element is rendered in a static state (the end state for oneshot -/// animations, the start state for repeating ones) and no animation frames are -/// scheduled. -pub trait AnimationExt { - /// Render this component or element with an animation - fn with_animation( - self, - id: impl Into, - animation: Animation, - animator: impl Fn(Self, f32) -> Self + 'static, - ) -> AnimationElement - where - Self: Sized, - { - AnimationElement { - id: id.into(), - element: Some(self), - animator: Box::new(move |this, _, value| animator(this, value)), - animations: smallvec::smallvec![animation], - } - } - - /// Render this component or element with a chain of animations - fn with_animations( - self, - id: impl Into, - animations: Vec, - animator: impl Fn(Self, usize, f32) -> Self + 'static, - ) -> AnimationElement - where - Self: Sized, - { - AnimationElement { - id: id.into(), - element: Some(self), - animator: Box::new(animator), - animations: animations.into(), - } - } - - /// Renders this component or element at the value produced by a spring. - /// - /// The element ID preserves position and velocity across target changes. - /// A newly mounted spring starts at its target unless configured with - /// [`SpringAnimation::from`]. - fn with_spring( - self, - id: impl Into, - animation: SpringAnimation, - animator: impl FnOnce(Self, T::Output) -> Self + 'static, - ) -> SpringAnimationElement - where - Self: Sized, - T: SpringTarget, - T::Output: 'static, - { - let SpringAnimation { - config, - target, - epsilon, - initial, - playback, - } = animation; - let scalar_target = target.target(); - SpringAnimationElement { - id: id.into(), - element: Some(self), - config, - target: scalar_target, - epsilon, - initial, - playback, - animator: Some(Box::new(move |this, value| { - animator(this, target.resolve(value)) - })), - } - } -} - -impl AnimationExt for E {} - -/// A GPUI element that applies an animation to another element -pub struct AnimationElement { - id: ElementId, - element: Option, - animations: SmallVec<[Animation; 1]>, - animator: Box E + 'static>, -} - -/// A GPUI element driven by a stateful spring. -pub struct SpringAnimationElement { - id: ElementId, - element: Option, - config: SpringConfig, - target: f32, - epsilon: f32, - initial: Option, - playback: SpringPlayback, - animator: Option E + 'static>>, -} - -impl ParentElement for SpringAnimationElement { - fn extend(&mut self, elements: impl IntoIterator) { - let Some(element) = &mut self.element else { - return; - }; - - element.extend(elements); - } -} - -impl SpringAnimationElement { - /// Returns a new [`SpringAnimationElement`] after applying the given function - /// to the element being animated. - pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> SpringAnimationElement { - self.element = self.element.map(f); - self - } -} - -impl IntoElement for SpringAnimationElement { - type Element = SpringAnimationElement; - - fn into_element(self) -> Self::Element { - self - } -} - -impl ParentElement for AnimationElement { - fn extend(&mut self, elements: impl IntoIterator) { - let Some(element) = &mut self.element else { - return; - }; - - element.extend(elements); - } -} - -impl AnimationElement { - /// Returns a new [`AnimationElement`] after applying the given function - /// to the element being animated. - pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> AnimationElement { - self.element = self.element.map(f); - self - } -} - -impl IntoElement for AnimationElement { - type Element = AnimationElement; - - fn into_element(self) -> Self::Element { - self - } -} - -struct AnimationState { - start: Instant, - animation_ix: usize, - /// Whether a throttled re-render (see [`Animation::with_max_fps`]) is - /// already scheduled, so overlapping renders don't stack extra timers. - delayed_frame_pending: Rc>, -} - -struct SpringElementState { - spring: SpringState, - target: f32, - config: SpringConfig, - initial: f32, - playback: SpringPlayback, - updated_at: Instant, -} - -impl Element for SpringAnimationElement { - type RequestLayoutState = AnyElement; - type PrepaintState = (); - - fn id(&self) -> Option { - Some(self.id.clone()) - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - global_id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (crate::LayoutId, Self::RequestLayoutState) { - window.with_element_state(global_id.unwrap(), |state, window| { - let now = Instant::now(); - let initial = self.initial.unwrap_or(self.target); - let mut state = state.unwrap_or_else(|| SpringElementState { - spring: SpringState { - position: initial, - velocity: 0.0, - }, - target: self.target, - config: self.config, - initial, - playback: self.playback, - updated_at: now, - }); - - let elapsed = now.duration_since(state.updated_at).as_secs_f32(); - match state.playback { - SpringPlayback::Running => { - state.spring = state.config.step(state.spring, state.target, elapsed); - } - SpringPlayback::Paused - | SpringPlayback::Stopped - | SpringPlayback::Completed - | SpringPlayback::Cancelled => {} - } - - state.config = self.config; - state.target = self.target; - - let done = match self.playback { - SpringPlayback::Running => { - if cx.reduce_motion() { - state.spring = SpringState { - position: state.target, - velocity: 0.0, - }; - true - } else { - let done = - state - .config - .is_settled(state.spring, state.target, self.epsilon); - if done { - state.spring = SpringState { - position: state.target, - velocity: 0.0, - }; - } - done - } - } - SpringPlayback::Paused => true, - SpringPlayback::Stopped => { - state.spring.velocity = 0.0; - true - } - SpringPlayback::Completed => { - state.spring = SpringState { - position: state.target, - velocity: 0.0, - }; - true - } - SpringPlayback::Cancelled => { - state.spring = SpringState { - position: state.initial, - velocity: 0.0, - }; - true - } - }; - state.playback = self.playback; - state.updated_at = now; - - let element = self.element.take().expect("should only be called once"); - let animator = self.animator.take().expect("should only be called once"); - let mut element = animator(element, state.spring.position).into_any_element(); - - if !done { - window.request_animation_frame(); - } - - ((element.request_layout(window, cx), element), state) - }) - } - - fn prepaint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _bounds: crate::Bounds, - element: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState { - element.prepaint(window, cx); - } - - fn paint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _bounds: crate::Bounds, - element: &mut Self::RequestLayoutState, - _: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - element.paint(window, cx); - } -} - -impl Element for AnimationElement { - type RequestLayoutState = AnyElement; - type PrepaintState = (); - - fn id(&self) -> Option { - Some(self.id.clone()) - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - global_id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (crate::LayoutId, Self::RequestLayoutState) { - window.with_element_state(global_id.unwrap(), |state, window| { - let mut state = state.unwrap_or_else(|| AnimationState { - start: Instant::now(), - animation_ix: 0, - delayed_frame_pending: Rc::new(Cell::new(false)), - }); - let (animation_ix, delta, done) = if cx.reduce_motion() { - let animation_ix = self.animations.len() - 1; - let delta = if self.animations[animation_ix].oneshot { - 1.0 - } else { - 0.0 - }; - (animation_ix, delta, true) - } else { - let animation_ix = state.animation_ix; - let duration = self.animations[animation_ix].duration; - - let elapsed = if self.animations[animation_ix].synced && !duration.is_zero() { - let elapsed = cx.background_executor().now() - cx.synced_animation_epoch; - // Reduce modulo the duration before f32 conversion, which loses sub-second precision at scale. - Duration::from_nanos((elapsed.as_nanos() % duration.as_nanos()) as u64) - } else { - state.start.elapsed() - }; - let mut delta = elapsed.as_secs_f32() / duration.as_secs_f32(); - - let mut done = false; - if delta > 1.0 { - if self.animations[animation_ix].oneshot { - if animation_ix >= self.animations.len() - 1 { - done = true; - } else { - state.start = Instant::now(); - state.animation_ix += 1; - } - delta = 1.0; - } else { - delta %= 1.0; - } - } - (animation_ix, delta, done) - }; - let delta = (self.animations[animation_ix].easing)(delta); - - debug_assert!(delta.is_finite(), "animated value should be finite"); - - let element = self.element.take().expect("should only be called once"); - let mut element = (self.animator)(element, animation_ix, delta).into_any_element(); - - if !done { - match self.animations[animation_ix].max_fps { - Some(max_fps) if max_fps.is_finite() && max_fps > 0.0 => { - if !state.delayed_frame_pending.get() { - state.delayed_frame_pending.set(true); - let delayed_frame_pending = state.delayed_frame_pending.clone(); - let view = window.current_view(); - let interval = Duration::from_secs_f32(1.0 / max_fps); - window - .spawn(cx, async move |cx| { - cx.background_executor().timer(interval).await; - delayed_frame_pending.set(false); - cx.update(move |_, cx| cx.notify(view)).ok(); - }) - .detach(); - } - } - _ => window.request_animation_frame(), - } - } - - ((element.request_layout(window, cx), element), state) - }) - } - - fn prepaint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _bounds: crate::Bounds, - element: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState { - element.prepaint(window, cx); - } - - fn paint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _bounds: crate::Bounds, - element: &mut Self::RequestLayoutState, - _: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - element.paint(window, cx); - } -} - -mod easing { - use std::f32::consts::PI; - - /// The linear easing function, or delta itself - pub fn linear(delta: f32) -> f32 { - delta - } - - /// The quadratic easing function, delta * delta - pub fn quadratic(delta: f32) -> f32 { - delta * delta - } - - /// The quadratic ease-in-out function, which starts and ends slowly but speeds up in the middle - pub fn ease_in_out(delta: f32) -> f32 { - if delta < 0.5 { - 2.0 * delta * delta - } else { - let x = -2.0 * delta + 2.0; - 1.0 - x * x / 2.0 - } - } - - /// The Quint ease-out function, which starts quickly and decelerates to a stop - pub fn ease_out_quint() -> impl Fn(f32) -> f32 { - move |delta| 1.0 - (1.0 - delta).powi(5) - } - - /// Apply the given easing function, first in the forward direction and then in the reverse direction - pub fn bounce(easing: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 { - move |delta| { - if delta < 0.5 { - easing(delta * 2.0) - } else { - easing((1.0 - delta) * 2.0) - } - } - } - - /// A custom easing function for pulsating alpha that slows down as it approaches 0.1 - pub fn pulsating_between(min: f32, max: f32) -> impl Fn(f32) -> f32 { - let range = max - min; - - move |delta| { - // Use a combination of sine and cubic functions for a more natural breathing rhythm - let t = (delta * 2.0 * PI).sin(); - let breath = (t * t * t + t) / 2.0; - - // Map the breath to our desired alpha range - let normalized_alpha = (breath + 1.0) / 2.0; - - min + (normalized_alpha * range) - } - } -} - -#[cfg(test)] -mod tests { - use std::{cell::RefCell, rc::Rc, time::Duration}; - - use crate::{ - Animation, Context, InteractiveElement, Pixels, Render, SpringAnimation, SpringConfig, - TestAppContext, WindowHandle, div, prelude::*, px, size, - }; - - use super::*; - - struct AnimationTestView { - rendered_deltas: Rc>>, - max_fps: Option, - } - - struct SyncedAnimationTestView { - show_second: bool, - first_deltas: Rc>>, - second_deltas: Rc>>, - } - - struct SpringAnimationTestView { - target: Pixels, - initial: Option, - playback: SpringPlayback, - rendered_values: Rc>>, - } - - impl Render for SpringAnimationTestView { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let rendered_values = self.rendered_values.clone(); - let mut animation = SpringAnimation::new(SpringConfig::new(100.0, 2.0, 1.0)) - .to(self.target) - .with_epsilon(0.01) - .playback(self.playback); - if let Some(initial) = self.initial { - animation = animation.from(initial); - } - div().with_spring("spring-animation", animation, move |this, value| { - rendered_values.borrow_mut().push(value); - this.left(value) - }) - } - } - - impl Render for SyncedAnimationTestView { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let record_deltas = |deltas: Rc>>| { - move |this, delta| { - deltas.borrow_mut().push(delta); - this - } - }; - div() - .size_full() - .child(div().with_animation( - "first-synced-animation", - Animation::new(Duration::from_secs(1)).repeat_synced(), - record_deltas(self.first_deltas.clone()), - )) - .when(self.show_second, |this| { - this.child(div().with_animation( - "second-synced-animation", - Animation::new(Duration::from_secs(1)).repeat_synced(), - record_deltas(self.second_deltas.clone()), - )) - }) - } - } - - impl Render for AnimationTestView { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let rendered_deltas = self.rendered_deltas.clone(); - // The throttled variant syncs to the shared clock so the deltas - // follow the test scheduler's clock rather than wall time. - let mut animation = Animation::new(Duration::from_secs(1)); - if let Some(max_fps) = self.max_fps { - animation = animation.repeat_synced().with_max_fps(max_fps); - } else { - animation = animation.repeat(); - } - div().size_full().child(div().with_animation( - "repeating-animation", - animation, - move |this, delta| { - rendered_deltas.borrow_mut().push(delta); - this - }, - )) - } - } - - fn open_test_window( - cx: &mut TestAppContext, - ) -> (Rc>>, WindowHandle) { - open_test_window_with_max_fps(cx, None) - } - - fn open_test_window_with_max_fps( - cx: &mut TestAppContext, - max_fps: Option, - ) -> (Rc>>, WindowHandle) { - let rendered_deltas = Rc::new(RefCell::new(Vec::new())); - let window = cx.open_window(size(px(100.), px(100.)), { - let rendered_deltas = rendered_deltas.clone(); - move |_, _| AnimationTestView { - rendered_deltas, - max_fps, - } - }); - cx.run_until_parked(); - (rendered_deltas, window) - } - - fn simulate_next_frame(window: &WindowHandle, cx: &mut TestAppContext) -> usize { - let callback_count = window - .update(cx, |_, window, cx| window.simulate_next_frame(cx)) - .unwrap(); - cx.run_until_parked(); - callback_count - } - // Before parent-animation-element, using .with_animation - // would not allow chaining .parent after. This is just a - // build check that we can call div().id().with_animation().child() - #[test] - fn test_animation_parent() { - div() - .id("id") - // - .with_animation( - "animation", - Animation::new(Duration::from_secs(1)), - |el, _t| { - // - el - }, - ) - .child( - // - div(), - ); - } - - #[test] - fn test_spring_animation_parent() { - div() - .id("id") - .with_spring( - "spring-animation", - SpringAnimation::new(SpringConfig::new(100.0, 10.0, 1.0)) - .to(px(10.0)) - .from(px(0.0)), - |element, value| element.left(value), - ) - .child(div()); - } - - #[gpui::test] - fn test_spring_animation_preserves_velocity_when_retargeted(cx: &mut TestAppContext) { - let rendered_values = Rc::new(RefCell::new(Vec::new())); - let window = cx.open_window(size(px(100.0), px(100.0)), { - let rendered_values = rendered_values.clone(); - move |_, _| SpringAnimationTestView { - target: px(0.0), - initial: None, - playback: SpringPlayback::Running, - rendered_values, - } - }); - cx.run_until_parked(); - assert_eq!(*rendered_values.borrow(), vec![px(0.0)]); - - window - .update(cx, |view, _, cx| { - view.target = px(100.0); - cx.notify(); - }) - .unwrap(); - cx.run_until_parked(); - - cx.executor().advance_clock(Duration::from_millis(50)); - assert!(simulate_next_frame(&window, cx) > 0); - let value_before_retargeting = *rendered_values.borrow().last().unwrap(); - assert!(value_before_retargeting > px(0.0)); - assert!(value_before_retargeting < px(100.0)); - - window - .update(cx, |view, _, cx| { - view.target = px(0.0); - cx.notify(); - }) - .unwrap(); - cx.run_until_parked(); - - cx.executor().advance_clock(Duration::from_millis(5)); - assert!(simulate_next_frame(&window, cx) > 0); - let value_after_retargeting = *rendered_values.borrow().last().unwrap(); - assert!(value_after_retargeting > value_before_retargeting); - } - - #[gpui::test] - fn test_paused_spring_resumes_with_its_velocity(cx: &mut TestAppContext) { - let rendered_values = Rc::new(RefCell::new(Vec::new())); - let window = cx.open_window(size(px(100.0), px(100.0)), { - let rendered_values = rendered_values.clone(); - move |_, _| SpringAnimationTestView { - target: px(0.0), - initial: None, - playback: SpringPlayback::Running, - rendered_values, - } - }); - cx.run_until_parked(); - - window - .update(cx, |view, _, cx| { - view.target = px(100.0); - cx.notify(); - }) - .unwrap(); - cx.run_until_parked(); - cx.executor().advance_clock(Duration::from_millis(50)); - assert!(simulate_next_frame(&window, cx) > 0); - - window - .update(cx, |view, _, cx| { - view.target = px(0.0); - view.playback = SpringPlayback::Paused; - cx.notify(); - }) - .unwrap(); - cx.run_until_parked(); - let paused_value = *rendered_values.borrow().last().unwrap(); - - cx.executor().advance_clock(Duration::from_millis(500)); - assert!(simulate_next_frame(&window, cx) > 0); - assert_eq!(*rendered_values.borrow().last().unwrap(), paused_value); - assert_eq!(simulate_next_frame(&window, cx), 0); - - window - .update(cx, |view, _, cx| { - view.playback = SpringPlayback::Running; - cx.notify(); - }) - .unwrap(); - cx.run_until_parked(); - cx.executor().advance_clock(Duration::from_millis(5)); - assert!(simulate_next_frame(&window, cx) > 0); - assert!(*rendered_values.borrow().last().unwrap() > paused_value); - } - - #[gpui::test] - fn test_stopped_spring_resumes_without_velocity(cx: &mut TestAppContext) { - let rendered_values = Rc::new(RefCell::new(Vec::new())); - let window = cx.open_window(size(px(100.0), px(100.0)), { - let rendered_values = rendered_values.clone(); - move |_, _| SpringAnimationTestView { - target: px(0.0), - initial: None, - playback: SpringPlayback::Running, - rendered_values, - } - }); - cx.run_until_parked(); - - window - .update(cx, |view, _, cx| { - view.target = px(1_000_000.0); - cx.notify(); - }) - .unwrap(); - cx.run_until_parked(); - cx.executor().advance_clock(Duration::from_millis(50)); - assert!(simulate_next_frame(&window, cx) > 0); - - window - .update(cx, |view, _, cx| { - view.target = px(0.0); - view.playback = SpringPlayback::Stopped; - cx.notify(); - }) - .unwrap(); - cx.run_until_parked(); - let stopped_value = *rendered_values.borrow().last().unwrap(); - - cx.executor().advance_clock(Duration::from_millis(500)); - assert!(simulate_next_frame(&window, cx) > 0); - assert_eq!(*rendered_values.borrow().last().unwrap(), stopped_value); - assert_eq!(simulate_next_frame(&window, cx), 0); - - window - .update(cx, |view, _, cx| { - view.target = stopped_value; - view.playback = SpringPlayback::Running; - cx.notify(); - }) - .unwrap(); - cx.run_until_parked(); - assert_eq!(*rendered_values.borrow().last().unwrap(), stopped_value); - assert_eq!(simulate_next_frame(&window, cx), 0); - } - - #[gpui::test] - fn test_cancelled_and_completed_springs_resolve_their_endpoints(cx: &mut TestAppContext) { - let rendered_values = Rc::new(RefCell::new(Vec::new())); - let window = cx.open_window(size(px(100.0), px(100.0)), { - let rendered_values = rendered_values.clone(); - move |_, _| SpringAnimationTestView { - target: px(100.0), - initial: Some(px(20.0)), - playback: SpringPlayback::Running, - rendered_values, - } - }); - cx.run_until_parked(); - assert_eq!(*rendered_values.borrow(), vec![px(20.0)]); - - cx.executor().advance_clock(Duration::from_millis(50)); - assert!(simulate_next_frame(&window, cx) > 0); - assert!(*rendered_values.borrow().last().unwrap() > px(20.0)); - - window - .update(cx, |view, _, cx| { - view.playback = SpringPlayback::Cancelled; - cx.notify(); - }) - .unwrap(); - cx.run_until_parked(); - assert_eq!(*rendered_values.borrow().last().unwrap(), px(20.0)); - assert!(simulate_next_frame(&window, cx) > 0); - assert_eq!(simulate_next_frame(&window, cx), 0); - - window - .update(cx, |view, _, cx| { - view.playback = SpringPlayback::Completed; - cx.notify(); - }) - .unwrap(); - cx.run_until_parked(); - assert_eq!(*rendered_values.borrow().last().unwrap(), px(100.0)); - assert_eq!(simulate_next_frame(&window, cx), 0); - } - - #[gpui::test] - fn test_spring_animation_respects_reduced_motion(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_reduce_motion(true)); - let rendered_values = Rc::new(RefCell::new(Vec::new())); - let window = cx.open_window(size(px(100.0), px(100.0)), { - let rendered_values = rendered_values.clone(); - move |_, _| SpringAnimationTestView { - target: px(100.0), - initial: None, - playback: SpringPlayback::Running, - rendered_values, - } - }); - cx.run_until_parked(); - - assert_eq!(*rendered_values.borrow(), vec![px(100.0)]); - assert_eq!(simulate_next_frame(&window, cx), 0); - } - - #[gpui::test] - fn test_repeating_animation_schedules_animation_frames(cx: &mut TestAppContext) { - let (rendered_deltas, window) = open_test_window(cx); - - assert_eq!(rendered_deltas.borrow().len(), 1); - - for expected_frames in 2..=3 { - assert_eq!(simulate_next_frame(&window, cx), 1); - assert_eq!(rendered_deltas.borrow().len(), expected_frames); - } - } - - #[gpui::test] - fn test_max_fps_schedules_timer_driven_frames(cx: &mut TestAppContext) { - let (rendered_deltas, window) = open_test_window_with_max_fps(cx, Some(10.0)); - - // The test scheduler's clock jitters forward slightly on each poll, - // so compare against expectations loosely. - let assert_deltas_approx_eq = |expected: &[f32]| { - let actual = rendered_deltas.borrow(); - assert_eq!(actual.len(), expected.len(), "deltas: {actual:?}"); - for (actual, expected) in actual.iter().zip(expected) { - assert!( - (actual - expected).abs() < 1e-2, - "expected {expected}, got {actual}" - ); - } - }; - - assert_deltas_approx_eq(&[0.0]); - - // No per-frame callback is scheduled; re-renders are timer-driven. - assert_eq!(simulate_next_frame(&window, cx), 0); - assert_deltas_approx_eq(&[0.0]); - - cx.executor().advance_clock(Duration::from_millis(105)); - cx.run_until_parked(); - assert_deltas_approx_eq(&[0.0, 0.105]); - - cx.executor().advance_clock(Duration::from_millis(105)); - cx.run_until_parked(); - assert_deltas_approx_eq(&[0.0, 0.105, 0.21]); - } - - #[gpui::test] - fn test_synced_animations_share_phase_across_elements(cx: &mut TestAppContext) { - let first_deltas = Rc::new(RefCell::new(Vec::new())); - let second_deltas = Rc::new(RefCell::new(Vec::new())); - let window = cx.open_window(size(px(100.), px(100.)), { - let first_deltas = first_deltas.clone(); - let second_deltas = second_deltas.clone(); - move |_, _| SyncedAnimationTestView { - show_second: false, - first_deltas, - second_deltas, - } - }); - cx.run_until_parked(); - - assert_eq!(*first_deltas.borrow(), vec![0.0]); - - cx.executor().advance_clock(Duration::from_millis(250)); - simulate_next_frame(&window, cx); - assert_eq!(*first_deltas.borrow(), vec![0.0, 0.25]); - - // The second element mounts a quarter through the cycle, yet renders - // the shared phase rather than starting at zero. - window - .update(cx, |view, _, cx| { - view.show_second = true; - cx.notify(); - }) - .unwrap(); - cx.run_until_parked(); - cx.executor().advance_clock(Duration::from_millis(250)); - simulate_next_frame(&window, cx); - - assert_eq!(*second_deltas.borrow().last().unwrap(), 0.5); - assert_eq!( - *first_deltas.borrow().last().unwrap(), - *second_deltas.borrow().last().unwrap() - ); - assert!(second_deltas.borrow().iter().all(|delta| *delta > 0.0)); - - // The phase wraps around each full cycle. - cx.executor().advance_clock(Duration::from_millis(2250)); - simulate_next_frame(&window, cx); - assert_eq!(*first_deltas.borrow().last().unwrap(), 0.75); - - // Sub-second precision survives months of uptime: converting the raw - // elapsed time to f32 would round 0.25 away entirely. - cx.executor() - .advance_clock(Duration::from_secs(300 * 24 * 60 * 60) + Duration::from_millis(500)); - simulate_next_frame(&window, cx); - assert_eq!(*first_deltas.borrow().last().unwrap(), 0.25); - } - - #[gpui::test] - fn test_reduce_motion_renders_single_static_frame(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_reduce_motion(true)); - let (rendered_deltas, window) = open_test_window(cx); - - assert_eq!(*rendered_deltas.borrow(), vec![0.0]); - - assert_eq!(simulate_next_frame(&window, cx), 0); - assert_eq!(*rendered_deltas.borrow(), vec![0.0]); - } -} diff --git a/crates/gpui_pre/src/elements/canvas.rs b/crates/gpui_pre/src/elements/canvas.rs deleted file mode 100644 index d57d2f6..0000000 --- a/crates/gpui_pre/src/elements/canvas.rs +++ /dev/null @@ -1,95 +0,0 @@ -use refineable::Refineable as _; - -use crate::{ - App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, Pixels, - Style, StyleRefinement, Styled, Window, -}; - -/// Construct a canvas element with the given paint callback. -/// Useful for adding short term custom drawing to a view. -pub fn canvas( - prepaint: impl 'static + FnOnce(Bounds, &mut Window, &mut App) -> T, - paint: impl 'static + FnOnce(Bounds, T, &mut Window, &mut App), -) -> Canvas { - Canvas { - prepaint: Some(Box::new(prepaint)), - paint: Some(Box::new(paint)), - style: StyleRefinement::default(), - } -} - -/// A canvas element, meant for accessing the low level paint API without defining a whole -/// custom element -pub struct Canvas { - prepaint: Option, &mut Window, &mut App) -> T>>, - paint: Option, T, &mut Window, &mut App)>>, - style: StyleRefinement, -} - -impl IntoElement for Canvas { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -impl Element for Canvas { - type RequestLayoutState = Style; - type PrepaintState = Option; - - fn id(&self) -> Option { - None - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (crate::LayoutId, Self::RequestLayoutState) { - let mut style = Style::default(); - style.refine(&self.style); - let layout_id = window.request_layout(style.clone(), [], cx); - (layout_id, style) - } - - fn prepaint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - _request_layout: &mut Style, - window: &mut Window, - cx: &mut App, - ) -> Option { - Some(self.prepaint.take().unwrap()(bounds, window, cx)) - } - - fn paint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - style: &mut Style, - prepaint: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - let prepaint = prepaint.take().unwrap(); - style.paint(bounds, window, cx, |window, cx| { - (self.paint.take().unwrap())(bounds, prepaint, window, cx) - }); - } -} - -impl Styled for Canvas { - fn style(&mut self) -> &mut crate::StyleRefinement { - &mut self.style - } -} diff --git a/crates/gpui_pre/src/elements/container_query.rs b/crates/gpui_pre/src/elements/container_query.rs deleted file mode 100644 index 0363ce6..0000000 --- a/crates/gpui_pre/src/elements/container_query.rs +++ /dev/null @@ -1,126 +0,0 @@ -//! A container query element, in the spirit of CSS container queries. -//! The element's own size is determined solely by its style and the space -//! offered by its parent. - -use refineable::Refineable as _; - -use crate::{ - AnyElement, App, AvailableSpace, Bounds, Element, ElementId, GlobalElementId, - InspectorElementId, IntoElement, LayoutId, Pixels, Size, Style, StyleRefinement, Styled, - Window, relative, -}; - -/// Construct a container query element with the given render callback. -/// The callback receives the size the element was assigned during layout and -/// returns the contents to display within it. -/// -/// By default the element fills its parent (equivalent to `.size_full()`); -/// use the [`Styled`] methods to size it differently. Because the contents -/// don't exist until after layout, they cannot influence the element's size. -/// -/// # Example -/// -/// ``` -/// # use gpui::{container_query, div, px, IntoElement, ParentElement}; -/// container_query(|size, _window, _cx| { -/// if size.width < px(240.) { -/// div().child("Narrow layout") -/// } else { -/// div().child("Wide layout") -/// } -/// }); -/// ``` -pub fn container_query( - render: impl 'static + FnOnce(Size, &mut Window, &mut App) -> E, -) -> ContainerQuery -where - E: IntoElement, -{ - let mut base_style = StyleRefinement::default(); - base_style.size.width = Some(relative(1.).into()); - base_style.size.height = Some(relative(1.).into()); - - ContainerQuery { - render: Some(Box::new(|size, window, cx| { - render(size, window, cx).into_any_element() - })), - style: base_style, - } -} - -/// A container query element, created with [`container_query`]. -pub struct ContainerQuery { - render: Option, &mut Window, &mut App) -> AnyElement>>, - style: StyleRefinement, -} - -impl Element for ContainerQuery { - type RequestLayoutState = (); - type PrepaintState = Option; - - fn id(&self) -> Option { - None - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - let mut style = Style::default(); - style.refine(&self.style); - let layout_id = window.request_layout(style, [], cx); - (layout_id, ()) - } - - fn prepaint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Option { - let render = self.render.take()?; - let mut child = render(bounds.size, window, cx); - child.layout_as_root(bounds.size.map(AvailableSpace::Definite), window, cx); - child.prepaint_at(bounds.origin, window, cx); - Some(child) - } - - fn paint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - prepaint: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - if let Some(child) = prepaint { - child.paint(window, cx); - } - } -} - -impl IntoElement for ContainerQuery { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -impl Styled for ContainerQuery { - fn style(&mut self) -> &mut StyleRefinement { - &mut self.style - } -} diff --git a/crates/gpui_pre/src/elements/deferred.rs b/crates/gpui_pre/src/elements/deferred.rs deleted file mode 100644 index ddf324a..0000000 --- a/crates/gpui_pre/src/elements/deferred.rs +++ /dev/null @@ -1,206 +0,0 @@ -use crate::{ - AnyElement, App, Bounds, Element, GlobalElementId, InspectorElementId, IntoElement, LayoutId, - Pixels, Window, -}; - -/// Builds a `Deferred` element, which delays the layout and paint of its child. -pub fn deferred(child: impl IntoElement) -> Deferred { - Deferred { - child: Some(child.into_any_element()), - priority: 0, - } -} - -/// An element which delays the painting of its child until after all of -/// its ancestors, while keeping its layout as part of the current element tree. -pub struct Deferred { - child: Option, - priority: usize, -} - -impl Deferred { - /// Sets the `priority` value of the `deferred` element, which - /// determines the drawing order relative to other deferred elements, - /// with higher values being drawn on top. - pub fn with_priority(mut self, priority: usize) -> Self { - self.priority = priority; - self - } -} - -impl Element for Deferred { - type RequestLayoutState = (); - type PrepaintState = (); - - fn id(&self) -> Option { - None - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, ()) { - let layout_id = self.child.as_mut().unwrap().request_layout(window, cx); - (layout_id, ()) - } - - fn prepaint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - _cx: &mut App, - ) { - let child = self.child.take().unwrap(); - let element_offset = window.element_offset(); - window.defer_draw(child, element_offset, self.priority, None) - } - - fn paint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - _prepaint: &mut Self::PrepaintState, - _window: &mut Window, - _cx: &mut App, - ) { - } -} - -impl IntoElement for Deferred { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -impl Deferred { - /// Sets a priority for the element. A higher priority conceptually means painting the element - /// on top of deferred draws with a lower priority (i.e. closer to the viewer). - pub fn priority(mut self, priority: usize) -> Self { - self.priority = priority; - self - } -} - -#[cfg(test)] -mod tests { - use crate::{ - Context, Entity, StyleRefinement, TestAppContext, Window, anchored, deferred, div, point, - prelude::*, px, size, - }; - - /// A stand-in for a dock panel hosting a popover (deferred draw) whose - /// content opens another popover (a deferred draw created while - /// prepainting the first one's content). - struct PanelView; - - impl Render for PanelView { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().key_context("Panel").size_full().child( - deferred( - anchored().position(point(px(10.), px(10.))).child( - div().key_context("Popover").w(px(200.)).h(px(200.)).child( - deferred( - anchored().position(point(px(30.), px(30.))).child( - div() - .key_context("NestedMenu") - .debug_selector(|| "NESTED_MENU".into()) - .w(px(50.)) - .h(px(50.)), - ), - ) - .with_priority(2), - ), - ), - ) - .with_priority(1), - ) - } - } - - struct RootView { - panel: Entity, - } - - impl Render for RootView { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().key_context("Root").size_full().child( - self.panel - .clone() - .cached(StyleRefinement::default().size_full()), - ) - } - } - - /// Regression test for a crash with nested deferred draws (e.g. a popover - /// menu inside a popover hosted by a cached dock panel). Prepaint indices - /// recorded during the deferred draw rounds must index the same - /// `deferred_draws` vector that `reuse_prepaint` slices on the next frame; - /// previously they were measured against a transient per-round vector, so - /// reusing the panel's subtree grafted the wrong deferred draws and - /// panicked in the dispatch tree. - #[gpui::test] - fn test_nested_deferred_draws_with_reused_views(cx: &mut TestAppContext) { - let window = cx.open_window(size(px(800.), px(600.)), |_, cx| { - let panel = cx.new(|_| PanelView); - RootView { panel } - }); - cx.run_until_parked(); - - let menu_bounds = window - .update(cx, |_, window, _| { - window - .rendered_frame - .debug_bounds - .get("NESTED_MENU") - .copied() - }) - .unwrap() - .expect("NESTED_MENU debug bounds not found"); - assert_eq!(menu_bounds.size, size(px(50.), px(50.))); - - // Re-render only the root view; the panel is cached, so its subtree - - // including both deferred draw records - is reused from the previous - // frame. - window.update(cx, |_, _, cx| cx.notify()).unwrap(); - cx.run_until_parked(); - - // Reuse the subtree a second time, exercising ranges that were - // themselves recorded during a reused frame. - window.update(cx, |_, _, cx| cx.notify()).unwrap(); - cx.run_until_parked(); - - // Re-render the panel itself again to prove the popovers still draw. - window - .update(cx, |root, _, cx| { - root.panel.update(cx, |_, cx| cx.notify()); - }) - .unwrap(); - cx.run_until_parked(); - - window - .update(cx, |_, window, _| { - assert_eq!(window.rendered_frame.deferred_draws.len(), 2); - assert!( - window - .rendered_frame - .debug_bounds - .contains_key("NESTED_MENU") - ); - }) - .unwrap(); - } -} diff --git a/crates/gpui_pre/src/elements/div.rs b/crates/gpui_pre/src/elements/div.rs deleted file mode 100644 index a23a59e..0000000 --- a/crates/gpui_pre/src/elements/div.rs +++ /dev/null @@ -1,5450 +0,0 @@ -//! Div is the central, reusable element that most GPUI trees will be built from. -//! It functions as a container for other elements, and provides a number of -//! useful features for laying out and styling its children as well as binding -//! mouse events and action handlers. It is meant to be similar to the HTML `

` -//! element, but for GPUI. -//! -//! # Build your own div -//! -//! GPUI does not directly provide APIs for stateful, multi step events like `click` -//! and `drag`. We want GPUI users to be able to build their own abstractions for -//! their own needs. However, as a UI framework, we're also obliged to provide some -//! building blocks to make the process of building your own elements easier. -//! For this we have the [`Interactivity`] and the [`StyleRefinement`] structs, as well -//! as several associated traits. Together, these provide the full suite of Dom-like events -//! and Tailwind-like styling that you can use to build your own custom elements. Div is -//! constructed by combining these two systems into an all-in-one element. - -use crate::{ - point, px, size, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent, - DispatchPhase, Display, Element, ElementId, Entity, EntityId, ExternalDragPayload, - ExternalDragPayloadSource, FocusHandle, Global, GlobalElementId, Hitbox, HitboxBehavior, - HitboxId, InspectorElementId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, - KeyboardButton, KeyboardClickEvent, LayoutId, ModifiersChangedEvent, MouseButton, - MouseClickEvent, MouseDownEvent, MouseExitEvent, MouseMoveEvent, MousePressureEvent, - MouseUpEvent, OngoingScroll, Overflow, ParentElement, PinchEvent, Pixels, Point, Render, - ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, Task, TooltipId, - Visibility, Window, WindowControlArea, -}; -use collections::HashMap; -use gpui_util::ResultExt; -use refineable::Refineable; -use smallvec::SmallVec; -use std::{ - any::{Any, TypeId}, - cell::RefCell, - cmp::Ordering, - fmt::Debug, - marker::PhantomData, - mem, - rc::Rc, - sync::Arc, - time::Duration, -}; - -use super::ImageCacheProvider; - -#[cfg(feature = "stacker")] -type StackSafe = stacksafe::StackSafe; -#[cfg(not(feature = "stacker"))] -type StackSafe = T; - -const DRAG_THRESHOLD: f64 = 2.; -const DEFAULT_TOOLTIP_SHOW_DELAY: Duration = Duration::from_millis(500); -const HOVERABLE_TOOLTIP_HIDE_DELAY: Duration = Duration::from_millis(500); - -/// The styling information for a given group. -pub struct GroupStyle { - /// The identifier for this group. - pub group: SharedString, - - /// The specific style refinement that this group would apply - /// to its children. - pub style: Box, -} - -/// An event for when a drag is moving over this element, with the given state type. -pub struct DragMoveEvent { - /// The mouse move event that triggered this drag move event. - pub event: MouseMoveEvent, - - /// The bounds of this element. - pub bounds: Bounds, - drag: PhantomData, - dragged_item: Arc, -} - -impl DragMoveEvent { - /// Returns the drag state for this event. - pub fn drag<'b>(&self, cx: &'b App) -> &'b T { - cx.active_drag - .as_ref() - .and_then(|drag| drag.value.downcast_ref::()) - .expect("DragMoveEvent is only valid when the stored active drag is of the same type.") - } - - /// An item that is about to be dropped. - pub fn dragged_item(&self) -> &dyn Any { - self.dragged_item.as_ref() - } -} - -impl Interactivity { - /// Create an `Interactivity`, capturing the caller location in debug mode. - #[cfg(any(feature = "inspector", debug_assertions))] - #[track_caller] - pub fn new() -> Interactivity { - Interactivity { - source_location: Some(core::panic::Location::caller()), - ..Default::default() - } - } - - /// Create an `Interactivity`, capturing the caller location in debug mode. - #[cfg(not(any(feature = "inspector", debug_assertions)))] - pub fn new() -> Interactivity { - Interactivity::default() - } - - /// Gets the source location of construction. Returns `None` when not in debug mode. - pub fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { - #[cfg(any(feature = "inspector", debug_assertions))] - { - self.source_location - } - - #[cfg(not(any(feature = "inspector", debug_assertions)))] - { - None - } - } - - /// Bind the given callback to the mouse down event for the given mouse button, during the bubble phase. - /// The imperative API equivalent of [`InteractiveElement::on_mouse_down`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to the view state from this callback. - pub fn on_mouse_down( - &mut self, - button: MouseButton, - listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, - ) { - self.mouse_down_listeners - .push(Box::new(move |event, phase, hitbox, window, cx| { - if phase == DispatchPhase::Bubble - && event.button == button - && hitbox.is_hovered(window) - { - (listener)(event, window, cx) - } - })); - } - - /// Bind the given callback to the mouse down event for any button, during the capture phase. - /// The imperative API equivalent of [`InteractiveElement::capture_any_mouse_down`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn capture_any_mouse_down( - &mut self, - listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, - ) { - self.mouse_down_listeners - .push(Box::new(move |event, phase, hitbox, window, cx| { - if phase == DispatchPhase::Capture && hitbox.is_hovered(window) { - (listener)(event, window, cx) - } - })); - } - - /// Bind the given callback to the mouse down event for any button, during the bubble phase. - /// The imperative API equivalent to [`InteractiveElement::on_any_mouse_down`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn on_any_mouse_down( - &mut self, - listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, - ) { - self.mouse_down_listeners - .push(Box::new(move |event, phase, hitbox, window, cx| { - if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { - (listener)(event, window, cx) - } - })); - } - - /// Bind the given callback to the mouse pressure event, during the bubble phase - /// the imperative API equivalent to [`InteractiveElement::on_mouse_pressure`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn on_mouse_pressure( - &mut self, - listener: impl Fn(&MousePressureEvent, &mut Window, &mut App) + 'static, - ) { - self.mouse_pressure_listeners - .push(Box::new(move |event, phase, hitbox, window, cx| { - if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { - (listener)(event, window, cx) - } - })); - } - - /// Bind the given callback to the mouse pressure event, during the capture phase - /// the imperative API equivalent to [`InteractiveElement::on_mouse_pressure`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn capture_mouse_pressure( - &mut self, - listener: impl Fn(&MousePressureEvent, &mut Window, &mut App) + 'static, - ) { - self.mouse_pressure_listeners - .push(Box::new(move |event, phase, hitbox, window, cx| { - if phase == DispatchPhase::Capture && hitbox.is_hovered(window) { - (listener)(event, window, cx) - } - })); - } - - /// Bind the given callback to the mouse up event for the given button, during the bubble phase. - /// The imperative API equivalent to [`InteractiveElement::on_mouse_up`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn on_mouse_up( - &mut self, - button: MouseButton, - listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static, - ) { - self.mouse_up_listeners - .push(Box::new(move |event, phase, hitbox, window, cx| { - if phase == DispatchPhase::Bubble - && event.button == button - && hitbox.is_hovered(window) - { - (listener)(event, window, cx) - } - })); - } - - /// Bind the given callback to the mouse up event for any button, during the capture phase. - /// The imperative API equivalent to [`InteractiveElement::capture_any_mouse_up`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn capture_any_mouse_up( - &mut self, - listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static, - ) { - self.mouse_up_listeners - .push(Box::new(move |event, phase, hitbox, window, cx| { - if phase == DispatchPhase::Capture && hitbox.is_hovered(window) { - (listener)(event, window, cx) - } - })); - } - - /// Bind the given callback to the mouse up event for any button, during the bubble phase. - /// The imperative API equivalent to [`Interactivity::on_any_mouse_up`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn on_any_mouse_up( - &mut self, - listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static, - ) { - self.mouse_up_listeners - .push(Box::new(move |event, phase, hitbox, window, cx| { - if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { - (listener)(event, window, cx) - } - })); - } - - /// Bind the given callback to the mouse down event, on any button, during the capture phase, - /// when the mouse is outside of the bounds of this element. - /// The imperative API equivalent to [`InteractiveElement::on_mouse_down_out`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn on_mouse_down_out( - &mut self, - listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, - ) { - self.mouse_down_listeners - .push(Box::new(move |event, phase, hitbox, window, cx| { - if phase == DispatchPhase::Capture - && !window.has_active_prompt() - && !hitbox.contains(&window.mouse_position()) - { - (listener)(event, window, cx) - } - })); - } - - /// Bind the given callback to the mouse up event, for the given button, during the capture phase, - /// when the mouse is outside of the bounds of this element. - /// The imperative API equivalent to [`InteractiveElement::on_mouse_up_out`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn on_mouse_up_out( - &mut self, - button: MouseButton, - listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static, - ) { - self.mouse_up_listeners - .push(Box::new(move |event, phase, hitbox, window, cx| { - if phase == DispatchPhase::Capture - && event.button == button - && !hitbox.is_hovered(window) - { - (listener)(event, window, cx); - } - })); - } - - /// Bind the given callback to the mouse move event, during the bubble phase. - /// The imperative API equivalent to [`InteractiveElement::on_mouse_move`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn on_mouse_move( - &mut self, - listener: impl Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static, - ) { - self.mouse_move_listeners - .push(Box::new(move |event, phase, hitbox, window, cx| { - if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { - (listener)(event, window, cx); - } - })); - } - - /// Bind the given callback to the mouse exit event, during the bubble phase. - /// The imperative API equivalent to [`InteractiveElement::on_mouse_exit`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn on_mouse_exit( - &mut self, - listener: impl Fn(&MouseExitEvent, &mut Window, &mut App) + 'static, - ) { - self.mouse_exit_listeners - .push(Box::new(move |event, phase, hitbox, window, cx| { - if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { - (listener)(event, window, cx); - } - })); - } - - /// Bind the given callback to the mouse drag event of the given type. Note that this - /// will be called for all move events, inside or outside of this element, as long as the - /// drag was started with this element under the mouse. Useful for implementing draggable - /// UIs that don't conform to a drag and drop style interaction, like resizing. - /// The imperative API equivalent to [`InteractiveElement::on_drag_move`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn on_drag_move( - &mut self, - listener: impl Fn(&DragMoveEvent, &mut Window, &mut App) + 'static, - ) where - T: 'static, - { - self.mouse_move_listeners - .push(Box::new(move |event, phase, hitbox, window, cx| { - if phase == DispatchPhase::Capture - && let Some(drag) = &cx.active_drag - && drag.value.as_ref().type_id() == TypeId::of::() - { - (listener)( - &DragMoveEvent { - event: event.clone(), - bounds: hitbox.bounds, - drag: PhantomData, - dragged_item: Arc::clone(&drag.value), - }, - window, - cx, - ); - } - })); - } - - /// Bind the given callback to scroll wheel events during the bubble phase. - /// The imperative API equivalent to [`InteractiveElement::on_scroll_wheel`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn on_scroll_wheel( - &mut self, - listener: impl Fn(&ScrollWheelEvent, &mut Window, &mut App) + 'static, - ) { - self.scroll_wheel_listeners - .push(Box::new(move |event, phase, hitbox, window, cx| { - if phase == DispatchPhase::Bubble && hitbox.should_handle_scroll(window) { - (listener)(event, window, cx); - } - })); - } - - /// Bind the given callback to pinch gesture events during the bubble phase. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn on_pinch(&mut self, listener: impl Fn(&PinchEvent, &mut Window, &mut App) + 'static) { - self.pinch_listeners - .push(Box::new(move |event, phase, hitbox, window, cx| { - if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { - (listener)(event, window, cx); - } - })); - } - - /// Bind the given callback to pinch gesture events during the capture phase. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn capture_pinch( - &mut self, - listener: impl Fn(&PinchEvent, &mut Window, &mut App) + 'static, - ) { - self.pinch_listeners - .push(Box::new(move |event, phase, _hitbox, window, cx| { - if phase == DispatchPhase::Capture { - (listener)(event, window, cx); - } else { - cx.propagate(); - } - })); - } - - /// Bind the given callback to an action dispatch during the capture phase. - /// The imperative API equivalent to [`InteractiveElement::capture_action`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn capture_action( - &mut self, - listener: impl Fn(&A, &mut Window, &mut App) + 'static, - ) { - self.action_listeners.push(( - TypeId::of::(), - Box::new(move |action, phase, window, cx| { - let action = action.downcast_ref().unwrap(); - if phase == DispatchPhase::Capture { - (listener)(action, window, cx) - } else { - cx.propagate(); - } - }), - )); - } - - /// Bind the given callback to an action dispatch during the bubble phase. - /// The imperative API equivalent to [`InteractiveElement::on_action`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - #[track_caller] - pub fn on_action(&mut self, listener: impl Fn(&A, &mut Window, &mut App) + 'static) { - self.action_listeners.push(( - TypeId::of::(), - Box::new(move |action, phase, window, cx| { - let action = action.downcast_ref().unwrap(); - if phase == DispatchPhase::Bubble { - (listener)(action, window, cx) - } - }), - )); - } - - /// Bind the given callback to an action dispatch, based on a dynamic action parameter - /// instead of a type parameter. Useful for component libraries that want to expose - /// action bindings to their users. - /// The imperative API equivalent to [`InteractiveElement::on_boxed_action`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn on_boxed_action( - &mut self, - action: &dyn Action, - listener: impl Fn(&dyn Action, &mut Window, &mut App) + 'static, - ) { - let action = action.boxed_clone(); - self.action_listeners.push(( - (*action).type_id(), - Box::new(move |_, phase, window, cx| { - if phase == DispatchPhase::Bubble { - (listener)(&*action, window, cx) - } - }), - )); - } - - /// Bind the given callback to key down events during the bubble phase. - /// The imperative API equivalent to [`InteractiveElement::on_key_down`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn on_key_down( - &mut self, - listener: impl Fn(&KeyDownEvent, &mut Window, &mut App) + 'static, - ) { - self.key_down_listeners - .push(Box::new(move |event, phase, window, cx| { - if phase == DispatchPhase::Bubble { - (listener)(event, window, cx) - } - })); - } - - /// Bind the given callback to key down events during the capture phase. - /// The imperative API equivalent to [`InteractiveElement::capture_key_down`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn capture_key_down( - &mut self, - listener: impl Fn(&KeyDownEvent, &mut Window, &mut App) + 'static, - ) { - self.key_down_listeners - .push(Box::new(move |event, phase, window, cx| { - if phase == DispatchPhase::Capture { - listener(event, window, cx) - } - })); - } - - /// Bind the given callback to key up events during the bubble phase. - /// The imperative API equivalent to [`InteractiveElement::on_key_up`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn on_key_up(&mut self, listener: impl Fn(&KeyUpEvent, &mut Window, &mut App) + 'static) { - self.key_up_listeners - .push(Box::new(move |event, phase, window, cx| { - if phase == DispatchPhase::Bubble { - listener(event, window, cx) - } - })); - } - - /// Bind the given callback to key up events during the capture phase. - /// The imperative API equivalent to [`InteractiveElement::on_key_up`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn capture_key_up( - &mut self, - listener: impl Fn(&KeyUpEvent, &mut Window, &mut App) + 'static, - ) { - self.key_up_listeners - .push(Box::new(move |event, phase, window, cx| { - if phase == DispatchPhase::Capture { - listener(event, window, cx) - } - })); - } - - /// Bind the given callback to modifiers changing events. - /// The imperative API equivalent to [`InteractiveElement::on_modifiers_changed`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn on_modifiers_changed( - &mut self, - listener: impl Fn(&ModifiersChangedEvent, &mut Window, &mut App) + 'static, - ) { - self.modifiers_changed_listeners - .push(Box::new(move |event, window, cx| { - listener(event, window, cx) - })); - } - - /// Bind the given callback to drop events of the given type, whether or not the drag started on this element. - /// The imperative API equivalent to [`InteractiveElement::on_drop`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn on_drop(&mut self, listener: impl Fn(&T, &mut Window, &mut App) + 'static) { - self.drop_listeners.push(( - TypeId::of::(), - Box::new(move |dragged_value, window, cx| { - listener(dragged_value.downcast_ref().unwrap(), window, cx); - }), - )); - } - - /// Use the given predicate to determine whether or not a drop event should be dispatched to this element. - /// The imperative API equivalent to [`InteractiveElement::can_drop`]. - pub fn can_drop( - &mut self, - predicate: impl Fn(&dyn Any, &mut Window, &mut App) -> bool + 'static, - ) { - self.can_drop_predicate = Some(Box::new(predicate)); - } - - /// Bind the given callback to click events of this element. - /// The imperative API equivalent to [`StatefulInteractiveElement::on_click`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn on_click(&mut self, listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) - where - Self: Sized, - { - self.click_listeners.push(Rc::new(move |event, window, cx| { - listener(event, window, cx) - })); - } - - /// Bind the given callback to non-primary click events of this element. - /// The imperative API equivalent to [`StatefulInteractiveElement::on_aux_click`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn on_aux_click(&mut self, listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) - where - Self: Sized, - { - self.aux_click_listeners - .push(Rc::new(move |event, window, cx| { - listener(event, window, cx) - })); - } - - /// On drag initiation, this callback will be used to create a new view to render the dragged value for a - /// drag and drop operation. This API should also be used as the equivalent of 'on drag start' with - /// the [`Self::on_drag_move`] API. - /// The imperative API equivalent to [`StatefulInteractiveElement::on_drag`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn on_drag( - &mut self, - value: T, - constructor: impl Fn(&T, Point, &mut Window, &mut App) -> Entity + 'static, - ) where - Self: Sized, - T: 'static, - W: 'static + Render, - { - debug_assert!( - self.drag_listener.is_none(), - "calling on_drag more than once on the same element is not supported" - ); - self.drag_listener = Some(DragListener { - value: Arc::new(value), - render: Box::new(move |value, offset, window, cx| { - constructor(value.downcast_ref().unwrap(), offset, window, cx).into() - }), - external_payload: None, - }); - } - - /// Registers a callback resolving a payload to offer the platform if a drag started by this - /// element leaves the window. It is invoked at most once per drag gesture, when the pointer - /// exits the viewport. Must be called after [`Self::on_drag`], with the same dragged value - /// type `T`. - pub fn external_drag_payload( - &mut self, - resolver: impl Fn(&T, &mut Window, &mut App) -> Option + 'static, - ) where - Self: Sized, - T: 'static, - { - let Some(drag_listener) = self.drag_listener.as_mut() else { - debug_assert!(false, "external_drag_payload must be called after on_drag"); - return; - }; - debug_assert!( - drag_listener.value.as_ref().type_id() == TypeId::of::(), - "external_drag_payload must use the same dragged value type as on_drag" - ); - debug_assert!( - drag_listener.external_payload.is_none(), - "calling external_drag_payload more than once on the same element is not supported" - ); - drag_listener.external_payload = Some(Box::new(move |value, window, cx| { - resolver(value.downcast_ref::()?, window, cx) - })); - } - - /// Bind the given callback on the hover start and end events of this element. Note that the boolean - /// passed to the callback is true when the hover starts and false when it ends. - /// Transitions caused by layout changes under a stationary mouse also invoke the callback. - /// - /// By default, keyboard input suppresses hover until the next mouse move, mouse down, or touch. Set - /// [`HoverListenerMode::InputModalityIndependent`] with [`Self::hover_listener_mode`] to - /// continue hit-testing hover after keyboard input. - /// The imperative API equivalent to [`StatefulInteractiveElement::on_hover`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - pub fn on_hover(&mut self, listener: impl Fn(&bool, &mut Window, &mut App) + 'static) - where - Self: Sized, - { - debug_assert!( - self.hover_listener.is_none(), - "calling on_hover more than once on the same element is not supported" - ); - self.hover_listener = Some(Box::new(listener)); - } - - /// Sets how [`Self::on_hover`] responds to key presses while the mouse is stationary. - /// This affects only the hover listener, not hover styles or tooltips. The imperative API - /// equivalent to [`StatefulInteractiveElement::hover_listener_mode`]. - pub fn hover_listener_mode(&mut self, mode: HoverListenerMode) - where - Self: Sized, - { - self.hover_listener_mode = mode; - } - - /// Use the given callback to construct a new tooltip view when the mouse hovers over this element. - /// The imperative API equivalent to [`StatefulInteractiveElement::tooltip`]. - pub fn tooltip(&mut self, build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) - where - Self: Sized, - { - debug_assert!( - self.tooltip_builder.is_none(), - "calling tooltip more than once on the same element is not supported" - ); - self.tooltip_builder = Some(TooltipBuilder { - build: Rc::new(build_tooltip), - hoverable: false, - }); - } - - /// Use the given callback to construct a new tooltip view when the mouse hovers over this element. - /// The tooltip itself is also hoverable and won't disappear when the user moves the mouse into - /// the tooltip. The imperative API equivalent to [`StatefulInteractiveElement::hoverable_tooltip`]. - pub fn hoverable_tooltip( - &mut self, - build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static, - ) where - Self: Sized, - { - debug_assert!( - self.tooltip_builder.is_none(), - "calling tooltip more than once on the same element is not supported" - ); - self.tooltip_builder = Some(TooltipBuilder { - build: Rc::new(build_tooltip), - hoverable: true, - }); - } - - /// Set the delay before this element's tooltip is shown. - /// The imperative API equivalent to [`StatefulInteractiveElement::tooltip_show_delay`]. - pub fn tooltip_show_delay(&mut self, delay: Duration) { - self.tooltip_show_delay = Some(delay); - } - - /// Block the mouse from all interactions with elements behind this element's hitbox. Typically - /// `block_mouse_except_scroll` should be preferred. - /// - /// The imperative API equivalent to [`InteractiveElement::occlude`] - pub fn occlude_mouse(&mut self) { - self.hitbox_behavior = HitboxBehavior::BlockMouse; - } - - /// Set the bounds of this element as a window control area for the platform window. - /// The imperative API equivalent to [`InteractiveElement::window_control_area`] - pub fn window_control_area(&mut self, area: WindowControlArea) { - self.window_control = Some(area); - } - - /// Block non-scroll mouse interactions with elements behind this element's hitbox. - /// The imperative API equivalent to [`InteractiveElement::block_mouse_except_scroll`]. - /// - /// See [`Hitbox::is_hovered`] for details. - pub fn block_mouse_except_scroll(&mut self) { - self.hitbox_behavior = HitboxBehavior::BlockMouseExceptScroll; - } - - fn has_pinch_listeners(&self) -> bool { - !self.pinch_listeners.is_empty() - } -} - -/// A trait for elements that want to use the standard GPUI event handlers that don't -/// require any state. -pub trait InteractiveElement: Sized { - /// Retrieve the interactivity state associated with this element - fn interactivity(&mut self) -> &mut Interactivity; - - /// Assign this element to a group of elements that can be styled together - fn group(mut self, group: impl Into) -> Self { - self.interactivity().group = Some(group.into()); - self - } - - /// Assign this element an ID, so that it can be used with interactivity - fn id(mut self, id: impl Into) -> Stateful { - self.interactivity().element_id = Some(id.into()); - - Stateful { element: self } - } - - /// Track the focus state of the given focus handle on this element. - /// If the focus handle is focused by the application, this element will - /// apply its focused styles. - fn track_focus(mut self, focus_handle: &FocusHandle) -> Self { - self.interactivity().focusable = true; - self.interactivity().tracked_focus_handle = Some(focus_handle.clone()); - self - } - - /// Set whether this element is a tab stop. - /// - /// When false, the element remains in tab-index order but cannot be reached via keyboard navigation. - /// Useful for container elements: focus the container, then call `window.focus_next(cx)` to focus - /// the first tab stop inside it while having the container element itself be unreachable via the keyboard. - /// Should only be used with `tab_index`. - fn tab_stop(mut self, tab_stop: bool) -> Self { - self.interactivity().tab_stop = tab_stop; - self - } - - /// Set index of the tab stop order, and set this node as a tab stop. - /// This will default the element to being a tab stop. See [`Self::tab_stop`] for more information. - /// This should only be used in conjunction with `tab_group` - /// in order to not interfere with the tab index of other elements. - fn tab_index(mut self, index: isize) -> Self { - self.interactivity().focusable = true; - self.interactivity().tab_index = Some(index); - self.interactivity().tab_stop = true; - self - } - - /// Designate this div as a "tab group". Tab groups have their own location in the tab-index order, - /// but for children of the tab group, the tab index is reset to 0. This can be useful for swapping - /// the order of tab stops within the group, without having to renumber all the tab stops in the whole - /// application. - fn tab_group(mut self) -> Self { - self.interactivity().tab_group = true; - if self.interactivity().tab_index.is_none() { - self.interactivity().tab_index = Some(0); - } - self - } - - /// Set the keymap context for this element. This will be used to determine - /// which action to dispatch from the keymap. - fn key_context(mut self, key_context: C) -> Self - where - C: TryInto, - E: std::fmt::Display, - { - if let Some(key_context) = key_context.try_into().log_err() { - self.interactivity().key_context = Some(key_context); - } - self - } - - /// Apply the given style to this element when the mouse hovers over it - fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self { - debug_assert!( - self.interactivity().hover_style.is_none(), - "hover style already set" - ); - self.interactivity().hover_style = Some(Box::new(f(StyleRefinement::default()))); - self - } - - /// Apply the given style to this element when the mouse hovers over a group member - fn group_hover( - mut self, - group_name: impl Into, - f: impl FnOnce(StyleRefinement) -> StyleRefinement, - ) -> Self { - self.interactivity().group_hover_style = Some(GroupStyle { - group: group_name.into(), - style: Box::new(f(StyleRefinement::default())), - }); - self - } - - /// Bind the given callback to the mouse down event for the given mouse button. - /// The fluent API equivalent to [`Interactivity::on_mouse_down`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to the view state from this callback. - fn on_mouse_down( - mut self, - button: MouseButton, - listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.interactivity().on_mouse_down(button, listener); - self - } - - #[cfg(any(test, feature = "test-support"))] - /// Set a key that can be used to look up this element's bounds - /// in the [`crate::VisualTestContext::debug_bounds`] map - /// This is a noop in release builds - fn debug_selector(mut self, f: impl FnOnce() -> String) -> Self { - self.interactivity().debug_selector = Some(f()); - self - } - - #[cfg(not(any(test, feature = "test-support")))] - /// Set a key that can be used to look up this element's bounds - /// in the [`crate::VisualTestContext::debug_bounds`] map - /// This is a noop in release builds - #[inline] - fn debug_selector(self, _: impl FnOnce() -> String) -> Self { - self - } - - /// Bind the given callback to the mouse down event for any button, during the capture phase. - /// The fluent API equivalent to [`Interactivity::capture_any_mouse_down`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn capture_any_mouse_down( - mut self, - listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.interactivity().capture_any_mouse_down(listener); - self - } - - /// Bind the given callback to the mouse down event for any button, during the capture phase. - /// The fluent API equivalent to [`Interactivity::on_any_mouse_down`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn on_any_mouse_down( - mut self, - listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.interactivity().on_any_mouse_down(listener); - self - } - - /// Bind the given callback to the mouse up event for the given button, during the bubble phase. - /// The fluent API equivalent to [`Interactivity::on_mouse_up`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn on_mouse_up( - mut self, - button: MouseButton, - listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.interactivity().on_mouse_up(button, listener); - self - } - - /// Bind the given callback to the mouse up event for any button, during the capture phase. - /// The fluent API equivalent to [`Interactivity::capture_any_mouse_up`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn capture_any_mouse_up( - mut self, - listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.interactivity().capture_any_mouse_up(listener); - self - } - - /// Bind the given callback to the mouse pressure event, during the bubble phase - /// the fluent API equivalent to [`Interactivity::on_mouse_pressure`] - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn on_mouse_pressure( - mut self, - listener: impl Fn(&MousePressureEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.interactivity().on_mouse_pressure(listener); - self - } - - /// Bind the given callback to the mouse pressure event, during the capture phase - /// the fluent API equivalent to [`Interactivity::on_mouse_pressure`] - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn capture_mouse_pressure( - mut self, - listener: impl Fn(&MousePressureEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.interactivity().capture_mouse_pressure(listener); - self - } - - /// Bind the given callback to the mouse down event, on any button, during the capture phase, - /// when the mouse is outside of the bounds of this element. - /// The fluent API equivalent to [`Interactivity::on_mouse_down_out`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn on_mouse_down_out( - mut self, - listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.interactivity().on_mouse_down_out(listener); - self - } - - /// Bind the given callback to the mouse up event, for the given button, during the capture phase, - /// when the mouse is outside of the bounds of this element. - /// The fluent API equivalent to [`Interactivity::on_mouse_up_out`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn on_mouse_up_out( - mut self, - button: MouseButton, - listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.interactivity().on_mouse_up_out(button, listener); - self - } - - /// Bind the given callback to the mouse move event, during the bubble phase. - /// The fluent API equivalent to [`Interactivity::on_mouse_move`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn on_mouse_move( - mut self, - listener: impl Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.interactivity().on_mouse_move(listener); - self - } - - /// Bind the given callback to the mouse exit event, during the bubble phase. - /// The fluent API equivalent to [`Interactivity::on_mouse_exit`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn on_mouse_exit( - mut self, - listener: impl Fn(&MouseExitEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.interactivity().on_mouse_exit(listener); - self - } - - /// Bind the given callback to the mouse drag event of the given type. Note that this - /// will be called for all move events, inside or outside of this element, as long as the - /// drag was started with this element under the mouse. Useful for implementing draggable - /// UIs that don't conform to a drag and drop style interaction, like resizing. - /// The fluent API equivalent to [`Interactivity::on_drag_move`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn on_drag_move( - mut self, - listener: impl Fn(&DragMoveEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.interactivity().on_drag_move(listener); - self - } - - /// Bind the given callback to scroll wheel events during the bubble phase. - /// The fluent API equivalent to [`Interactivity::on_scroll_wheel`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn on_scroll_wheel( - mut self, - listener: impl Fn(&ScrollWheelEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.interactivity().on_scroll_wheel(listener); - self - } - - /// Bind the given callback to pinch gesture events during the bubble phase. - /// The fluent API equivalent to [`Interactivity::on_pinch`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn on_pinch(mut self, listener: impl Fn(&PinchEvent, &mut Window, &mut App) + 'static) -> Self { - self.interactivity().on_pinch(listener); - self - } - - /// Bind the given callback to pinch gesture events during the capture phase. - /// The fluent API equivalent to [`Interactivity::capture_pinch`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn capture_pinch( - mut self, - listener: impl Fn(&PinchEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.interactivity().capture_pinch(listener); - self - } - /// Capture the given action, before normal action dispatch can fire. - /// The fluent API equivalent to [`Interactivity::capture_action`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn capture_action( - mut self, - listener: impl Fn(&A, &mut Window, &mut App) + 'static, - ) -> Self { - self.interactivity().capture_action(listener); - self - } - - /// Bind the given callback to an action dispatch during the bubble phase. - /// The fluent API equivalent to [`Interactivity::on_action`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - #[track_caller] - fn on_action( - mut self, - listener: impl Fn(&A, &mut Window, &mut App) + 'static, - ) -> Self { - self.interactivity().on_action(listener); - self - } - - /// Bind the given callback to an action dispatch, based on a dynamic action parameter - /// instead of a type parameter. Useful for component libraries that want to expose - /// action bindings to their users. - /// The fluent API equivalent to [`Interactivity::on_boxed_action`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn on_boxed_action( - mut self, - action: &dyn Action, - listener: impl Fn(&dyn Action, &mut Window, &mut App) + 'static, - ) -> Self { - self.interactivity().on_boxed_action(action, listener); - self - } - - /// Bind the given callback to key down events during the bubble phase. - /// The fluent API equivalent to [`Interactivity::on_key_down`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn on_key_down( - mut self, - listener: impl Fn(&KeyDownEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.interactivity().on_key_down(listener); - self - } - - /// Bind the given callback to key down events during the capture phase. - /// The fluent API equivalent to [`Interactivity::capture_key_down`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn capture_key_down( - mut self, - listener: impl Fn(&KeyDownEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.interactivity().capture_key_down(listener); - self - } - - /// Bind the given callback to key up events during the bubble phase. - /// The fluent API equivalent to [`Interactivity::on_key_up`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn on_key_up( - mut self, - listener: impl Fn(&KeyUpEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.interactivity().on_key_up(listener); - self - } - - /// Bind the given callback to key up events during the capture phase. - /// The fluent API equivalent to [`Interactivity::capture_key_up`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn capture_key_up( - mut self, - listener: impl Fn(&KeyUpEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.interactivity().capture_key_up(listener); - self - } - - /// Bind the given callback to modifiers changing events. - /// The fluent API equivalent to [`Interactivity::on_modifiers_changed`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn on_modifiers_changed( - mut self, - listener: impl Fn(&ModifiersChangedEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.interactivity().on_modifiers_changed(listener); - self - } - - /// Apply the given style when the given data type is dragged over this element - fn drag_over( - mut self, - f: impl 'static + Fn(StyleRefinement, &S, &mut Window, &mut App) -> StyleRefinement, - ) -> Self { - self.interactivity().drag_over_styles.push(( - TypeId::of::(), - Box::new(move |currently_dragged: &dyn Any, window, cx| { - f( - StyleRefinement::default(), - currently_dragged.downcast_ref::().unwrap(), - window, - cx, - ) - }), - )); - self - } - - /// Apply the given style when the given data type is dragged over this element's group - fn group_drag_over( - mut self, - group_name: impl Into, - f: impl FnOnce(StyleRefinement) -> StyleRefinement, - ) -> Self { - self.interactivity().group_drag_over_styles.push(( - TypeId::of::(), - GroupStyle { - group: group_name.into(), - style: Box::new(f(StyleRefinement::default())), - }, - )); - self - } - - /// Bind the given callback to drop events of the given type, whether or not the drag started on this element. - /// The fluent API equivalent to [`Interactivity::on_drop`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn on_drop( - mut self, - listener: impl Fn(&T, &mut Window, &mut App) + 'static, - ) -> Self { - self.interactivity().on_drop(listener); - self - } - - /// Use the given predicate to determine whether or not a drop event should be dispatched to this element. - /// The fluent API equivalent to [`Interactivity::can_drop`]. - fn can_drop( - mut self, - predicate: impl Fn(&dyn Any, &mut Window, &mut App) -> bool + 'static, - ) -> Self { - self.interactivity().can_drop(predicate); - self - } - - /// Block the mouse from all interactions with elements behind this element's hitbox. Typically - /// `block_mouse_except_scroll` should be preferred. - /// The fluent API equivalent to [`Interactivity::occlude_mouse`]. - fn occlude(mut self) -> Self { - self.interactivity().occlude_mouse(); - self - } - - /// Set the bounds of this element as a window control area for the platform window. - /// The fluent API equivalent to [`Interactivity::window_control_area`]. - fn window_control_area(mut self, area: WindowControlArea) -> Self { - self.interactivity().window_control_area(area); - self - } - - /// Block non-scroll mouse interactions with elements behind this element's hitbox. - /// The fluent API equivalent to [`Interactivity::block_mouse_except_scroll`]. - /// - /// See [`Hitbox::is_hovered`] for details. - fn block_mouse_except_scroll(mut self) -> Self { - self.interactivity().block_mouse_except_scroll(); - self - } - - /// Set the given styles to be applied when this element, specifically, is focused. - /// Requires that the element is focusable. Elements can be made focusable using [`InteractiveElement::track_focus`]. - fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self - where - Self: Sized, - { - self.interactivity().focus_style = Some(Box::new(f(StyleRefinement::default()))); - self - } - - /// Set the given styles to be applied when this element is inside another element that is focused. - /// Requires that the element is focusable. Elements can be made focusable using [`InteractiveElement::track_focus`]. - fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self - where - Self: Sized, - { - self.interactivity().in_focus_style = Some(Box::new(f(StyleRefinement::default()))); - self - } - - /// Set the given styles to be applied when this element is focused via keyboard navigation. - /// This is similar to CSS's `:focus-visible` pseudo-class - it only applies when the element - /// is focused AND the user is navigating via keyboard (not mouse clicks). - /// Requires that the element is focusable. Elements can be made focusable using [`InteractiveElement::track_focus`]. - fn focus_visible(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self - where - Self: Sized, - { - self.interactivity().focus_visible_style = Some(Box::new(f(StyleRefinement::default()))); - self - } -} - -/// A trait for elements that want to use the standard GPUI interactivity features -/// that require state. -pub trait StatefulInteractiveElement: InteractiveElement { - /// Set the accessible role for this element. - /// - /// See the [accessibility guide](crate::_accessibility) for an overview. - fn role(mut self, role: accesskit::Role) -> Self { - debug_assert!( - role != accesskit::Role::GenericContainer, - "GenericContainer is filtered out of the a11y tree and has no effect" - ); - self.interactivity().override_role = Some(role); - self - } - - /// Set the author-provided identifier exposed to accessibility clients. - /// - /// Unlike the GPUI element ID, this value is visible outside the process. - /// Keep it stable and unique within its accessibility tree. - /// AccessKit maps it to platform identifiers where supported, including - /// UIA `AutomationId` on Windows, `AXIdentifier` on macOS, and AT-SPI - /// `AccessibleId` on Linux stacks whose deployed adapter exposes it. - fn accessibility_id(mut self, id: impl Into) -> Self { - self.interactivity().aria.author_id = Some(id.into()); - self - } - - /// Set the accessible label for this element. - fn aria_label(mut self, label: impl Into) -> Self { - self.interactivity().aria.label = Some(label.into()); - self - } - - /// Set the accessible description for this element. Unlike the label (which - /// names the element), the description provides supplementary information - /// that assistive technology announces after the name, role, and value - - /// for example a settings subtitle or a hint. - fn aria_description(mut self, description: impl Into) -> Self { - self.interactivity().aria.description = Some(description.into()); - self - } - - /// Set the keyboard shortcut(s) that activate this element, announced by - /// assistive technology (maps to AccessKit's `keyboard_shortcut`). - /// - /// Note that this does not create a keymap. It simply instructs assistive - /// technology what the keymap is. - fn aria_keyshortcuts(mut self, keyshortcuts: impl Into) -> Self { - self.interactivity().aria.keyshortcuts = Some(keyshortcuts.into()); - self - } - - /// Report this element as the focused node in the accessibility tree, - /// overriding the element that holds real keyboard focus — but only while - /// one of its ancestors actually holds focus. - /// - /// This implements the `aria-activedescendant` pattern for composite - /// widgets that keep keyboard focus on a container (e.g. a menu or - /// listbox) while a child is "selected": set this on the selected child so - /// assistive technology announces and highlights it as focused. - /// - /// The element must also have a [`role`][Self::role] (and an id) so it - /// produces an accessibility node. Unlike the web's container-side - /// `aria-activedescendant`, this is set on the descendant; GPUI honors it - /// only when a focused ancestor is present in the tree, so it is safe to - /// set unconditionally on the selected child — if the container isn't - /// focused, the claim is ignored. - fn aria_active_descendant(mut self) -> Self { - self.interactivity().report_active_descendant_focus = true; - self - } - - /// Contribute synthetic accessibility nodes — nodes that don't correspond - /// to any element — as children of this element's a11y node. For example, - /// text runs describing an editor's text content. - /// - /// The closure is called after this element is prepainted, and only if it - /// contributed a node to the accessibility tree (i.e. it has an id and a - /// [`role`][StatefulInteractiveElement::role]). - /// - /// See [`Element::a11y_synthetic_children`] for details. - fn a11y_synthetic_children( - mut self, - f: impl FnOnce(&mut crate::A11ySubtreeBuilder) + 'static, - ) -> Self { - self.interactivity().a11y_synthetic_children = Some(Box::new(f)); - self - } - - /// Set the selected state for this element. - fn aria_selected(mut self, selected: bool) -> Self { - self.interactivity().aria.selected = Some(selected); - self - } - - /// Set the ARIA current-value state for this element. - /// - /// AccessKit 0.24 carries `aria-current`, but the published gpui-pre - /// 0.3.3 builder surface omitted the corresponding setter. Keep the - /// extension next to the other accessibility builders so callers can - /// expose current-page/current-step semantics without inventing a second - /// node or mutating the AccessKit tree outside the element. - fn aria_current(mut self, current: accesskit::AriaCurrent) -> Self { - self.interactivity().aria.current = Some(current); - self - } - - /// Set the expanded state for this element. - fn aria_expanded(mut self, expanded: bool) -> Self { - self.interactivity().aria.expanded = Some(expanded); - self - } - - /// Set the toggled state for this element. - fn aria_toggled(mut self, toggled: accesskit::Toggled) -> Self { - self.interactivity().aria.toggled = Some(toggled); - self - } - - /// Set the numeric value for this element. - fn aria_numeric_value(mut self, value: f64) -> Self { - self.interactivity().aria.numeric_value = Some(value); - self - } - - /// Set the step by which assistive technology should expect the numeric - /// value of this element to change (e.g. when incrementing a spin button). - fn aria_numeric_value_step(mut self, step: f64) -> Self { - self.interactivity().aria.numeric_value_step = Some(step); - self - } - - /// Set the string value of this element, e.g. the text content of a simple - /// text input. - fn aria_value(mut self, value: impl Into) -> Self { - self.interactivity().aria.value = Some(value.into()); - self - } - - /// Set the placeholder text reported to assistive technology for this - /// element, shown when a text input is empty. - fn aria_placeholder(mut self, placeholder: impl Into) -> Self { - self.interactivity().aria.placeholder = Some(placeholder.into()); - self - } - - /// Set the minimum numeric value for this element. - fn aria_min_numeric_value(mut self, value: f64) -> Self { - self.interactivity().aria.min_numeric_value = Some(value); - self - } - - /// Set the maximum numeric value for this element. - fn aria_max_numeric_value(mut self, value: f64) -> Self { - self.interactivity().aria.max_numeric_value = Some(value); - self - } - - /// Set the orientation of this element. - fn aria_orientation(mut self, orientation: accesskit::Orientation) -> Self { - self.interactivity().aria.orientation = Some(orientation); - self - } - - /// Set the heading level of this element. - fn aria_level(mut self, level: usize) -> Self { - self.interactivity().aria.level = Some(level); - self - } - - /// Set the position in set of this element. - fn aria_position_in_set(mut self, position: usize) -> Self { - self.interactivity().aria.position_in_set = Some(position); - self - } - - /// Set the size of set for this element. - fn aria_size_of_set(mut self, size: usize) -> Self { - self.interactivity().aria.size_of_set = Some(size); - self - } - - /// Set the row index for this element. - fn aria_row_index(mut self, index: usize) -> Self { - self.interactivity().aria.row_index = Some(index); - self - } - - /// Set the column index for this element. - fn aria_column_index(mut self, index: usize) -> Self { - self.interactivity().aria.column_index = Some(index); - self - } - - /// Set the row count for this element. - fn aria_row_count(mut self, count: usize) -> Self { - self.interactivity().aria.row_count = Some(count); - self - } - - /// Set the column count for this element. - fn aria_column_count(mut self, count: usize) -> Self { - self.interactivity().aria.column_count = Some(count); - self - } - - /// Register a handler for an accessibility action on this element. - /// The handler is called when a screen reader requests the given action. - /// - /// See the [accessibility guide](crate::_accessibility) for an overview. - fn on_a11y_action( - mut self, - action: accesskit::Action, - listener: impl FnMut(Option<&accesskit::ActionData>, &mut crate::Window, &mut crate::App) - + 'static, - ) -> Self { - self.interactivity() - .a11y_action_listeners - .push((action, Box::new(listener))); - self - } - - /// Set this element to focusable. - fn focusable(mut self) -> Self { - self.interactivity().focusable = true; - self - } - - /// Set the overflow x and y to scroll. - fn overflow_scroll(mut self) -> Self { - self.interactivity().base_style.overflow.x = Some(Overflow::Scroll); - self.interactivity().base_style.overflow.y = Some(Overflow::Scroll); - self - } - - /// Set the overflow x to scroll. - fn overflow_x_scroll(mut self) -> Self { - self.interactivity().base_style.overflow.x = Some(Overflow::Scroll); - self - } - - /// Set the overflow y to scroll. - fn overflow_y_scroll(mut self) -> Self { - self.interactivity().base_style.overflow.y = Some(Overflow::Scroll); - self - } - - /// Restrict scrolling of this element to the axis of the input gesture. - /// - /// See [`Style::restrict_scroll_to_axis`](crate::Style::restrict_scroll_to_axis) for details. - fn restrict_scroll_to_axis(mut self) -> Self { - self.interactivity().base_style.restrict_scroll_to_axis = Some(true); - self - } - - /// Track the scroll state of this element with the given handle. - fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self { - self.interactivity().tracked_scroll_handle = Some(scroll_handle.clone()); - self - } - - /// Track the scroll state of this element with the given handle. - fn anchor_scroll(mut self, scroll_anchor: Option) -> Self { - self.interactivity().scroll_anchor = scroll_anchor; - self - } - - /// Set the given styles to be applied when this element is active. - fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self - where - Self: Sized, - { - self.interactivity().active_style = Some(Box::new(f(StyleRefinement::default()))); - self - } - - /// Set the given styles to be applied when this element's group is active. - fn group_active( - mut self, - group_name: impl Into, - f: impl FnOnce(StyleRefinement) -> StyleRefinement, - ) -> Self - where - Self: Sized, - { - self.interactivity().group_active_style = Some(GroupStyle { - group: group_name.into(), - style: Box::new(f(StyleRefinement::default())), - }); - self - } - - /// Bind the given callback to click events of this element. - /// The fluent API equivalent to [`Interactivity::on_click`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn on_click(mut self, listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self - where - Self: Sized, - { - self.interactivity().on_click(listener); - self - } - - /// Bind the given callback to non-primary click events of this element. - /// The fluent API equivalent to [`Interactivity::on_aux_click`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn on_aux_click( - mut self, - listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self - where - Self: Sized, - { - self.interactivity().on_aux_click(listener); - self - } - - /// On drag initiation, this callback will be used to create a new view to render the dragged value for a - /// drag and drop operation. This API should also be used as the equivalent of 'on drag start' with - /// the [`InteractiveElement::on_drag_move`] API. - /// The callback also has access to the offset of triggering click from the origin of parent element. - /// The fluent API equivalent to [`Interactivity::on_drag`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn on_drag( - mut self, - value: T, - constructor: impl Fn(&T, Point, &mut Window, &mut App) -> Entity + 'static, - ) -> Self - where - Self: Sized, - T: 'static, - W: 'static + Render, - { - self.interactivity().on_drag(value, constructor); - self - } - - /// Registers a callback resolving a payload to offer the platform if a drag started by this - /// element leaves the window. It is invoked at most once per drag gesture, when the pointer - /// exits the viewport. Must be called after [`Self::on_drag`], with the same dragged value - /// type `T`. - /// The fluent API equivalent to [`Interactivity::external_drag_payload`]. - fn external_drag_payload( - mut self, - resolver: impl Fn(&T, &mut Window, &mut App) -> Option + 'static, - ) -> Self - where - Self: Sized, - T: 'static, - { - self.interactivity().external_drag_payload(resolver); - self - } - - /// Bind the given callback on the hover start and end events of this element. Note that the boolean - /// passed to the callback is true when the hover starts and false when it ends. - /// Transitions caused by layout changes under a stationary mouse also invoke the callback. - /// - /// By default, keyboard input suppresses hover until the next mouse move, mouse down, or touch. Set - /// [`HoverListenerMode::InputModalityIndependent`] with [`Self::hover_listener_mode`] to - /// continue hit-testing hover after keyboard input. - /// The fluent API equivalent to [`Interactivity::on_hover`]. - /// - /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - fn on_hover(mut self, listener: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self - where - Self: Sized, - { - self.interactivity().on_hover(listener); - self - } - - /// Sets how [`Self::on_hover`] responds to key presses while the mouse is stationary. - /// This affects only the hover listener, not hover styles or tooltips. The fluent API - /// equivalent to [`Interactivity::hover_listener_mode`]. - fn hover_listener_mode(mut self, mode: HoverListenerMode) -> Self - where - Self: Sized, - { - self.interactivity().hover_listener_mode(mode); - self - } - - /// Use the given callback to construct a new tooltip view when the mouse hovers over this element. - /// The fluent API equivalent to [`Interactivity::tooltip`]. - fn tooltip(mut self, build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self - where - Self: Sized, - { - self.interactivity().tooltip(build_tooltip); - self - } - - /// Use the given callback to construct a new tooltip view when the mouse hovers over this element. - /// The tooltip itself is also hoverable and won't disappear when the user moves the mouse into - /// the tooltip. The fluent API equivalent to [`Interactivity::hoverable_tooltip`]. - fn hoverable_tooltip( - mut self, - build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static, - ) -> Self - where - Self: Sized, - { - self.interactivity().hoverable_tooltip(build_tooltip); - self - } - - /// Set the delay before this element's tooltip is shown. - /// The fluent API equivalent to [`Interactivity::tooltip_show_delay`]. - fn tooltip_show_delay(mut self, delay: Duration) -> Self - where - Self: Sized, - { - self.interactivity().tooltip_show_delay(delay); - self - } -} - -pub(crate) type MouseDownListener = - Box; -pub(crate) type MouseUpListener = - Box; -pub(crate) type MousePressureListener = - Box; -pub(crate) type MouseMoveListener = - Box; -pub(crate) type MouseExitListener = - Box; - -pub(crate) type ScrollWheelListener = - Box; - -pub(crate) type PinchListener = - Box; - -pub(crate) type ClickListener = Rc; - -/// Controls how [`StatefulInteractiveElement::on_hover`] responds to key presses while the mouse -/// is stationary. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -pub enum HoverListenerMode { - /// Use input-modality-aware hit testing. Keyboard input suppresses hover until the mouse moves - /// again, unless pointer capture or an active mouse-down interaction keeps the listener hovered. - #[default] - InputModalityAware, - /// Use hit testing even when the last input was from the keyboard. This changes only - /// keyboard-modality filtering; all other [`StatefulInteractiveElement::on_hover`] behavior - /// remains unchanged. - InputModalityIndependent, -} - -impl HoverListenerMode { - fn is_hovered(self, hitbox: &Hitbox, window: &Window) -> bool { - match self { - Self::InputModalityAware => hitbox.is_hovered(window), - Self::InputModalityIndependent => hitbox.id.is_hovered_ignoring_last_input(window), - } - } -} - -pub(crate) struct DragListener { - value: Arc, - render: Box, &mut Window, &mut App) -> AnyView + 'static>, - external_payload: Option, -} - -type ExternalDragPayloadResolver = - Box Option + 'static>; - -type DropListener = Box; - -type CanDropPredicate = Box bool + 'static>; - -pub(crate) struct TooltipBuilder { - build: Rc AnyView + 'static>, - hoverable: bool, -} - -pub(crate) type KeyDownListener = - Box; - -pub(crate) type KeyUpListener = - Box; - -pub(crate) type ModifiersChangedListener = - Box; - -pub(crate) type ActionListener = - Box; - -/// Construct a new [`Div`] element -#[track_caller] -pub fn div() -> Div { - Div { - interactivity: Interactivity::new(), - children: SmallVec::default(), - prepaint_listener: None, - image_cache: None, - prepaint_order_fn: None, - } -} - -/// A [`Div`] element, the all-in-one element for building complex UIs in GPUI -pub struct Div { - interactivity: Interactivity, - children: SmallVec<[StackSafe; 2]>, - prepaint_listener: Option>, &mut Window, &mut App) + 'static>>, - image_cache: Option>, - prepaint_order_fn: Option SmallVec<[usize; 8]>>>, -} - -impl Div { - /// Add a listener to be called when the children of this `Div` are prepainted. - /// This allows you to store the [`Bounds`] of the children for later use. - pub fn on_children_prepainted( - mut self, - listener: impl Fn(Vec>, &mut Window, &mut App) + 'static, - ) -> Self { - self.prepaint_listener = Some(Box::new(listener)); - self - } - - /// Add an image cache at the location of this div in the element tree. - pub fn image_cache(mut self, cache: impl ImageCacheProvider) -> Self { - self.image_cache = Some(Box::new(cache)); - self - } - - /// Specify a function that determines the order in which children are prepainted. - /// - /// The function is called at prepaint time and should return a vector of child indices - /// in the desired prepaint order. Each index should appear exactly once. - /// - /// This is useful when the prepaint of one child affects state that another child reads. - /// For example, in split editor views, the editor with an autoscroll request should - /// be prepainted first so its scroll position update is visible to the other editor. - pub fn with_dynamic_prepaint_order( - mut self, - order_fn: impl Fn(&mut Window, &mut App) -> SmallVec<[usize; 8]> + 'static, - ) -> Self { - self.prepaint_order_fn = Some(Box::new(order_fn)); - self - } -} - -/// A frame state for a `Div` element, which contains layout IDs for its children. -/// -/// This struct is used internally by the `Div` element to manage the layout state of its children -/// during the UI update cycle. It holds a small vector of `LayoutId` values, each corresponding to -/// a child element of the `Div`. These IDs are used to query the layout engine for the computed -/// bounds of the children after the layout phase is complete. -pub struct DivFrameState { - child_layout_ids: SmallVec<[LayoutId; 2]>, -} - -/// Interactivity state displayed an manipulated in the inspector. -#[derive(Clone)] -pub struct DivInspectorState { - /// The inspected element's base style. This is used for both inspecting and modifying the - /// state. In the future it will make sense to separate the read and write, possibly tracking - /// the modifications. - #[cfg(any(feature = "inspector", debug_assertions))] - pub base_style: Box, - /// Inspects the bounds of the element. - pub bounds: Bounds, - /// Size of the children of the element, or `bounds.size` if it has no children. - pub content_size: Size, -} - -impl Styled for Div { - fn style(&mut self) -> &mut StyleRefinement { - &mut self.interactivity.base_style - } -} - -impl InteractiveElement for Div { - fn interactivity(&mut self) -> &mut Interactivity { - &mut self.interactivity - } -} - -impl ParentElement for Div { - fn extend(&mut self, elements: impl IntoIterator) { - #[cfg(feature = "stacker")] - self.children - .extend(elements.into_iter().map(StackSafe::new)); - #[cfg(not(feature = "stacker"))] - self.children.extend(elements); - } -} - -impl Element for Div { - type RequestLayoutState = DivFrameState; - type PrepaintState = Option; - - fn id(&self) -> Option { - self.interactivity.element_id.clone() - } - - fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { - self.interactivity.source_location() - } - - fn a11y_role(&self) -> Option { - // Nodes with `GenericContainer` should never be reported to accesskit. - // Equivalent to an HTML div with no role. - self.interactivity - .override_role - .filter(|role| *role != accesskit::Role::GenericContainer) - } - - fn write_a11y_info(&self, node: &mut accesskit::Node) { - self.interactivity.write_a11y_info(node); - } - - fn a11y_synthetic_children( - &mut self, - _prepaint: &mut Self::PrepaintState, - builder: &mut crate::A11ySubtreeBuilder, - ) { - if let Some(f) = self.interactivity.a11y_synthetic_children.take() { - f(builder); - } - } - - #[cfg_attr(feature = "stacker", stacksafe::stacksafe)] - fn request_layout( - &mut self, - global_id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - let mut child_layout_ids = SmallVec::new(); - let image_cache = self - .image_cache - .as_mut() - .map(|provider| provider.provide(window, cx)); - - let layout_id = window.with_image_cache(image_cache, |window| { - self.interactivity.request_layout( - global_id, - inspector_id, - window, - cx, - |style, window, cx| { - window.with_text_style(style.text_style().cloned(), |window| { - child_layout_ids = self - .children - .iter_mut() - .map(|child| child.request_layout(window, cx)) - .collect::>(); - window.request_layout(style, child_layout_ids.iter().copied(), cx) - }) - }, - ) - }); - - (layout_id, DivFrameState { child_layout_ids }) - } - - #[cfg_attr(feature = "stacker", stacksafe::stacksafe)] - fn prepaint( - &mut self, - global_id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Option { - let image_cache = self - .image_cache - .as_mut() - .map(|provider| provider.provide(window, cx)); - - let has_prepaint_listener = self.prepaint_listener.is_some(); - let mut children_bounds = Vec::with_capacity(if has_prepaint_listener { - request_layout.child_layout_ids.len() - } else { - 0 - }); - - let mut child_min = point(Pixels::MAX, Pixels::MAX); - let mut child_max = Point::default(); - if let Some(handle) = self.interactivity.scroll_anchor.as_ref() { - *handle.last_origin.borrow_mut() = bounds.origin - window.element_offset(); - } - let content_size = if request_layout.child_layout_ids.is_empty() { - bounds.size - } else if let Some(scroll_handle) = self.interactivity.tracked_scroll_handle.as_ref() { - let mut state = scroll_handle.0.borrow_mut(); - state.child_bounds = Vec::with_capacity(request_layout.child_layout_ids.len()); - for child_layout_id in &request_layout.child_layout_ids { - let child_bounds = window.layout_bounds(*child_layout_id); - child_min = child_min.min(&child_bounds.origin); - child_max = child_max.max(&child_bounds.bottom_right()); - state.child_bounds.push(child_bounds); - } - (child_max - child_min).into() - } else { - for child_layout_id in &request_layout.child_layout_ids { - let child_bounds = window.layout_bounds(*child_layout_id); - child_min = child_min.min(&child_bounds.origin); - child_max = child_max.max(&child_bounds.bottom_right()); - - if has_prepaint_listener { - children_bounds.push(child_bounds); - } - } - (child_max - child_min).into() - }; - - if let Some(scroll_handle) = self.interactivity.tracked_scroll_handle.as_ref() { - scroll_handle.scroll_to_active_item(); - } - - self.interactivity.prepaint( - global_id, - inspector_id, - bounds, - content_size, - window, - cx, - |style, scroll_offset, hitbox, window, cx| { - // skip children - if style.display == Display::None { - return hitbox; - } - - window.with_image_cache(image_cache, |window| { - window.with_element_offset(scroll_offset, |window| { - if let Some(order_fn) = &self.prepaint_order_fn { - let order = order_fn(window, cx); - for idx in order { - if let Some(child) = self.children.get_mut(idx) { - child.prepaint(window, cx); - } - } - } else { - for child in &mut self.children { - child.prepaint(window, cx); - } - } - }); - - if let Some(listener) = self.prepaint_listener.as_ref() { - listener(children_bounds, window, cx); - } - }); - - hitbox - }, - ) - } - - #[cfg_attr(feature = "stacker", stacksafe::stacksafe)] - fn paint( - &mut self, - global_id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - hitbox: &mut Option, - window: &mut Window, - cx: &mut App, - ) { - let image_cache = self - .image_cache - .as_mut() - .map(|provider| provider.provide(window, cx)); - - window.with_image_cache(image_cache, |window| { - self.interactivity.paint( - global_id, - inspector_id, - bounds, - hitbox.as_ref(), - window, - cx, - |style, window, cx| { - // skip children - if style.display == Display::None { - return; - } - - for child in &mut self.children { - child.paint(window, cx); - } - }, - ) - }); - } -} - -impl IntoElement for Div { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -#[derive(Default)] -pub(crate) struct AriaProperties { - pub(crate) author_id: Option, - pub(crate) label: Option, - pub(crate) description: Option, - pub(crate) keyshortcuts: Option, - pub(crate) selected: Option, - pub(crate) current: Option, - pub(crate) expanded: Option, - pub(crate) toggled: Option, - pub(crate) numeric_value: Option, - pub(crate) min_numeric_value: Option, - pub(crate) max_numeric_value: Option, - pub(crate) numeric_value_step: Option, - pub(crate) value: Option, - pub(crate) placeholder: Option, - pub(crate) orientation: Option, - pub(crate) level: Option, - pub(crate) position_in_set: Option, - pub(crate) size_of_set: Option, - pub(crate) row_index: Option, - pub(crate) column_index: Option, - pub(crate) row_count: Option, - pub(crate) column_count: Option, -} - -/// The interactivity struct. Powers all of the general-purpose -/// interactivity in the `Div` element. -#[derive(Default)] -pub struct Interactivity { - /// The element ID of the element. In id is required to support a stateful subset of the interactivity such as on_click. - pub element_id: Option, - /// Whether the element was clicked. This will only be present after layout. - pub active: Option, - /// Whether the element was hovered. This will only be present after paint if an hitbox - /// was created for the interactive element. - pub hovered: Option, - pub(crate) tooltip_id: Option, - pub(crate) content_size: Size, - pub(crate) key_context: Option, - pub(crate) focusable: bool, - pub(crate) tracked_focus_handle: Option, - pub(crate) tracked_scroll_handle: Option, - pub(crate) scroll_anchor: Option, - pub(crate) scroll_offset: Option>>>, - pub(crate) ongoing_scroll: Option>>, - pub(crate) group: Option, - /// The base style of the element, before any modifications are applied - /// by focus, active, etc. - pub base_style: Box, - pub(crate) focus_style: Option>, - pub(crate) in_focus_style: Option>, - pub(crate) focus_visible_style: Option>, - pub(crate) hover_style: Option>, - pub(crate) group_hover_style: Option, - pub(crate) active_style: Option>, - pub(crate) group_active_style: Option, - pub(crate) drag_over_styles: Vec<( - TypeId, - Box StyleRefinement>, - )>, - pub(crate) group_drag_over_styles: Vec<(TypeId, GroupStyle)>, - pub(crate) mouse_down_listeners: Vec, - pub(crate) mouse_up_listeners: Vec, - pub(crate) mouse_pressure_listeners: Vec, - pub(crate) mouse_move_listeners: Vec, - pub(crate) mouse_exit_listeners: Vec, - pub(crate) scroll_wheel_listeners: Vec, - pub(crate) pinch_listeners: Vec, - pub(crate) key_down_listeners: Vec, - pub(crate) key_up_listeners: Vec, - pub(crate) modifiers_changed_listeners: Vec, - pub(crate) action_listeners: Vec<(TypeId, ActionListener)>, - pub(crate) drop_listeners: Vec<(TypeId, DropListener)>, - pub(crate) can_drop_predicate: Option, - pub(crate) click_listeners: Vec, - pub(crate) aux_click_listeners: Vec, - pub(crate) drag_listener: Option, - pub(crate) hover_listener: Option>, - pub(crate) hover_listener_mode: HoverListenerMode, - pub(crate) tooltip_builder: Option, - pub(crate) tooltip_show_delay: Option, - pub(crate) window_control: Option, - pub(crate) hitbox_behavior: HitboxBehavior, - pub(crate) tab_index: Option, - pub(crate) tab_group: bool, - pub(crate) tab_stop: bool, - - pub(crate) a11y_action_listeners: - Vec<(accesskit::Action, crate::window::a11y::A11yActionListener)>, - pub(crate) a11y_synthetic_children: Option>, - pub(crate) report_active_descendant_focus: bool, - pub(crate) override_role: Option, - pub(crate) aria: AriaProperties, - - #[cfg(any(feature = "inspector", debug_assertions))] - pub(crate) source_location: Option<&'static core::panic::Location<'static>>, - - #[cfg(any(test, feature = "test-support"))] - pub(crate) debug_selector: Option, -} - -impl Interactivity { - /// Layout this element according to this interactivity state's configured styles - pub fn request_layout( - &mut self, - global_id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - f: impl FnOnce(Style, &mut Window, &mut App) -> LayoutId, - ) -> LayoutId { - #[cfg(any(feature = "inspector", debug_assertions))] - window.with_inspector_state( - _inspector_id, - cx, - |inspector_state: &mut Option, _window| { - if let Some(inspector_state) = inspector_state { - self.base_style = inspector_state.base_style.clone(); - } else { - *inspector_state = Some(DivInspectorState { - base_style: self.base_style.clone(), - bounds: Default::default(), - content_size: Default::default(), - }) - } - }, - ); - - window.with_optional_element_state::( - global_id, - |element_state, window| { - let mut element_state = - element_state.map(|element_state| element_state.unwrap_or_default()); - - if let Some(element_state) = element_state.as_ref() - && cx.has_active_drag() - { - if let Some(pending_mouse_down) = element_state.pending_mouse_down.as_ref() { - *pending_mouse_down.borrow_mut() = None; - } - if let Some(clicked_state) = element_state.clicked_state.as_ref() { - *clicked_state.borrow_mut() = ElementClickedState::default(); - } - } - - // Ensure we store a focus handle in our element state if we're focusable. - // If there's an explicit focus handle we're tracking, use that. Otherwise - // create a new handle and store it in the element state, which lives for as - // as frames contain an element with this id. - if self.focusable - && self.tracked_focus_handle.is_none() - && let Some(element_state) = element_state.as_mut() - { - let mut handle = element_state - .focus_handle - .get_or_insert_with(|| cx.focus_handle()) - .clone() - .tab_stop(self.tab_stop); - - if let Some(index) = self.tab_index { - handle = handle.tab_index(index); - } - - self.tracked_focus_handle = Some(handle); - } - - if let Some(scroll_handle) = self.tracked_scroll_handle.as_ref() { - let scroll_handle_state = scroll_handle.0.borrow(); - self.scroll_offset = Some(scroll_handle_state.offset.clone()); - self.ongoing_scroll = Some(scroll_handle_state.ongoing_scroll.clone()); - } else if (self.base_style.overflow.x == Some(Overflow::Scroll) - || self.base_style.overflow.y == Some(Overflow::Scroll)) - && let Some(element_state) = element_state.as_mut() - { - self.scroll_offset = Some( - element_state - .scroll_offset - .get_or_insert_with(Rc::default) - .clone(), - ); - self.ongoing_scroll = Some( - element_state - .ongoing_scroll - .get_or_insert_with(|| Rc::new(RefCell::new(OngoingScroll::default()))) - .clone(), - ); - } - - let style = self.compute_style_internal(None, element_state.as_mut(), window, cx); - let layout_id = f(style, window, cx); - (layout_id, element_state) - }, - ) - } - - /// Commit the bounds of this element according to this interactivity state's configured styles. - pub fn prepaint( - &mut self, - global_id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - content_size: Size, - window: &mut Window, - cx: &mut App, - f: impl FnOnce(&Style, Point, Option, &mut Window, &mut App) -> R, - ) -> R { - self.content_size = content_size; - - #[cfg(any(feature = "inspector", debug_assertions))] - window.with_inspector_state( - _inspector_id, - cx, - |inspector_state: &mut Option, _window| { - if let Some(inspector_state) = inspector_state { - inspector_state.bounds = bounds; - inspector_state.content_size = content_size; - } - }, - ); - - if let Some(focus_handle) = self.tracked_focus_handle.as_ref() { - window.set_focus_handle(focus_handle, cx); - - if window.a11y.is_active() { - if let Some(global_id) = global_id { - let node_id = global_id.accesskit_node_id(); - window.a11y.set_focusable(node_id, focus_handle.id); - if focus_handle.is_focused(window) { - window.a11y.set_focus(node_id); - } - } else if focus_handle.is_focused(window) { - // Focusable, but with no element id it can't have an - // accessibility node, so screen readers fall back to the - // whole window. - window - .a11y - .note_focus_without_node(focus_handle.id, "it has no element id"); - } - } - } - - if self.report_active_descendant_focus && window.a11y.is_active() { - if let Some(global_id) = global_id { - window - .a11y - .set_active_descendant(global_id.accesskit_node_id()); - } - } - window.with_optional_element_state::( - global_id, - |element_state, window| { - let mut element_state = - element_state.map(|element_state| element_state.unwrap_or_default()); - let style = self.compute_style_internal(None, element_state.as_mut(), window, cx); - - if let Some(element_state) = element_state.as_mut() { - if let Some(clicked_state) = element_state.clicked_state.as_ref() { - let clicked_state = clicked_state.borrow(); - self.active = Some(clicked_state.element); - } - if self.hover_style.is_some() || self.group_hover_style.is_some() { - element_state - .hover_state - .get_or_insert_with(Default::default); - } - if let Some(active_tooltip) = element_state.active_tooltip.as_ref() { - if self.tooltip_builder.is_some() { - self.tooltip_id = set_tooltip_on_window(active_tooltip, window); - } else { - // If there is no longer a tooltip builder, remove the active tooltip. - element_state.active_tooltip.take(); - } - } - } - - window.with_text_style(style.text_style().cloned(), |window| { - window.with_content_mask( - style.overflow_mask(bounds, window.rem_size(), window.scale_factor()), - |window| { - let hitbox = if self.should_insert_hitbox(&style, window, cx) { - Some(window.insert_hitbox(bounds, self.hitbox_behavior)) - } else { - None - }; - - let scroll_offset = - self.clamp_scroll_position(bounds, &style, window, cx); - let result = f(&style, scroll_offset, hitbox, window, cx); - (result, element_state) - }, - ) - }) - }, - ) - } - - fn should_insert_hitbox(&self, style: &Style, window: &Window, cx: &App) -> bool { - self.hitbox_behavior != HitboxBehavior::Normal - || self.window_control.is_some() - || style.mouse_cursor.is_some() - || self.group.is_some() - || self.scroll_offset.is_some() - || self.tracked_focus_handle.is_some() - || self.hover_style.is_some() - || self.group_hover_style.is_some() - || self.hover_listener.is_some() - || !self.mouse_up_listeners.is_empty() - || !self.mouse_pressure_listeners.is_empty() - || !self.mouse_down_listeners.is_empty() - || !self.mouse_move_listeners.is_empty() - || !self.mouse_exit_listeners.is_empty() - || !self.click_listeners.is_empty() - || !self.aux_click_listeners.is_empty() - || !self.scroll_wheel_listeners.is_empty() - || self.has_pinch_listeners() - || self.drag_listener.is_some() - || !self.drop_listeners.is_empty() - || !self.drag_over_styles.is_empty() - || self.tooltip_builder.is_some() - || window.is_inspector_picking(cx) - } - - fn clamp_scroll_position( - &self, - bounds: Bounds, - style: &Style, - window: &mut Window, - _cx: &mut App, - ) -> Point { - fn round_to_two_decimals(pixels: Pixels) -> Pixels { - const ROUNDING_FACTOR: f32 = 100.0; - (pixels * ROUNDING_FACTOR).round() / ROUNDING_FACTOR - } - - if let Some(scroll_offset) = self.scroll_offset.as_ref() { - let mut scroll_to_bottom = false; - let mut tracked_scroll_handle = self - .tracked_scroll_handle - .as_ref() - .map(|handle| handle.0.borrow_mut()); - if let Some(mut scroll_handle_state) = tracked_scroll_handle.as_deref_mut() { - scroll_handle_state.overflow = style.overflow; - scroll_to_bottom = mem::take(&mut scroll_handle_state.scroll_to_bottom); - } - - let rem_size = window.rem_size(); - // Taffy lays the box out with the padding snapped to the device pixel - // grid (`to_taffy`); recomputed unsnapped, e.g. py_1 at a fractional - // rem size, it exceeds `bounds` and leaves the box scrollable by the - // sub-pixel difference. - let padding = style - .padding - .to_pixels(bounds.size.into(), rem_size) - .map(|edge| window.pixel_snap(*edge)); - let padding_size = size(padding.left + padding.right, padding.top + padding.bottom); - // The floating point values produced by Taffy and ours often vary - // slightly after ~5 decimal places. This can lead to cases where after - // subtracting these, the container becomes scrollable for less than - // 0.00000x pixels. As we generally don't benefit from a precision that - // high for the maximum scroll, we round the scroll max to 2 decimal - // places here. - let padded_content_size = self.content_size + padding_size; - let scroll_max = Point::from(padded_content_size - bounds.size) - .map(round_to_two_decimals) - .max(&Default::default()); - // Clamp scroll offset in case scroll max is smaller now (e.g., if children - // were removed or the bounds became larger). - let mut scroll_offset = scroll_offset.borrow_mut(); - - scroll_offset.x = scroll_offset.x.clamp(-scroll_max.x, px(0.)); - if scroll_to_bottom { - scroll_offset.y = -scroll_max.y; - } else { - scroll_offset.y = scroll_offset.y.clamp(-scroll_max.y, px(0.)); - } - - if let Some(mut scroll_handle_state) = tracked_scroll_handle { - scroll_handle_state.max_offset = scroll_max; - scroll_handle_state.bounds = bounds; - } - - *scroll_offset - } else { - Point::default() - } - } - - /// Paint this element according to this interactivity state's configured styles - /// and bind the element's mouse and keyboard events. - /// - /// content_size is the size of the content of the element, which may be larger than the - /// element's bounds if the element is scrollable. - /// - /// the final computed style will be passed to the provided function, along - /// with the current scroll offset - pub fn paint( - &mut self, - global_id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - hitbox: Option<&Hitbox>, - window: &mut Window, - cx: &mut App, - f: impl FnOnce(&Style, &mut Window, &mut App), - ) { - self.hovered = hitbox.map(|hitbox| hitbox.is_hovered(window)); - window.with_optional_element_state::( - global_id, - |element_state, window| { - let mut element_state = - element_state.map(|element_state| element_state.unwrap_or_default()); - - let style = self.compute_style_internal(hitbox, element_state.as_mut(), window, cx); - - #[cfg(any(feature = "test-support", test))] - if let Some(debug_selector) = &self.debug_selector { - window - .next_frame - .debug_bounds - .insert(debug_selector.clone(), bounds); - } - - self.paint_hover_group_handler(window, cx); - - if style.visibility == Visibility::Hidden { - return ((), element_state); - } - - let mut tab_group = None; - if self.tab_group { - tab_group = self.tab_index; - } - - window.with_element_opacity(style.opacity, |window| { - style.paint(bounds, window, cx, |window: &mut Window, cx: &mut App| { - window.with_text_style(style.text_style().cloned(), |window| { - window.with_content_mask( - style.overflow_mask( - bounds, - window.rem_size(), - window.scale_factor(), - ), - |window| { - window.with_tab_group(tab_group, |window| { - // Register the container's own focus handle *inside* its - // tab group, so that focusing the container and then - // calling `focus_next` descends into this group's first - // item. Inserting it before `with_tab_group` would give the - // container a shallower tab path than its children; with - // sibling groups every container would then sort ahead of - // every item, and `focus_next` from a container would jump - // to the first item in the whole window instead of its own. - if let Some(focus_handle) = &self.tracked_focus_handle { - window.next_frame.tab_stops.insert(focus_handle); - } - if let Some(hitbox) = hitbox { - #[cfg(debug_assertions)] - self.paint_debug_info( - global_id, hitbox, &style, window, cx, - ); - - if let Some(drag) = cx.active_drag.as_ref() { - if let Some(mouse_cursor) = drag.cursor_style { - window.set_window_cursor_style(mouse_cursor); - } - } else { - if let Some(mouse_cursor) = style.mouse_cursor { - window.set_cursor_style(mouse_cursor, hitbox); - } - } - - if let Some(group) = self.group.clone() { - GroupHitboxes::push(group, hitbox.id, cx); - } - - if let Some(area) = self.window_control { - window.insert_window_control_hitbox( - area, - hitbox.clone(), - ); - } - - self.paint_mouse_listeners( - hitbox, - element_state.as_mut(), - window, - cx, - ); - self.paint_scroll_listener(hitbox, &style, window, cx); - } - - self.paint_keyboard_listeners(window, cx); - - if window.a11y.is_active() { - if let Some(global_id) = global_id { - if !self.a11y_action_listeners.is_empty() { - let node_id = global_id.accesskit_node_id(); - for (action, listener) in - self.a11y_action_listeners.drain(..) - { - window.on_a11y_action( - node_id, action, listener, - ); - } - } - } - } - - f(&style, window, cx); - - if let Some(_hitbox) = hitbox { - #[cfg(any(feature = "inspector", debug_assertions))] - window.insert_inspector_hitbox( - _hitbox.id, - _inspector_id, - cx, - ); - - if let Some(group) = self.group.as_ref() { - GroupHitboxes::pop(group, cx); - } - } - }) - }, - ); - }); - }); - }); - - ((), element_state) - }, - ); - } - - #[cfg(debug_assertions)] - fn paint_debug_info( - &self, - global_id: Option<&GlobalElementId>, - hitbox: &Hitbox, - style: &Style, - window: &mut Window, - cx: &mut App, - ) { - use crate::{BorderStyle, TextAlign}; - - if let Some(global_id) = global_id - && (style.debug || style.debug_below || cx.has_global::()) - && hitbox.is_hovered(window) - { - const FONT_SIZE: crate::Pixels = crate::Pixels(10.); - let element_id = format!("{global_id:?}"); - let str_len = element_id.len(); - - let render_debug_text = |window: &mut Window| { - if let Some(text) = window - .text_system() - .shape_text( - element_id.into(), - FONT_SIZE, - &[window.text_style().to_run(str_len)], - None, - None, - ) - .ok() - .and_then(|mut text| text.pop()) - { - text.paint(hitbox.origin, FONT_SIZE, TextAlign::Left, None, window, cx) - .ok(); - - let text_bounds = crate::Bounds { - origin: hitbox.origin, - size: text.size(FONT_SIZE), - }; - if let Some(source_location) = self.source_location - && text_bounds.contains(&window.mouse_position()) - && window.modifiers().secondary() - { - let secondary_held = window.modifiers().secondary(); - window.on_key_event({ - move |e: &crate::ModifiersChangedEvent, _phase, window, _cx| { - if e.modifiers.secondary() != secondary_held - && text_bounds.contains(&window.mouse_position()) - { - window.refresh(); - } - } - }); - - let was_hovered = hitbox.is_hovered(window); - let current_view = window.current_view(); - window.on_mouse_event({ - let hitbox = hitbox.clone(); - move |_: &MouseMoveEvent, phase, window, cx| { - if phase == DispatchPhase::Capture { - let hovered = hitbox.is_hovered(window); - if hovered != was_hovered { - cx.notify(current_view) - } - } - } - }); - - window.on_mouse_event({ - let hitbox = hitbox.clone(); - move |e: &crate::MouseDownEvent, phase, window, cx| { - if text_bounds.contains(&e.position) - && phase.capture() - && hitbox.is_hovered(window) - { - cx.stop_propagation(); - let Ok(dir) = std::env::current_dir() else { - return; - }; - - eprintln!( - "This element was created at:\n{}:{}:{}", - dir.join(source_location.file()).to_string_lossy(), - source_location.line(), - source_location.column() - ); - } - } - }); - window.paint_quad(crate::outline( - crate::Bounds { - origin: hitbox.origin - + crate::point(crate::px(0.), FONT_SIZE - px(2.)), - size: crate::Size { - width: text_bounds.size.width, - height: crate::px(1.), - }, - }, - crate::red(), - BorderStyle::default(), - )) - } - } - }; - - window.with_text_style( - Some(crate::TextStyleRefinement { - color: Some(crate::red()), - line_height: Some(FONT_SIZE.into()), - background_color: Some(crate::white()), - ..Default::default() - }), - render_debug_text, - ) - } - } - - fn paint_mouse_listeners( - &mut self, - hitbox: &Hitbox, - element_state: Option<&mut InteractiveElementState>, - window: &mut Window, - cx: &mut App, - ) { - let is_focused = self - .tracked_focus_handle - .as_ref() - .map(|handle| handle.is_focused(window)) - .unwrap_or(false); - - // If this element can be focused, register a mouse down listener - // that will automatically transfer focus when hitting the element. - // This behavior can be suppressed by using `cx.prevent_default()`. - if let Some(focus_handle) = self.tracked_focus_handle.clone() { - let hitbox = hitbox.clone(); - window.on_mouse_event(move |_: &MouseDownEvent, phase, window, cx| { - if phase == DispatchPhase::Bubble - && hitbox.is_hovered(window) - && !window.default_prevented() - { - window.focus(&focus_handle, cx); - // If there is a parent that is also focusable, prevent it - // from transferring focus because we already did so. - window.prevent_default(); - } - }); - } - - for listener in self.mouse_down_listeners.drain(..) { - let hitbox = hitbox.clone(); - window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| { - listener(event, phase, &hitbox, window, cx); - }) - } - - for listener in self.mouse_up_listeners.drain(..) { - let hitbox = hitbox.clone(); - window.on_mouse_event(move |event: &MouseUpEvent, phase, window, cx| { - listener(event, phase, &hitbox, window, cx); - }) - } - - for listener in self.mouse_pressure_listeners.drain(..) { - let hitbox = hitbox.clone(); - window.on_mouse_event(move |event: &MousePressureEvent, phase, window, cx| { - listener(event, phase, &hitbox, window, cx); - }) - } - - for listener in self.mouse_move_listeners.drain(..) { - let hitbox = hitbox.clone(); - window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| { - listener(event, phase, &hitbox, window, cx); - }) - } - - for listener in self.mouse_exit_listeners.drain(..) { - let hitbox = hitbox.clone(); - window.on_mouse_event(move |event: &MouseExitEvent, phase, window, cx| { - listener(event, phase, &hitbox, window, cx); - }) - } - - for listener in self.scroll_wheel_listeners.drain(..) { - let hitbox = hitbox.clone(); - window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| { - listener(event, phase, &hitbox, window, cx); - }) - } - - for listener in self.pinch_listeners.drain(..) { - let hitbox = hitbox.clone(); - window.on_mouse_event(move |event: &PinchEvent, phase, window, cx| { - listener(event, phase, &hitbox, window, cx); - }) - } - - if self.hover_style.is_some() - || self.base_style.mouse_cursor.is_some() - || cx.active_drag.is_some() && !self.drag_over_styles.is_empty() - { - let hitbox = hitbox.clone(); - let hover_state = self.hover_style.as_ref().and_then(|_| { - element_state - .as_ref() - .and_then(|state| state.hover_state.as_ref()) - .cloned() - }); - let current_view = window.current_view(); - - window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, cx| { - let hovered = hitbox.is_hovered(window); - let was_hovered = hover_state - .as_ref() - .is_some_and(|state| state.borrow().element); - if phase == DispatchPhase::Capture && hovered != was_hovered { - if let Some(hover_state) = &hover_state { - hover_state.borrow_mut().element = hovered; - cx.notify(current_view); - } - } - }); - } - - if let Some(group_hover) = self.group_hover_style.as_ref() { - if let Some(group_hitbox_id) = GroupHitboxes::get(&group_hover.group, cx) { - let hover_state = element_state - .as_ref() - .and_then(|element| element.hover_state.as_ref()) - .cloned(); - let current_view = window.current_view(); - - window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, cx| { - let group_hovered = group_hitbox_id.is_hovered(window); - let was_group_hovered = hover_state - .as_ref() - .is_some_and(|state| state.borrow().group); - if phase == DispatchPhase::Capture && group_hovered != was_group_hovered { - if let Some(hover_state) = &hover_state { - hover_state.borrow_mut().group = group_hovered; - cx.notify(current_view); - } - } - }); - } - } - - let drag_cursor_style = self.base_style.as_ref().mouse_cursor; - - let mut drag_listener = mem::take(&mut self.drag_listener); - let drop_listeners = mem::take(&mut self.drop_listeners); - let click_listeners = mem::take(&mut self.click_listeners); - let aux_click_listeners = mem::take(&mut self.aux_click_listeners); - let can_drop_predicate = mem::take(&mut self.can_drop_predicate); - - if !drop_listeners.is_empty() { - let hitbox = hitbox.clone(); - window.on_mouse_event({ - move |_: &MouseUpEvent, phase, window, cx| { - if let Some(drag) = &cx.active_drag - && phase == DispatchPhase::Bubble - && hitbox.is_hovered(window) - { - let drag_state_type = drag.value.as_ref().type_id(); - for (drop_state_type, listener) in &drop_listeners { - if *drop_state_type == drag_state_type { - let drag = cx - .active_drag - .take() - .expect("checked for type drag state type above"); - - let mut can_drop = true; - if let Some(predicate) = &can_drop_predicate { - can_drop = predicate(drag.value.as_ref(), window, cx); - } - - if can_drop { - listener(drag.value.as_ref(), window, cx); - window.refresh(); - cx.stop_propagation(); - } - } - } - } - } - }); - } - - if let Some(element_state) = element_state { - if !click_listeners.is_empty() - || !aux_click_listeners.is_empty() - || drag_listener.is_some() - { - let pending_mouse_down = element_state - .pending_mouse_down - .get_or_insert_with(Default::default) - .clone(); - - let pending_keyboard_down = element_state - .pending_keyboard_down - .get_or_insert_with(Default::default) - .clone(); - - let clicked_state = element_state - .clicked_state - .get_or_insert_with(Default::default) - .clone(); - - window.on_mouse_event({ - let pending_mouse_down = pending_mouse_down.clone(); - let hitbox = hitbox.clone(); - let has_aux_click_listeners = !aux_click_listeners.is_empty(); - move |event: &MouseDownEvent, phase, window, _cx| { - if phase == DispatchPhase::Bubble - && (event.button == MouseButton::Left || has_aux_click_listeners) - && hitbox.is_hovered(window) - { - *pending_mouse_down.borrow_mut() = Some(event.clone()); - window.refresh(); - } - } - }); - - window.on_mouse_event({ - let pending_mouse_down = pending_mouse_down.clone(); - let hitbox = hitbox.clone(); - move |event: &MouseMoveEvent, phase, window, cx| { - if phase == DispatchPhase::Capture { - return; - } - - let mut pending_mouse_down = pending_mouse_down.borrow_mut(); - if let Some(mouse_down) = pending_mouse_down.clone() - && !cx.has_active_drag() - && (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD - && let Some(listener) = drag_listener.take() - && mouse_down.button == MouseButton::Left - { - *clicked_state.borrow_mut() = ElementClickedState::default(); - let cursor_offset = event.position - hitbox.origin; - let drag = (listener.render)( - listener.value.as_ref(), - cursor_offset, - window, - cx, - ); - let external_payload_source = - listener.external_payload.map(|external_payload| { - let value = listener.value.clone(); - Box::new(move |window: &mut Window, cx: &mut App| { - external_payload(value.as_ref(), window, cx) - }) - as ExternalDragPayloadSource - }); - cx.active_drag = Some(AnyDrag { - view: drag, - value: listener.value, - cursor_offset, - cursor_style: drag_cursor_style, - external_payload_source, - }); - pending_mouse_down.take(); - window.refresh(); - cx.stop_propagation(); - } - } - }); - - if is_focused { - // Record the focus generation at which an enter/space key - // down event happened on this element. The next key up - // event will be mapped to a click event if both of the - // following are true: - // - no other key events happen in between - // - the focus generation is the same (implying focus did not move) - // - // This design avoids an ABA problem that happens if you - // store the focus handle that registered the keypress. - window.on_key_event({ - let pending_keyboard_down = pending_keyboard_down.clone(); - move |event: &KeyDownEvent, phase, window, _cx| { - if phase.bubble() && !window.default_prevented() { - let stroke = &event.keystroke; - let is_activation_key = (stroke.key.eq("enter") - || stroke.key.eq("space")) - && !stroke.modifiers.modified(); - *pending_keyboard_down.borrow_mut() = - is_activation_key.then_some(window.focus_generation); - } - } - }); - - // Press enter, space to trigger click, when the element is focused. - window.on_key_event({ - let click_listeners = click_listeners.clone(); - let hitbox = hitbox.clone(); - move |event: &KeyUpEvent, phase, window, cx| { - if phase.bubble() && !window.default_prevented() { - let stroke = &event.keystroke; - let keyboard_button = if stroke.key.eq("enter") { - Some(KeyboardButton::Enter) - } else if stroke.key.eq("space") { - Some(KeyboardButton::Space) - } else { - None - }; - - if let Some(button) = keyboard_button - && !stroke.modifiers.modified() - { - let pending = - std::mem::take(&mut *pending_keyboard_down.borrow_mut()); - if pending != Some(window.focus_generation) { - return; - } - - let click_event = ClickEvent::Keyboard(KeyboardClickEvent { - button, - bounds: hitbox.bounds, - }); - - for listener in &click_listeners { - listener(&click_event, window, cx); - } - } else { - // Releasing any other key mid-press means - // this isn't a clean activation, so cancel - // the pending keydown. - *pending_keyboard_down.borrow_mut() = None; - } - } - } - }); - } - - window.on_mouse_event({ - let mut captured_mouse_down = None; - let hitbox = hitbox.clone(); - move |event: &MouseUpEvent, phase, window, cx| match phase { - // Clear the pending mouse down during the capture phase, - // so that it happens even if another event handler stops - // propagation. - DispatchPhase::Capture => { - let mut pending_mouse_down = pending_mouse_down.borrow_mut(); - if pending_mouse_down.is_some() && hitbox.is_hovered(window) { - captured_mouse_down = pending_mouse_down.take(); - window.refresh(); - } else if pending_mouse_down.is_some() { - // Clear the pending mouse down event (without firing click handlers) - // if the hitbox is not being hovered. - // This avoids dragging elements that changed their position - // immediately after being clicked. - // See https://github.com/zed-industries/zed/issues/24600 for more details - pending_mouse_down.take(); - window.refresh(); - } - } - // Fire click handlers during the bubble phase. - DispatchPhase::Bubble => { - if let Some(mouse_down) = captured_mouse_down.take() { - let btn = mouse_down.button; - - let mouse_click = ClickEvent::Mouse(MouseClickEvent { - down: mouse_down, - up: event.clone(), - }); - - match btn { - MouseButton::Left => { - for listener in &click_listeners { - listener(&mouse_click, window, cx); - } - } - _ => { - for listener in &aux_click_listeners { - listener(&mouse_click, window, cx); - } - } - } - } - } - } - }); - } - - if let Some(hover_listener) = self.hover_listener.take() { - let was_hovered = element_state - .hover_listener_state - .get_or_insert_with(Default::default) - .clone(); - let has_mouse_down = element_state - .pending_mouse_down - .get_or_insert_with(Default::default) - .clone(); - let hover_listener = Rc::new(hover_listener); - let hover_listener_state = was_hovered.clone(); - let update_hover = move |is_hovered: bool, window: &mut Window, cx: &mut App| { - let mut was_hovered = hover_listener_state.borrow_mut(); - if is_hovered != *was_hovered { - *was_hovered = is_hovered; - drop(was_hovered); - hover_listener(&is_hovered, window, cx); - } - }; - let hover_listener_mode = self.hover_listener_mode; - - if has_mouse_down.borrow().is_none() { - let is_hovered = - !cx.has_active_drag() && hover_listener_mode.is_hovered(hitbox, window); - if is_hovered != *was_hovered.borrow() { - let update_hover = update_hover.clone(); - window.defer(cx, move |window, cx| { - update_hover(is_hovered, window, cx); - }); - } - } - - window.on_mouse_event({ - let update_hover = update_hover.clone(); - let hitbox = hitbox.clone(); - move |_: &MouseMoveEvent, phase, window, cx| { - if phase == DispatchPhase::Bubble { - let is_hovered = has_mouse_down.borrow().is_none() - && !cx.has_active_drag() - && hover_listener_mode.is_hovered(&hitbox, window); - update_hover(is_hovered, window, cx); - } - } - }); - - // The pointer can leave the window without a final MouseMove, so also - // clear hover on MouseExited. - window.on_mouse_event(move |_: &MouseExitEvent, phase, window, cx| { - if phase == DispatchPhase::Bubble { - update_hover(false, window, cx); - } - }); - } - - if let Some(tooltip_builder) = self.tooltip_builder.take() { - let active_tooltip = element_state - .active_tooltip - .get_or_insert_with(Default::default) - .clone(); - let pending_mouse_down = element_state - .pending_mouse_down - .get_or_insert_with(Default::default) - .clone(); - - let tooltip_is_hoverable = tooltip_builder.hoverable; - let build_tooltip = Rc::new(move |window: &mut Window, cx: &mut App| { - Some(((tooltip_builder.build)(window, cx), tooltip_is_hoverable)) - }); - // Use bounds instead of testing hitbox since this is called during prepaint. - let check_is_hovered_during_prepaint = Rc::new({ - let pending_mouse_down = pending_mouse_down.clone(); - let source_bounds = hitbox.bounds; - move |window: &Window| { - !window.last_input_was_keyboard() - && pending_mouse_down.borrow().is_none() - && source_bounds.contains(&window.mouse_position()) - } - }); - let check_is_hovered = Rc::new({ - let hitbox = hitbox.clone(); - move |window: &Window| { - pending_mouse_down.borrow().is_none() && hitbox.is_hovered(window) - } - }); - register_tooltip_mouse_handlers( - &active_tooltip, - self.tooltip_id, - build_tooltip, - check_is_hovered, - check_is_hovered_during_prepaint, - self.tooltip_show_delay, - window, - ); - } - - // We unconditionally bind both the mouse up and mouse down active state handlers - // Because we might not get a chance to render a frame before the mouse up event arrives. - let active_state = element_state - .clicked_state - .get_or_insert_with(Default::default) - .clone(); - - { - let active_state = active_state.clone(); - window.on_mouse_event(move |_: &MouseUpEvent, phase, window, _cx| { - if phase == DispatchPhase::Capture && active_state.borrow().is_clicked() { - *active_state.borrow_mut() = ElementClickedState::default(); - window.refresh(); - } - }); - } - - { - let active_group_hitbox = self - .group_active_style - .as_ref() - .and_then(|group_active| GroupHitboxes::get(&group_active.group, cx)); - let hitbox = hitbox.clone(); - window.on_mouse_event(move |_: &MouseDownEvent, phase, window, _cx| { - if phase == DispatchPhase::Bubble && !window.default_prevented() { - let group_hovered = active_group_hitbox - .is_some_and(|group_hitbox_id| group_hitbox_id.is_hovered(window)); - let element_hovered = hitbox.is_hovered(window); - if group_hovered || element_hovered { - *active_state.borrow_mut() = ElementClickedState { - group: group_hovered, - element: element_hovered, - }; - window.refresh(); - } - } - }); - } - } - } - - fn paint_keyboard_listeners(&mut self, window: &mut Window, _cx: &mut App) { - let key_down_listeners = mem::take(&mut self.key_down_listeners); - let key_up_listeners = mem::take(&mut self.key_up_listeners); - let modifiers_changed_listeners = mem::take(&mut self.modifiers_changed_listeners); - let action_listeners = mem::take(&mut self.action_listeners); - if let Some(context) = self.key_context.clone() { - window.set_key_context(context); - } - - for listener in key_down_listeners { - window.on_key_event(move |event: &KeyDownEvent, phase, window, cx| { - listener(event, phase, window, cx); - }) - } - - for listener in key_up_listeners { - window.on_key_event(move |event: &KeyUpEvent, phase, window, cx| { - listener(event, phase, window, cx); - }) - } - - for listener in modifiers_changed_listeners { - window.on_modifiers_changed(move |event: &ModifiersChangedEvent, window, cx| { - listener(event, window, cx); - }) - } - - for (action_type, listener) in action_listeners { - window.on_action(action_type, listener) - } - } - - fn paint_hover_group_handler(&self, window: &mut Window, cx: &mut App) { - let group_hitbox = self - .group_hover_style - .as_ref() - .and_then(|group_hover| GroupHitboxes::get(&group_hover.group, cx)); - - if let Some(group_hitbox) = group_hitbox { - let was_hovered = group_hitbox.is_hovered(window); - let current_view = window.current_view(); - window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, cx| { - let hovered = group_hitbox.is_hovered(window); - if phase == DispatchPhase::Capture && hovered != was_hovered { - cx.notify(current_view); - } - }); - } - } - - fn paint_scroll_listener( - &self, - hitbox: &Hitbox, - style: &Style, - window: &mut Window, - _cx: &mut App, - ) { - if let Some(scroll_offset) = self.scroll_offset.clone() { - let ongoing_scroll = self.ongoing_scroll.clone(); - let overflow = style.overflow; - let allow_concurrent_scroll = style.allow_concurrent_scroll; - let restrict_scroll_to_axis = style.restrict_scroll_to_axis; - let line_height = window.line_height(); - let hitbox = hitbox.clone(); - let current_view = window.current_view(); - window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| { - if phase == DispatchPhase::Bubble && hitbox.should_handle_scroll(window) { - let mut scroll_offset = scroll_offset.borrow_mut(); - let old_scroll_offset = *scroll_offset; - let mut delta = event.delta.pixel_delta(line_height); - - if restrict_scroll_to_axis - && event.delta.precise() - && let Some(ongoing_scroll) = &ongoing_scroll - { - ongoing_scroll - .borrow_mut() - .filter(&mut delta, event.touch_phase); - } - - let mut delta_x = match overflow.x { - Overflow::Scroll if !delta.x.is_zero() => delta.x, - Overflow::Scroll - if !restrict_scroll_to_axis && overflow.y != Overflow::Scroll => - { - delta.y - } - _ => Pixels::ZERO, - }; - let mut delta_y = match overflow.y { - Overflow::Scroll if !delta.y.is_zero() => delta.y, - Overflow::Scroll - if !restrict_scroll_to_axis && overflow.x != Overflow::Scroll => - { - delta.x - } - _ => Pixels::ZERO, - }; - if !allow_concurrent_scroll && !delta_x.is_zero() && !delta_y.is_zero() { - if delta_x.abs() > delta_y.abs() { - delta_y = Pixels::ZERO; - } else { - delta_x = Pixels::ZERO; - } - } - scroll_offset.y += delta_y; - scroll_offset.x += delta_x; - if *scroll_offset != old_scroll_offset { - cx.notify(current_view); - } - } - }); - } - } - - /// Compute the visual style for this element, based on the current bounds and the element's state. - pub fn compute_style( - &self, - global_id: Option<&GlobalElementId>, - hitbox: Option<&Hitbox>, - window: &mut Window, - cx: &mut App, - ) -> Style { - window.with_optional_element_state(global_id, |element_state, window| { - let mut element_state = - element_state.map(|element_state| element_state.unwrap_or_default()); - let style = self.compute_style_internal(hitbox, element_state.as_mut(), window, cx); - (style, element_state) - }) - } - - /// Called from internal methods that have already called with_element_state. - fn compute_style_internal( - &self, - hitbox: Option<&Hitbox>, - element_state: Option<&mut InteractiveElementState>, - window: &mut Window, - cx: &mut App, - ) -> Style { - let mut style = Style::default(); - style.refine(&self.base_style); - - if let Some(focus_handle) = self.tracked_focus_handle.as_ref() { - if let Some(in_focus_style) = self.in_focus_style.as_ref() - && focus_handle.within_focused(window, cx) - { - style.refine(in_focus_style); - } - - if let Some(focus_style) = self.focus_style.as_ref() - && focus_handle.is_focused(window) - { - style.refine(focus_style); - } - - if let Some(focus_visible_style) = self.focus_visible_style.as_ref() - && focus_handle.is_focused(window) - && window.last_input_was_keyboard() - { - style.refine(focus_visible_style); - } - } - - if !cx.has_active_drag() { - if let Some(group_hover) = self.group_hover_style.as_ref() { - let is_group_hovered = - if let Some(group_hitbox_id) = GroupHitboxes::get(&group_hover.group, cx) { - !window.last_input_was_touch() && group_hitbox_id.is_hovered(window) - } else if let Some(element_state) = element_state.as_ref() { - !window.last_input_was_touch() - && element_state - .hover_state - .as_ref() - .map(|state| state.borrow().group) - .unwrap_or(false) - } else { - false - }; - - if is_group_hovered { - style.refine(&group_hover.style); - } - } - - if let Some(hover_style) = self.hover_style.as_ref() { - let is_hovered = if let Some(hitbox) = hitbox { - !window.last_input_was_touch() && hitbox.is_hovered(window) - } else if let Some(element_state) = element_state.as_ref() { - !window.last_input_was_touch() - && element_state - .hover_state - .as_ref() - .map(|state| state.borrow().element) - .unwrap_or(false) - } else { - false - }; - - if is_hovered { - style.refine(hover_style); - } - } - } - - if let Some(hitbox) = hitbox { - if let Some(drag) = cx.active_drag.take() { - let mut can_drop = true; - if let Some(can_drop_predicate) = &self.can_drop_predicate { - can_drop = can_drop_predicate(drag.value.as_ref(), window, cx); - } - - if can_drop { - for (state_type, group_drag_style) in &self.group_drag_over_styles { - if let Some(group_hitbox_id) = - GroupHitboxes::get(&group_drag_style.group, cx) - && *state_type == drag.value.as_ref().type_id() - && group_hitbox_id.is_hovered(window) - { - style.refine(&group_drag_style.style); - } - } - - for (state_type, build_drag_over_style) in &self.drag_over_styles { - if *state_type == drag.value.as_ref().type_id() && hitbox.is_hovered(window) - { - style.refine(&build_drag_over_style(drag.value.as_ref(), window, cx)); - } - } - } - - style.mouse_cursor = drag.cursor_style; - cx.active_drag = Some(drag); - } - } - - if let Some(element_state) = element_state { - let clicked_state = element_state - .clicked_state - .get_or_insert_with(Default::default) - .borrow(); - if clicked_state.group - && let Some(group) = self.group_active_style.as_ref() - { - style.refine(&group.style) - } - - if let Some(active_style) = self.active_style.as_ref() - && clicked_state.element - { - style.refine(active_style) - } - } - - style - } - - pub(crate) fn write_a11y_info(&self, node: &mut accesskit::Node) { - if let Some(id) = &self.aria.author_id { - node.set_author_id(id.to_string()); - } - if let Some(label) = &self.aria.label { - node.set_label(label.to_string()); - } - if let Some(description) = &self.aria.description { - node.set_description(description.to_string()); - } - if let Some(keyshortcuts) = &self.aria.keyshortcuts { - node.set_keyboard_shortcut(keyshortcuts.to_string()); - } - if let Some(selected) = self.aria.selected { - node.set_selected(selected); - } - if let Some(current) = self.aria.current { - node.set_aria_current(current); - } - if let Some(expanded) = self.aria.expanded { - node.set_expanded(expanded); - } - if let Some(toggled) = self.aria.toggled { - node.set_toggled(toggled); - } - if let Some(value) = self.aria.numeric_value { - node.set_numeric_value(value); - } - if let Some(value) = self.aria.min_numeric_value { - node.set_min_numeric_value(value); - } - if let Some(value) = self.aria.max_numeric_value { - node.set_max_numeric_value(value); - } - if let Some(step) = self.aria.numeric_value_step { - node.set_numeric_value_step(step); - } - if let Some(value) = &self.aria.value { - node.set_value(value.to_string()); - } - if let Some(placeholder) = &self.aria.placeholder { - node.set_placeholder(placeholder.to_string()); - } - if let Some(orientation) = self.aria.orientation { - node.set_orientation(orientation); - } - if let Some(level) = self.aria.level { - node.set_level(level); - } - if let Some(position) = self.aria.position_in_set { - node.set_position_in_set(position); - } - if let Some(size) = self.aria.size_of_set { - node.set_size_of_set(size); - } - if let Some(index) = self.aria.row_index { - node.set_row_index(index); - } - if let Some(index) = self.aria.column_index { - node.set_column_index(index); - } - if let Some(count) = self.aria.row_count { - node.set_row_count(count); - } - if let Some(count) = self.aria.column_count { - node.set_column_count(count); - } - if !self.click_listeners.is_empty() { - node.add_action(accesskit::Action::Click); - } - if self.tracked_focus_handle.is_some() || self.focusable { - node.add_action(accesskit::Action::Focus); - } - for (action, _) in &self.a11y_action_listeners { - node.add_action(*action); - } - } -} - -/// The per-frame state of an interactive element. Used for tracking stateful interactions like clicks -/// and scroll offsets. -#[derive(Default)] -pub struct InteractiveElementState { - pub(crate) focus_handle: Option, - pub(crate) clicked_state: Option>>, - pub(crate) hover_state: Option>>, - pub(crate) hover_listener_state: Option>>, - pub(crate) pending_mouse_down: Option>>>, - /// Set to the window's [`focus_generation`](crate::Window::focus_generation) - /// when an Enter/Space keydown is received while this element is focused, - /// recording that we are waiting for the matching keyup to fire a keyboard - /// click. On keyup the click only fires if the stored generation still - /// matches the window's current one, i.e. focus never moved during the - /// press (mirroring the browser clearing a control's pressed state on - /// blur). `None` means no activation key is pending. - pub(crate) pending_keyboard_down: Option>>>, - pub(crate) scroll_offset: Option>>>, - ongoing_scroll: Option>>, - pub(crate) active_tooltip: Option>>>, -} - -/// Whether or not the element or a group that contains it is clicked by the mouse. -#[derive(Copy, Clone, Default, Eq, PartialEq)] -pub struct ElementClickedState { - /// True if this element's group has been clicked, false otherwise - pub group: bool, - - /// True if this element has been clicked, false otherwise - pub element: bool, -} - -impl ElementClickedState { - fn is_clicked(&self) -> bool { - self.group || self.element - } -} - -/// Whether or not the element or a group that contains it is hovered. -#[derive(Copy, Clone, Default, Eq, PartialEq)] -pub struct ElementHoverState { - /// True if this element's group is hovered, false otherwise - pub group: bool, - - /// True if this element is hovered, false otherwise - pub element: bool, -} - -pub(crate) enum ActiveTooltip { - /// Currently delaying before showing the tooltip. - WaitingForShow { _task: Task<()> }, - /// Tooltip is visible, element was hovered or for hoverable tooltips, the tooltip was hovered. - Visible { - tooltip: AnyTooltip, - is_hoverable: bool, - }, - /// Tooltip is visible and hoverable, but the mouse is no longer hovering. Currently delaying - /// before hiding it. - WaitingForHide { - tooltip: AnyTooltip, - _task: Task<()>, - }, -} - -pub(crate) fn clear_active_tooltip( - active_tooltip: &Rc>>, - window: &mut Window, -) { - match active_tooltip.borrow_mut().take() { - None => {} - Some(ActiveTooltip::WaitingForShow { .. }) => {} - Some(ActiveTooltip::Visible { .. }) => window.refresh(), - Some(ActiveTooltip::WaitingForHide { .. }) => window.refresh(), - } -} - -pub(crate) fn clear_active_tooltip_if_not_hoverable( - active_tooltip: &Rc>>, - window: &mut Window, -) { - let should_clear = match active_tooltip.borrow().as_ref() { - None => false, - Some(ActiveTooltip::WaitingForShow { .. }) => false, - Some(ActiveTooltip::Visible { is_hoverable, .. }) => !is_hoverable, - Some(ActiveTooltip::WaitingForHide { .. }) => false, - }; - if should_clear { - active_tooltip.borrow_mut().take(); - window.refresh(); - } -} - -pub(crate) fn set_tooltip_on_window( - active_tooltip: &Rc>>, - window: &mut Window, -) -> Option { - let tooltip = match active_tooltip.borrow().as_ref() { - None => return None, - Some(ActiveTooltip::WaitingForShow { .. }) => return None, - Some(ActiveTooltip::Visible { tooltip, .. }) => tooltip.clone(), - Some(ActiveTooltip::WaitingForHide { tooltip, .. }) => tooltip.clone(), - }; - Some(window.set_tooltip(tooltip)) -} - -pub(crate) fn register_tooltip_mouse_handlers( - active_tooltip: &Rc>>, - tooltip_id: Option, - build_tooltip: Rc Option<(AnyView, bool)>>, - check_is_hovered: Rc bool>, - check_is_hovered_during_prepaint: Rc bool>, - show_delay: Option, - window: &mut Window, -) { - let current_view = window.current_view(); - let show_delay = show_delay.unwrap_or(DEFAULT_TOOLTIP_SHOW_DELAY); - - window.on_mouse_event({ - let active_tooltip = active_tooltip.clone(); - let build_tooltip = build_tooltip.clone(); - let check_is_hovered = check_is_hovered.clone(); - move |_: &MouseMoveEvent, phase, window, cx| { - handle_tooltip_mouse_move( - &active_tooltip, - &build_tooltip, - &check_is_hovered, - &check_is_hovered_during_prepaint, - tooltip_id, - current_view, - phase, - show_delay, - window, - cx, - ) - } - }); - - window.on_mouse_event({ - let active_tooltip = active_tooltip.clone(); - move |_: &MouseDownEvent, _phase, window: &mut Window, _cx| { - if !tooltip_id.is_some_and(|tooltip_id| tooltip_id.is_hovered(window)) { - clear_active_tooltip_if_not_hoverable(&active_tooltip, window); - } - } - }); - - window.on_mouse_event({ - let active_tooltip = active_tooltip.clone(); - move |_: &ScrollWheelEvent, _phase, window: &mut Window, _cx| { - if !tooltip_id.is_some_and(|tooltip_id| tooltip_id.is_hovered(window)) { - clear_active_tooltip_if_not_hoverable(&active_tooltip, window); - } - } - }); -} - -/// Handles displaying tooltips when an element is hovered. -/// -/// The mouse hovering logic also relies on being called from window prepaint in order to handle the -/// case where the element the tooltip is on is not rendered - in that case its mouse listeners are -/// also not registered. During window prepaint, the hitbox information is not available, so -/// `check_is_hovered_during_prepaint` is used which bases the check off of the absolute bounds of -/// the element. -/// -/// TODO: There's a minor bug due to the use of absolute bounds while checking during prepaint - it -/// does not know if the hitbox is occluded. In the case where a tooltip gets displayed and then -/// gets occluded after display, it will stick around until the mouse exits the hover bounds. -fn handle_tooltip_mouse_move( - active_tooltip: &Rc>>, - build_tooltip: &Rc Option<(AnyView, bool)>>, - check_is_hovered: &Rc bool>, - check_is_hovered_during_prepaint: &Rc bool>, - tooltip_id: Option, - current_view: EntityId, - phase: DispatchPhase, - show_delay: Duration, - window: &mut Window, - cx: &mut App, -) { - // Separates logic for what mutation should occur from applying it, to avoid overlapping - // RefCell borrows. - enum Action { - None, - CancelShow, - ScheduleShow, - CheckVisible, - } - - let action = match active_tooltip.borrow().as_ref() { - None => { - let is_hovered = check_is_hovered(window); - if is_hovered && phase.bubble() { - Action::ScheduleShow - } else { - Action::None - } - } - Some(ActiveTooltip::WaitingForShow { .. }) => { - let is_hovered = check_is_hovered(window); - if is_hovered { - Action::None - } else { - Action::CancelShow - } - } - Some(ActiveTooltip::Visible { is_hoverable, .. }) => { - if phase.capture() - && !check_is_hovered(window) - && (!*is_hoverable - || !tooltip_id.is_some_and(|tooltip_id| tooltip_id.is_hovered(window))) - { - Action::CheckVisible - } else { - Action::None - } - } - Some(ActiveTooltip::WaitingForHide { .. }) => { - if phase.capture() - && (check_is_hovered(window) - || tooltip_id.is_some_and(|tooltip_id| tooltip_id.is_hovered(window))) - { - Action::CheckVisible - } else { - Action::None - } - } - }; - - match action { - Action::None => {} - Action::CancelShow => { - // Cancel waiting to show tooltip when it is no longer hovered. - active_tooltip.borrow_mut().take(); - } - Action::ScheduleShow => { - let delayed_show_task = window.spawn(cx, { - let weak_active_tooltip = Rc::downgrade(active_tooltip); - let build_tooltip = build_tooltip.clone(); - let check_is_hovered_during_prepaint = check_is_hovered_during_prepaint.clone(); - async move |cx| { - cx.background_executor().timer(show_delay).await; - let Some(active_tooltip) = weak_active_tooltip.upgrade() else { - return; - }; - cx.update(|window, cx| { - let new_tooltip = - build_tooltip(window, cx).map(|(view, tooltip_is_hoverable)| { - let weak_active_tooltip = Rc::downgrade(&active_tooltip); - ActiveTooltip::Visible { - tooltip: AnyTooltip { - view, - mouse_position: window.mouse_position(), - check_visible_and_update: Rc::new( - move |tooltip_bounds, window, cx| { - let Some(active_tooltip) = - weak_active_tooltip.upgrade() - else { - return false; - }; - handle_tooltip_check_visible_and_update( - &active_tooltip, - tooltip_is_hoverable, - &check_is_hovered_during_prepaint, - tooltip_bounds, - window, - cx, - ) - }, - ), - }, - is_hoverable: tooltip_is_hoverable, - } - }); - *active_tooltip.borrow_mut() = new_tooltip; - window.refresh(); - }) - .ok(); - } - }); - active_tooltip - .borrow_mut() - .replace(ActiveTooltip::WaitingForShow { - _task: delayed_show_task, - }); - } - Action::CheckVisible => cx.notify(current_view), - } -} - -/// Returns a callback which will be called by window prepaint to update tooltip visibility. The -/// purpose of doing this logic here instead of the mouse move handler is that the mouse move -/// handler won't get called when the element is not painted (e.g. via use of `visible_on_hover`). -fn handle_tooltip_check_visible_and_update( - active_tooltip: &Rc>>, - tooltip_is_hoverable: bool, - check_is_hovered: &Rc bool>, - tooltip_bounds: Bounds, - window: &mut Window, - cx: &mut App, -) -> bool { - // Separates logic for what mutation should occur from applying it, to avoid overlapping RefCell - // borrows. - enum Action { - None, - Hide, - ScheduleHide(AnyTooltip), - CancelHide(AnyTooltip), - } - - let is_hovered = check_is_hovered(window) - || (tooltip_is_hoverable && tooltip_bounds.contains(&window.mouse_position())); - let action = match active_tooltip.borrow().as_ref() { - Some(ActiveTooltip::Visible { tooltip, .. }) => { - if is_hovered { - Action::None - } else { - if tooltip_is_hoverable { - Action::ScheduleHide(tooltip.clone()) - } else { - Action::Hide - } - } - } - Some(ActiveTooltip::WaitingForHide { tooltip, .. }) => { - if is_hovered { - Action::CancelHide(tooltip.clone()) - } else { - Action::None - } - } - None | Some(ActiveTooltip::WaitingForShow { .. }) => Action::None, - }; - - match action { - Action::None => {} - Action::Hide => clear_active_tooltip(active_tooltip, window), - Action::ScheduleHide(tooltip) => { - let delayed_hide_task = window.spawn(cx, { - let weak_active_tooltip = Rc::downgrade(active_tooltip); - async move |cx| { - cx.background_executor() - .timer(HOVERABLE_TOOLTIP_HIDE_DELAY) - .await; - let Some(active_tooltip) = weak_active_tooltip.upgrade() else { - return; - }; - if active_tooltip.borrow_mut().take().is_some() { - cx.update(|window, _cx| window.refresh()).ok(); - } - } - }); - active_tooltip - .borrow_mut() - .replace(ActiveTooltip::WaitingForHide { - tooltip, - _task: delayed_hide_task, - }); - } - Action::CancelHide(tooltip) => { - // Cancel waiting to hide tooltip when it becomes hovered. - active_tooltip.borrow_mut().replace(ActiveTooltip::Visible { - tooltip, - is_hoverable: true, - }); - } - } - - active_tooltip.borrow().is_some() -} - -#[derive(Default)] -pub(crate) struct GroupHitboxes(HashMap>); - -impl Global for GroupHitboxes {} - -impl GroupHitboxes { - pub fn get(name: &SharedString, cx: &mut App) -> Option { - cx.default_global::() - .0 - .get(name) - .and_then(|bounds_stack| bounds_stack.last()) - .cloned() - } - - pub fn push(name: SharedString, hitbox_id: HitboxId, cx: &mut App) { - cx.default_global::() - .0 - .entry(name) - .or_default() - .push(hitbox_id); - } - - pub fn pop(name: &SharedString, cx: &mut App) { - cx.default_global::().0.get_mut(name).unwrap().pop(); - } -} - -/// A wrapper around an element that can store state, produced after assigning an ElementId. -pub struct Stateful { - pub(crate) element: E, -} - -impl Styled for Stateful -where - E: Styled, -{ - fn style(&mut self) -> &mut StyleRefinement { - self.element.style() - } -} - -impl StatefulInteractiveElement for Stateful -where - E: Element, - Self: InteractiveElement, -{ -} - -impl InteractiveElement for Stateful -where - E: InteractiveElement, -{ - fn interactivity(&mut self) -> &mut Interactivity { - self.element.interactivity() - } -} - -impl Element for Stateful -where - E: Element, -{ - type RequestLayoutState = E::RequestLayoutState; - type PrepaintState = E::PrepaintState; - - fn id(&self) -> Option { - self.element.id() - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - self.element.source_location() - } - - fn a11y_role(&self) -> Option { - self.element.a11y_role() - } - - fn write_a11y_info(&self, node: &mut accesskit::Node) { - self.element.write_a11y_info(node); - } - - fn a11y_synthetic_children( - &mut self, - prepaint: &mut Self::PrepaintState, - builder: &mut crate::A11ySubtreeBuilder, - ) { - self.element.a11y_synthetic_children(prepaint, builder); - } - - fn request_layout( - &mut self, - id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - self.element.request_layout(id, inspector_id, window, cx) - } - - fn prepaint( - &mut self, - id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - state: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> E::PrepaintState { - self.element - .prepaint(id, inspector_id, bounds, state, window, cx) - } - - fn paint( - &mut self, - id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - request_layout: &mut Self::RequestLayoutState, - prepaint: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - self.element.paint( - id, - inspector_id, - bounds, - request_layout, - prepaint, - window, - cx, - ); - } -} - -impl IntoElement for Stateful -where - E: Element, -{ - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -impl ParentElement for Stateful -where - E: ParentElement, -{ - fn extend(&mut self, elements: impl IntoIterator) { - self.element.extend(elements) - } -} - -/// Represents an element that can be scrolled *to* in its parent element. -/// Contrary to [ScrollHandle::scroll_to_active_item], an anchored element does not have to be an immediate child of the parent. -#[derive(Clone)] -pub struct ScrollAnchor { - handle: ScrollHandle, - last_origin: Rc>>, -} - -impl ScrollAnchor { - /// Creates a [ScrollAnchor] associated with a given [ScrollHandle]. - pub fn for_handle(handle: ScrollHandle) -> Self { - Self { - handle, - last_origin: Default::default(), - } - } - /// Request scroll to this item on the next frame. - pub fn scroll_to(&self, window: &mut Window, _cx: &mut App) { - let this = self.clone(); - - window.on_next_frame(move |_, _| { - let viewport_bounds = this.handle.bounds(); - let self_bounds = *this.last_origin.borrow(); - this.handle.set_offset(viewport_bounds.origin - self_bounds); - }); - } -} - -#[derive(Default, Debug)] -struct ScrollHandleState { - offset: Rc>>, - ongoing_scroll: Rc>, - bounds: Bounds, - max_offset: Point, - child_bounds: Vec>, - scroll_to_bottom: bool, - overflow: Point, - active_item: Option, -} - -#[derive(Default, Debug, Clone, Copy)] -struct ScrollActiveItem { - index: usize, - strategy: ScrollStrategy, -} - -#[derive(Default, Debug, Clone, Copy)] -enum ScrollStrategy { - #[default] - FirstVisible, - Top, -} - -/// A handle to the scrollable aspects of an element. -/// Used for accessing scroll state, like the current scroll offset, -/// and for mutating the scroll state, like scrolling to a specific child. -#[derive(Clone, Debug)] -pub struct ScrollHandle(Rc>); - -impl Default for ScrollHandle { - fn default() -> Self { - Self::new() - } -} - -impl ScrollHandle { - /// Construct a new scroll handle. - pub fn new() -> Self { - Self(Rc::default()) - } - - /// Get the current scroll offset. - pub fn offset(&self) -> Point { - *self.0.borrow().offset.borrow() - } - - /// Get the maximum scroll offset. - pub fn max_offset(&self) -> Point { - self.0.borrow().max_offset - } - - /// Get the top child that's scrolled into view. - pub fn top_item(&self) -> usize { - let state = self.0.borrow(); - let top = state.bounds.top() - state.offset.borrow().y; - - match state.child_bounds.binary_search_by(|bounds| { - if top < bounds.top() { - Ordering::Greater - } else if top > bounds.bottom() { - Ordering::Less - } else { - Ordering::Equal - } - }) { - Ok(ix) => ix, - Err(ix) => ix.min(state.child_bounds.len().saturating_sub(1)), - } - } - - /// Get the bottom child that's scrolled into view. - pub fn bottom_item(&self) -> usize { - let state = self.0.borrow(); - let bottom = state.bounds.bottom() - state.offset.borrow().y; - - match state.child_bounds.binary_search_by(|bounds| { - if bottom < bounds.top() { - Ordering::Greater - } else if bottom > bounds.bottom() { - Ordering::Less - } else { - Ordering::Equal - } - }) { - Ok(ix) => ix, - Err(ix) => ix.min(state.child_bounds.len().saturating_sub(1)), - } - } - - /// Return the bounds into which this child is painted - pub fn bounds(&self) -> Bounds { - self.0.borrow().bounds - } - - /// Get the bounds for a specific child. - pub fn bounds_for_item(&self, ix: usize) -> Option> { - self.0.borrow().child_bounds.get(ix).cloned() - } - - /// Update [ScrollHandleState]'s active item for scrolling to in prepaint - pub fn scroll_to_item(&self, ix: usize) { - let mut state = self.0.borrow_mut(); - state.active_item = Some(ScrollActiveItem { - index: ix, - strategy: ScrollStrategy::default(), - }); - } - - /// Update [ScrollHandleState]'s active item for scrolling to in prepaint - /// This scrolls the minimal amount to ensure that the child is the first visible element - pub fn scroll_to_top_of_item(&self, ix: usize) { - let mut state = self.0.borrow_mut(); - state.active_item = Some(ScrollActiveItem { - index: ix, - strategy: ScrollStrategy::Top, - }); - } - - /// Scrolls the minimal amount to either ensure that the child is - /// fully visible or the top element of the view depends on the - /// scroll strategy - fn scroll_to_active_item(&self) { - let mut state = self.0.borrow_mut(); - - let Some(active_item) = state.active_item else { - return; - }; - - let active_item = match state.child_bounds.get(active_item.index) { - Some(bounds) => { - let mut scroll_offset = state.offset.borrow_mut(); - - match active_item.strategy { - ScrollStrategy::FirstVisible => { - if state.overflow.y == Overflow::Scroll { - let child_height = bounds.size.height; - let viewport_height = state.bounds.size.height; - if child_height > viewport_height { - scroll_offset.y = state.bounds.top() - bounds.top(); - } else if bounds.top() + scroll_offset.y < state.bounds.top() { - scroll_offset.y = state.bounds.top() - bounds.top(); - } else if bounds.bottom() + scroll_offset.y > state.bounds.bottom() { - scroll_offset.y = state.bounds.bottom() - bounds.bottom(); - } - } - } - ScrollStrategy::Top => { - scroll_offset.y = state.bounds.top() - bounds.top(); - } - } - - if state.overflow.x == Overflow::Scroll { - let child_width = bounds.size.width; - let viewport_width = state.bounds.size.width; - if child_width > viewport_width { - scroll_offset.x = state.bounds.left() - bounds.left(); - } else if bounds.left() + scroll_offset.x < state.bounds.left() { - scroll_offset.x = state.bounds.left() - bounds.left(); - } else if bounds.right() + scroll_offset.x > state.bounds.right() { - scroll_offset.x = state.bounds.right() - bounds.right(); - } - } - None - } - None => Some(active_item), - }; - state.active_item = active_item; - } - - /// Scrolls to the bottom. - pub fn scroll_to_bottom(&self) { - let mut state = self.0.borrow_mut(); - state.scroll_to_bottom = true; - } - - /// Set the offset explicitly. The offset is the distance from the top left of the - /// parent container to the top left of the first child. - /// As you scroll further down the offset becomes more negative. - pub fn set_offset(&self, mut position: Point) { - let state = self.0.borrow(); - *state.offset.borrow_mut() = position; - } - - /// Get the logical scroll top, based on a child index and a pixel offset. - pub fn logical_scroll_top(&self) -> (usize, Pixels) { - let ix = self.top_item(); - let state = self.0.borrow(); - - if let Some(child_bounds) = state.child_bounds.get(ix) { - ( - ix, - child_bounds.top() + state.offset.borrow().y - state.bounds.top(), - ) - } else { - (ix, px(0.)) - } - } - - /// Get the logical scroll bottom, based on a child index and a pixel offset. - pub fn logical_scroll_bottom(&self) -> (usize, Pixels) { - let ix = self.bottom_item(); - let state = self.0.borrow(); - - if let Some(child_bounds) = state.child_bounds.get(ix) { - ( - ix, - child_bounds.bottom() + state.offset.borrow().y - state.bounds.bottom(), - ) - } else { - (ix, px(0.)) - } - } - - /// Get the count of children for scrollable item. - pub fn children_count(&self) -> usize { - self.0.borrow().child_bounds.len() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - canvas, util::FluentBuilder as _, AnyWindowHandle, AppContext as _, Context, InputEvent, - Keystroke, MouseMoveEvent, TestAppContext, - }; - use std::{cell::Cell, rc::Weak}; - - struct GroupHoverTestView { - render_count: Rc>, - anonymous_paint_count: Rc>, - stateful_width: Rc>, - } - - impl Render for GroupHoverTestView { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - self.render_count.set(self.render_count.get() + 1); - let anonymous_paint_count = self.anonymous_paint_count.clone(); - let stateful_width = self.stateful_width.clone(); - div().size_full().child( - div() - .ml(px(20.)) - .mt(px(20.)) - .size(px(50.)) - .relative() - .group("hover-group") - .child( - div() - .absolute() - .size_full() - .invisible() - .group_hover("hover-group", |style| style.visible()) - .child(canvas( - |_, _, _| {}, - move |_, _, _, _| { - anonymous_paint_count.set(anonymous_paint_count.get() + 1) - }, - )), - ) - .child( - div() - .id("stateful-group-hover-target") - .absolute() - .top_0() - .left_0() - .size(px(10.)) - .group_hover("hover-group", |style| style.size(px(20.))) - .child(canvas( - move |bounds, _, _| stateful_width.set(bounds.size.width), - |_, _, _, _| {}, - )), - ), - ) - } - } - - #[gpui::test] - fn group_hover_styles_update_only_on_transitions(cx: &mut TestAppContext) { - let render_count = Rc::new(Cell::new(0)); - let anonymous_paint_count = Rc::new(Cell::new(0)); - let stateful_width = Rc::new(Cell::new(px(0.))); - let window = cx.add_window({ - let render_count = render_count.clone(); - let anonymous_paint_count = anonymous_paint_count.clone(); - let stateful_width = stateful_width.clone(); - move |_, _| GroupHoverTestView { - render_count, - anonymous_paint_count, - stateful_width, - } - }); - let window = AnyWindowHandle::from(window); - - cx.update_window(window, |_, window, cx| window.draw(cx).clear(cx)) - .unwrap(); - assert_eq!(anonymous_paint_count.get(), 0); - assert_eq!(stateful_width.get(), px(10.)); - - let move_mouse = |cx: &mut TestAppContext, position| { - cx.update_window(window, |_, window, cx| { - window.simulate_mouse_move(position, cx) - }) - .unwrap(); - }; - - let initial_render_count = render_count.get(); - move_mouse(cx, point(px(25.), px(25.))); - assert_eq!(render_count.get(), initial_render_count + 1); - assert_eq!(anonymous_paint_count.get(), 1); - assert_eq!(stateful_width.get(), px(20.)); - - move_mouse(cx, point(px(30.), px(30.))); - assert_eq!(render_count.get(), initial_render_count + 1); - assert_eq!(anonymous_paint_count.get(), 1); - assert_eq!(stateful_width.get(), px(20.)); - - move_mouse(cx, point(px(5.), px(5.))); - assert_eq!(render_count.get(), initial_render_count + 2); - assert_eq!(anonymous_paint_count.get(), 1); - assert_eq!(stateful_width.get(), px(10.)); - - move_mouse(cx, point(px(10.), px(10.))); - assert_eq!(render_count.get(), initial_render_count + 2); - assert_eq!(anonymous_paint_count.get(), 1); - assert_eq!(stateful_width.get(), px(10.)); - } - - struct HoverListenerLayoutTestView { - target_left: Pixels, - hover_transitions: Rc>>, - } - - impl Render for HoverListenerLayoutTestView { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let hover_transitions = self.hover_transitions.clone(); - div().relative().size_full().child( - div() - .id("hover-target") - .absolute() - .left(self.target_left) - .top_0() - .size(px(20.)) - .on_click(|_, _, _| {}) - .on_hover(move |is_hovered, _, _| { - hover_transitions.borrow_mut().push(*is_hovered); - }), - ) - } - } - - #[gpui::test] - fn default_hover_listener_updates_when_layout_changes_under_stationary_mouse( - cx: &mut TestAppContext, - ) { - let hover_transitions = Rc::new(RefCell::new(Vec::new())); - let window = cx.add_window({ - let hover_transitions = hover_transitions.clone(); - move |_, _| HoverListenerLayoutTestView { - target_left: px(40.), - hover_transitions, - } - }); - let any_window = AnyWindowHandle::from(window); - - cx.update_window(any_window, |_, window, cx| { - window.draw(cx).clear(cx); - window.simulate_mouse_move(point(px(10.), px(10.)), cx); - }) - .unwrap(); - assert!(hover_transitions.borrow().is_empty()); - - window - .update(cx, |view, _, cx| { - view.target_left = px(0.); - cx.notify(); - }) - .unwrap(); - cx.update_window(any_window, |_, window, cx| window.draw(cx).clear(cx)) - .unwrap(); - assert_eq!(*hover_transitions.borrow(), [true]); - - window - .update(cx, |view, _, cx| { - view.target_left = px(40.); - cx.notify(); - }) - .unwrap(); - cx.update_window(any_window, |_, window, cx| window.draw(cx).clear(cx)) - .unwrap(); - assert_eq!(*hover_transitions.borrow(), [true, false]); - } - - #[gpui::test] - fn default_hover_listener_ends_after_key_press(cx: &mut TestAppContext) { - let hover_transitions = Rc::new(RefCell::new(Vec::new())); - let window = cx.add_window({ - let hover_transitions = hover_transitions.clone(); - move |_, _| HoverListenerLayoutTestView { - target_left: px(0.), - hover_transitions, - } - }); - let any_window = AnyWindowHandle::from(window); - - cx.update_window(any_window, |_, window, cx| { - window.draw(cx).clear(cx); - window.simulate_mouse_move(point(px(10.), px(10.)), cx); - }) - .unwrap(); - assert_eq!(*hover_transitions.borrow(), [true]); - - key_down(cx, any_window, "a"); - cx.update_window(any_window, |_, window, cx| window.draw(cx).clear(cx)) - .unwrap(); - assert_eq!(*hover_transitions.borrow(), [true, false]); - } - - struct HoverListenerModeLayoutTestView { - target_left: Pixels, - hover_transitions: Rc>>, - } - - impl Render for HoverListenerModeLayoutTestView { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let hover_transitions = self.hover_transitions.clone(); - div().relative().size_full().child( - div() - .id("hover-target") - .absolute() - .left(self.target_left) - .top_0() - .size(px(20.)) - .hover_listener_mode(HoverListenerMode::InputModalityIndependent) - .on_hover(move |is_hovered, _, _| { - hover_transitions.borrow_mut().push(*is_hovered); - }), - ) - } - } - - #[gpui::test] - fn input_modality_independent_hover_listener_updates_after_key_press(cx: &mut TestAppContext) { - let hover_transitions = Rc::new(RefCell::new(Vec::new())); - let window = cx.add_window({ - let hover_transitions = hover_transitions.clone(); - move |_, _| HoverListenerModeLayoutTestView { - target_left: px(40.), - hover_transitions, - } - }); - let any_window = AnyWindowHandle::from(window); - let pointer_position = point(px(10.), px(10.)); - - cx.update_window(any_window, |_, window, cx| { - window.draw(cx).clear(cx); - window.simulate_mouse_move(pointer_position, cx); - }) - .unwrap(); - assert!(hover_transitions.borrow().is_empty()); - - key_down(cx, any_window, "a"); - window - .update(cx, |view, _, cx| { - view.target_left = px(0.); - cx.notify(); - }) - .unwrap(); - cx.update_window(any_window, |_, window, cx| window.draw(cx).clear(cx)) - .unwrap(); - assert_eq!(*hover_transitions.borrow(), [true]); - - key_down(cx, any_window, "b"); - cx.update_window(any_window, |_, window, cx| window.draw(cx).clear(cx)) - .unwrap(); - assert_eq!(*hover_transitions.borrow(), [true]); - - cx.update_window(any_window, |_, window, cx| { - window.simulate_mouse_move(point(px(30.), px(10.)), cx); - }) - .unwrap(); - assert_eq!(*hover_transitions.borrow(), [true, false]); - - cx.update_window(any_window, |_, window, cx| { - window.simulate_mouse_move(pointer_position, cx); - window.dispatch_event( - MouseExitEvent { - position: pointer_position, - ..Default::default() - } - .to_platform_input(), - cx, - ); - }) - .unwrap(); - assert_eq!(*hover_transitions.borrow(), [true, false, true, false]); - } - - #[gpui::test] - fn default_hover_listener_remains_hovered_during_stationary_mouse_press( - cx: &mut TestAppContext, - ) { - let hover_transitions = Rc::new(RefCell::new(Vec::new())); - let window = cx.add_window({ - let hover_transitions = hover_transitions.clone(); - move |_, _| HoverListenerLayoutTestView { - target_left: px(0.), - hover_transitions, - } - }); - let any_window = AnyWindowHandle::from(window); - let mouse_position = point(px(10.), px(10.)); - - cx.update_window(any_window, |_, window, cx| { - window.draw(cx).clear(cx); - window.simulate_mouse_move(mouse_position, cx); - }) - .unwrap(); - assert_eq!(*hover_transitions.borrow(), [true]); - - cx.update_window(any_window, |_, window, cx| { - window.dispatch_event( - MouseDownEvent { - position: mouse_position, - button: MouseButton::Left, - modifiers: Default::default(), - click_count: 1, - first_mouse: false, - } - .to_platform_input(), - cx, - ); - window.draw(cx).clear(cx); - }) - .unwrap(); - assert_eq!(*hover_transitions.borrow(), [true]); - - cx.update_window(any_window, |_, window, cx| { - window.dispatch_event( - MouseUpEvent { - position: mouse_position, - button: MouseButton::Left, - modifiers: Default::default(), - click_count: 1, - } - .to_platform_input(), - cx, - ); - window.draw(cx).clear(cx); - }) - .unwrap(); - assert_eq!(*hover_transitions.borrow(), [true]); - } - - struct TestTooltipView; - - impl Render for TestTooltipView { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().w(px(20.)).h(px(20.)).child("tooltip") - } - } - - type CapturedActiveTooltip = Rc>>>>>; - - struct TooltipCaptureElement { - child: AnyElement, - captured_active_tooltip: CapturedActiveTooltip, - } - - impl IntoElement for TooltipCaptureElement { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } - } - - impl Element for TooltipCaptureElement { - type RequestLayoutState = (); - type PrepaintState = (); - - fn id(&self) -> Option { - None - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - (self.child.request_layout(window, cx), ()) - } - - fn prepaint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState { - self.child.prepaint(window, cx); - } - - fn paint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - _prepaint: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - self.child.paint(window, cx); - window.with_global_id("target".into(), |global_id, window| { - window.with_element_state::( - global_id, - |state, _window| { - let state = state.unwrap(); - *self.captured_active_tooltip.borrow_mut() = - state.active_tooltip.as_ref().map(Rc::downgrade); - ((), state) - }, - ) - }); - } - } - - struct TooltipOwner { - captured_active_tooltip: CapturedActiveTooltip, - show_delay_override: Option, - } - - impl Render for TooltipOwner { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - TooltipCaptureElement { - child: div() - .size_full() - .child( - div() - .id("target") - .w(px(50.)) - .h(px(50.)) - .tooltip(|_, cx| cx.new(|_| TestTooltipView).into()) - .when_some(self.show_delay_override, |this, delay| { - this.tooltip_show_delay(delay) - }), - ) - .into_any_element(), - captured_active_tooltip: self.captured_active_tooltip.clone(), - } - } - } - - #[test] - fn scroll_handle_aligns_wide_children_to_left_edge() { - let handle = ScrollHandle::new(); - { - let mut state = handle.0.borrow_mut(); - state.bounds = Bounds::new(point(px(0.), px(0.)), size(px(80.), px(20.))); - state.child_bounds = vec![Bounds::new(point(px(25.), px(0.)), size(px(200.), px(20.)))]; - state.overflow.x = Overflow::Scroll; - state.active_item = Some(ScrollActiveItem { - index: 0, - strategy: ScrollStrategy::default(), - }); - } - - handle.scroll_to_active_item(); - - assert_eq!(handle.offset().x, px(-25.)); - } - - #[test] - fn scroll_handle_aligns_tall_children_to_top_edge() { - let handle = ScrollHandle::new(); - { - let mut state = handle.0.borrow_mut(); - state.bounds = Bounds::new(point(px(0.), px(0.)), size(px(20.), px(80.))); - state.child_bounds = vec![Bounds::new(point(px(0.), px(25.)), size(px(20.), px(200.)))]; - state.overflow.y = Overflow::Scroll; - state.active_item = Some(ScrollActiveItem { - index: 0, - strategy: ScrollStrategy::default(), - }); - } - - handle.scroll_to_active_item(); - - assert_eq!(handle.offset().y, px(-25.)); - } - - fn setup_tooltip_owner_test( - show_delay_override: Option, - ) -> ( - TestAppContext, - crate::AnyWindowHandle, - CapturedActiveTooltip, - ) { - let mut test_app = TestAppContext::single(); - let captured_active_tooltip: CapturedActiveTooltip = Rc::new(RefCell::new(None)); - let window = test_app.add_window({ - let captured_active_tooltip = captured_active_tooltip.clone(); - move |_, _| TooltipOwner { - captured_active_tooltip, - show_delay_override, - } - }); - let any_window = window.into(); - - test_app - .update_window(any_window, |_, window, cx| { - window.draw(cx).clear(cx); - }) - .unwrap(); - - test_app - .update_window(any_window, |_, window, cx| { - window.dispatch_event( - MouseMoveEvent { - position: point(px(10.), px(10.)), - modifiers: Default::default(), - pressed_button: None, - } - .to_platform_input(), - cx, - ); - }) - .unwrap(); - - test_app - .update_window(any_window, |_, window, cx| { - window.draw(cx).clear(cx); - }) - .unwrap(); - - (test_app, any_window, captured_active_tooltip) - } - - #[test] - fn tooltip_waiting_for_show_is_released_when_its_owner_disappears() { - let (mut test_app, any_window, captured_active_tooltip) = setup_tooltip_owner_test(None); - - let weak_active_tooltip = captured_active_tooltip.borrow().clone().unwrap(); - let active_tooltip = weak_active_tooltip.upgrade().unwrap(); - assert!(matches!( - active_tooltip.borrow().as_ref(), - Some(ActiveTooltip::WaitingForShow { .. }) - )); - - test_app - .update_window(any_window, |_, window, _| { - window.remove_window(); - }) - .unwrap(); - test_app.run_until_parked(); - drop(active_tooltip); - - assert!(weak_active_tooltip.upgrade().is_none()); - } - - #[test] - fn tooltip_respects_custom_show_delay() { - let extra_delay = Duration::from_secs(1); - let show_delay_override = DEFAULT_TOOLTIP_SHOW_DELAY + extra_delay; - let (mut test_app, _any_window, captured_active_tooltip) = - setup_tooltip_owner_test(Some(show_delay_override)); - - let weak_active_tooltip = captured_active_tooltip.borrow().clone().unwrap(); - let active_tooltip = weak_active_tooltip.upgrade().unwrap(); - - test_app - .dispatcher - .advance_clock(DEFAULT_TOOLTIP_SHOW_DELAY); - test_app.run_until_parked(); - - assert!(matches!( - active_tooltip.borrow().as_ref(), - Some(ActiveTooltip::WaitingForShow { .. }) - )); - - test_app.dispatcher.advance_clock(extra_delay); - test_app.run_until_parked(); - - assert!(matches!( - active_tooltip.borrow().as_ref(), - Some(ActiveTooltip::Visible { .. }) - )); - } - - #[test] - fn tooltip_is_released_when_its_owner_disappears() { - let (mut test_app, any_window, captured_active_tooltip) = setup_tooltip_owner_test(None); - - let weak_active_tooltip = captured_active_tooltip.borrow().clone().unwrap(); - let active_tooltip = weak_active_tooltip.upgrade().unwrap(); - - test_app - .dispatcher - .advance_clock(DEFAULT_TOOLTIP_SHOW_DELAY); - test_app.run_until_parked(); - - assert!(matches!( - active_tooltip.borrow().as_ref(), - Some(ActiveTooltip::Visible { .. }) - )); - - test_app - .update_window(any_window, |_, window, _| { - window.remove_window(); - }) - .unwrap(); - test_app.run_until_parked(); - drop(active_tooltip); - - assert!(weak_active_tooltip.upgrade().is_none()); - } - - #[test] - fn tooltip_hides_after_mouse_leaves_origin() { - let (mut test_app, any_window, captured_active_tooltip) = setup_tooltip_owner_test(None); - - let weak_active_tooltip = captured_active_tooltip.borrow().clone().unwrap(); - let active_tooltip = weak_active_tooltip.upgrade().unwrap(); - - test_app - .dispatcher - .advance_clock(DEFAULT_TOOLTIP_SHOW_DELAY); - test_app.run_until_parked(); - - assert!(matches!( - active_tooltip.borrow().as_ref(), - Some(ActiveTooltip::Visible { .. }) - )); - - test_app - .update_window(any_window, |_, window, cx| { - window.dispatch_event( - MouseMoveEvent { - position: point(px(75.), px(75.)), - modifiers: Default::default(), - pressed_button: None, - } - .to_platform_input(), - cx, - ); - }) - .unwrap(); - - assert!(active_tooltip.borrow().is_none()); - } - - struct MouseDownOutOwner { - mouse_down_out_count: Rc>, - } - - impl Render for MouseDownOutOwner { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let mouse_down_out_count = self.mouse_down_out_count.clone(); - div() - .size_full() - .child(div().id("target").w(px(50.)).h(px(50.)).on_mouse_down_out( - move |_, _, _| { - *mouse_down_out_count.borrow_mut() += 1; - }, - )) - } - } - - #[test] - fn mouse_down_out_is_suppressed_while_window_prompt_is_active() { - let mut test_app = TestAppContext::single(); - let mouse_down_out_count = Rc::new(RefCell::new(0)); - let window = test_app.add_window({ - let mouse_down_out_count = mouse_down_out_count.clone(); - move |_, _| MouseDownOutOwner { - mouse_down_out_count, - } - }); - let any_window: AnyWindowHandle = window.into(); - - fn dispatch_mouse_down_outside_target( - test_app: &mut TestAppContext, - any_window: AnyWindowHandle, - ) { - test_app - .update_window(any_window, |_, window, cx| { - window.dispatch_event( - MouseDownEvent { - position: point(px(75.), px(75.)), - button: MouseButton::Left, - modifiers: Default::default(), - click_count: 1, - first_mouse: false, - } - .to_platform_input(), - cx, - ); - }) - .unwrap(); - } - - test_app - .update_window(any_window, |_, window, cx| { - window.draw(cx).clear(cx); - }) - .unwrap(); - - dispatch_mouse_down_outside_target(&mut test_app, any_window); - assert_eq!( - *mouse_down_out_count.borrow(), - 1, - "mouse down outside the element should fire mouse-down-out listeners" - ); - - test_app - .update_window(any_window, |_, window, cx| { - cx.set_prompt_builder(crate::fallback_prompt_renderer); - let _receiver = - window.prompt(crate::PromptLevel::Warning, "message", None, &["Ok"], cx); - assert!(window.has_active_prompt()); - window.draw(cx).clear(cx); - }) - .unwrap(); - - dispatch_mouse_down_outside_target(&mut test_app, any_window); - assert_eq!( - *mouse_down_out_count.borrow(), - 1, - "mouse down over an active prompt should not fire mouse-down-out listeners" - ); - } - - #[test] - fn test_accessibility_id_builder_writes_author_id() { - let mut element = div() - .id("buffer-font-size") - .accessibility_id("settings.buffer-font-size"); - let mut node = accesskit::Node::new(accesskit::Role::SpinButton); - - element.interactivity().write_a11y_info(&mut node); - - assert_eq!(node.author_id(), Some("settings.buffer-font-size")); - } - - #[test] - fn test_write_a11y_info_string_and_numeric_properties() { - let mut interactivity = Interactivity::default(); - interactivity.aria.author_id = Some("settings.buffer-font-size".into()); - interactivity.aria.label = Some("Buffer Font Size".into()); - interactivity.aria.value = Some("15".into()); - interactivity.aria.placeholder = Some("Search".into()); - interactivity.aria.numeric_value = Some(15.0); - interactivity.aria.min_numeric_value = Some(6.0); - interactivity.aria.max_numeric_value = Some(72.0); - interactivity.aria.numeric_value_step = Some(1.0); - - let mut node = accesskit::Node::new(accesskit::Role::SpinButton); - interactivity.write_a11y_info(&mut node); - - assert_eq!(node.author_id(), Some("settings.buffer-font-size")); - assert_eq!(node.label(), Some("Buffer Font Size")); - assert_eq!(node.value(), Some("15")); - assert_eq!(node.placeholder(), Some("Search")); - assert_eq!(node.numeric_value(), Some(15.0)); - assert_eq!(node.min_numeric_value(), Some(6.0)); - assert_eq!(node.max_numeric_value(), Some(72.0)); - assert_eq!(node.numeric_value_step(), Some(1.0)); - } - - #[test] - fn test_write_a11y_info_aria_current() { - let mut interactivity = Interactivity::default(); - interactivity.aria.current = Some(accesskit::AriaCurrent::Page); - - let mut node = accesskit::Node::new(accesskit::Role::Link); - interactivity.write_a11y_info(&mut node); - - assert_eq!(node.aria_current(), Some(accesskit::AriaCurrent::Page)); - } - - /// Two focusable, clickable elements ("a" and "b") used to exercise the - /// Enter/Space -> synthesized click press/release pairing. - struct KeyboardActivationTest { - focus_a: FocusHandle, - focus_b: FocusHandle, - clicks: Rc>>, - } - - impl Render for KeyboardActivationTest { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let clicks_a = self.clicks.clone(); - let clicks_b = self.clicks.clone(); - div() - .size_full() - .child( - div() - .id("a") - .w(px(50.)) - .h(px(50.)) - .track_focus(&self.focus_a) - .on_click(move |_, _, _| clicks_a.borrow_mut().push("a")), - ) - .child( - div() - .id("b") - .w(px(50.)) - .h(px(50.)) - .track_focus(&self.focus_b) - .on_click(move |_, _, _| clicks_b.borrow_mut().push("b")), - ) - } - } - - fn setup_keyboard_activation_test() -> ( - TestAppContext, - AnyWindowHandle, - Rc>>, - FocusHandle, - FocusHandle, - ) { - let mut cx = TestAppContext::single(); - let (focus_a, focus_b) = cx.update(|cx| (cx.focus_handle(), cx.focus_handle())); - let clicks: Rc>> = Rc::new(RefCell::new(Vec::new())); - let window = cx.add_window({ - let focus_a = focus_a.clone(); - let focus_b = focus_b.clone(); - let clicks = clicks.clone(); - move |_, _| KeyboardActivationTest { - focus_a, - focus_b, - clicks, - } - }); - (cx, window.into(), clicks, focus_a, focus_b) - } - - /// Move focus to `handle`, flush effects, then paint so the newly focused - /// element registers its key handlers for the next dispatched event. - fn focus_and_draw(cx: &mut TestAppContext, window: AnyWindowHandle, handle: &FocusHandle) { - cx.update_window(window, |_, window, cx| window.focus(handle, cx)) - .unwrap(); - cx.run_until_parked(); - cx.update_window(window, |_, window, cx| { - window.draw(cx).clear(cx); - }) - .unwrap(); - } - - fn key_down(cx: &mut TestAppContext, window: AnyWindowHandle, key: &str) { - let keystroke = Keystroke::parse(key).unwrap(); - cx.update_window(window, |_, window, cx| { - window.dispatch_event( - KeyDownEvent { - keystroke, - is_held: false, - prefer_character_input: false, - } - .to_platform_input(), - cx, - ); - }) - .unwrap(); - } - - fn key_up(cx: &mut TestAppContext, window: AnyWindowHandle, key: &str) { - let keystroke = Keystroke::parse(key).unwrap(); - cx.update_window(window, |_, window, cx| { - window.dispatch_event(KeyUpEvent { keystroke }.to_platform_input(), cx); - }) - .unwrap(); - } - - /// Pressing and releasing Enter on the same focused element fires a click. - #[test] - fn keyboard_activation_fires_click_on_same_element() { - let (mut cx, window, clicks, focus_a, _focus_b) = setup_keyboard_activation_test(); - - focus_and_draw(&mut cx, window, &focus_a); - key_down(&mut cx, window, "enter"); - key_up(&mut cx, window, "enter"); - - assert_eq!(*clicks.borrow(), vec!["a"]); - } - - /// A key-down whose key-up lands on a *different* element (because focus - /// moved in between) must not leak a synthesized click onto the newly - /// focused element. This is the core regression: previously the key-up - /// handler fired unconditionally on whatever was focused at key-up time. - #[test] - fn keyboard_activation_does_not_leak_across_focus_change() { - let (mut cx, window, clicks, focus_a, focus_b) = setup_keyboard_activation_test(); - - // Enter pressed while "a" is focused... - focus_and_draw(&mut cx, window, &focus_a); - key_down(&mut cx, window, "enter"); - - // ...focus moves to "b" before the release (as a confirm action would)... - focus_and_draw(&mut cx, window, &focus_b); - key_up(&mut cx, window, "enter"); - - // ...so neither element is clicked: "a" never saw the up, and "b" - // never saw the down. - assert!(clicks.borrow().is_empty(), "clicks: {:?}", clicks.borrow()); - } - - /// A keydown whose flag is left pending because focus moved away before - /// the keyup must not fire a click when focus later *returns* to the same - /// element (the menu trigger reopening case). The stamped focus generation - /// no longer matches, so the stale pending state is ignored. - #[test] - fn keyboard_activation_does_not_leak_when_focus_returns() { - let (mut cx, window, clicks, focus_a, focus_b) = setup_keyboard_activation_test(); - - // Enter pressed on "a"... - focus_and_draw(&mut cx, window, &focus_a); - key_down(&mut cx, window, "enter"); - - // ...focus leaves "a" before its keyup (so the pending state is never - // consumed), then comes back to "a"... - focus_and_draw(&mut cx, window, &focus_b); - focus_and_draw(&mut cx, window, &focus_a); - key_up(&mut cx, window, "enter"); - - // ...and the now-stale pending keydown must not fire a click. - assert!(clicks.borrow().is_empty(), "clicks: {:?}", clicks.borrow()); - } - - /// A non-activation key *released* during the press must cancel the pending - /// activation. For the sequence escape-down, space-down, escape-up, - /// space-up the space forms a clean down/up pair, but the intervening - /// escape-up means this isn't a plain space activation, so no click fires. - #[test] - fn keyboard_activation_cleared_by_intervening_key_release() { - let (mut cx, window, clicks, focus_a, _focus_b) = setup_keyboard_activation_test(); - - focus_and_draw(&mut cx, window, &focus_a); - key_down(&mut cx, window, "escape"); - key_down(&mut cx, window, "space"); - key_up(&mut cx, window, "escape"); - key_up(&mut cx, window, "space"); - - assert!(clicks.borrow().is_empty(), "clicks: {:?}", clicks.borrow()); - } - - /// The flag is a single activation marker, not keyed by which activation - /// key was used, so a Space down paired with an Enter up on the same - /// element still fires a click. - #[test] - fn keyboard_activation_does_not_distinguish_space_and_enter() { - let (mut cx, window, clicks, focus_a, _focus_b) = setup_keyboard_activation_test(); - - focus_and_draw(&mut cx, window, &focus_a); - key_down(&mut cx, window, "space"); - key_up(&mut cx, window, "enter"); - - assert_eq!(*clicks.borrow(), vec!["a"]); - } - - /// A non-activation key pressed between the activation down and up clears - /// the pending flag, suppressing the click. - #[test] - fn keyboard_activation_cleared_by_intervening_keydown() { - let (mut cx, window, clicks, focus_a, _focus_b) = setup_keyboard_activation_test(); - - focus_and_draw(&mut cx, window, &focus_a); - key_down(&mut cx, window, "enter"); - key_down(&mut cx, window, "a"); - key_up(&mut cx, window, "enter"); - - assert!(clicks.borrow().is_empty(), "clicks: {:?}", clicks.borrow()); - } - - /// A modified Enter (e.g. cmd-enter) is not treated as an activation key, - /// so it neither sets the pending flag nor fires a click on release. - #[test] - fn keyboard_activation_ignores_modified_keys() { - let (mut cx, window, clicks, focus_a, _focus_b) = setup_keyboard_activation_test(); - - focus_and_draw(&mut cx, window, &focus_a); - key_down(&mut cx, window, "cmd-enter"); - key_up(&mut cx, window, "cmd-enter"); - - assert!(clicks.borrow().is_empty(), "clicks: {:?}", clicks.borrow()); - } - - /// Two sibling tab groups, each a focusable container that is *not* itself a - /// tab stop and holds a single tab stop. Mirrors how the title bar and - /// status bar expose their controls as ARIA toolbars. - struct TabGroupFocus { - group_a: FocusHandle, - item_a: FocusHandle, - group_b: FocusHandle, - item_b: FocusHandle, - } - - impl Render for TabGroupFocus { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - fn group(container: &FocusHandle, item: &FocusHandle) -> Div { - div() - .track_focus(container) - .tab_group() - .child(div().track_focus(item)) - } - div() - .child(group(&self.group_a, &self.item_a)) - .child(group(&self.group_b, &self.item_b)) - } - } - - /// Focusing a tab-group container and pressing Tab (`focus_next`) must move - /// focus to the first tab stop *inside that container*, as documented on - /// [`InteractiveElement::tab_stop`]. - #[test] - fn focus_next_from_tab_group_container_enters_that_group() { - let mut cx = TestAppContext::single(); - let (group_a, item_a, group_b, item_b) = cx.update(|cx| { - ( - cx.focus_handle(), - cx.focus_handle().tab_stop(true), - cx.focus_handle(), - cx.focus_handle().tab_stop(true), - ) - }); - let window: AnyWindowHandle = cx - .add_window({ - let (group_a, item_a, group_b, item_b) = - (group_a, item_a, group_b.clone(), item_b.clone()); - move |_, _| TabGroupFocus { - group_a, - item_a, - group_b, - item_b, - } - }) - .into(); - cx.update_window(window, |_, window, cx| window.draw(cx).clear(cx)) - .unwrap(); - - // Focus the *second* group's container, then advance like Tab would. - let focused = cx - .update_window(window, |_, window, cx| { - window.focus(&group_b, cx); - window.focus_next(cx); - window.focused(cx).map(|handle| handle.id) - }) - .unwrap(); - - assert_eq!(focused, Some(item_b.id)); - } - - #[gpui::test] - fn test_fractional_padding_does_not_make_a_fitting_container_scrollable( - cx: &mut TestAppContext, - ) { - struct PaddedContainer { - scroll_handle: ScrollHandle, - } - - impl Render for PaddedContainer { - fn render( - &mut self, - _window: &mut Window, - _cx: &mut Context, - ) -> impl IntoElement { - // 4.25px of padding snaps to 4px in layout, so a 42px child - // fits the 50px box exactly. - div().size_full().child( - div() - .id("container") - .h(px(50.)) - .w(px(100.)) - .py(px(4.25)) - .overflow_y_scroll() - .track_scroll(&self.scroll_handle) - .child(div().w_full().h(px(42.))), - ) - } - } - - let scroll_handle = ScrollHandle::new(); - let window: AnyWindowHandle = cx - .add_window({ - let scroll_handle = scroll_handle.clone(); - move |_, _| PaddedContainer { scroll_handle } - }) - .into(); - cx.update_window(window, |_, window, cx| window.draw(cx).clear(cx)) - .unwrap(); - - assert_eq!(scroll_handle.max_offset().y, px(0.)); - } - - struct ContentSizedGrid; - - impl Render for ContentSizedGrid { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let widths = [px(100.), px(200.), px(50.)]; - div().size_full().child( - div() - .w_full() - .grid() - .grid_cols_max_content(widths.len() as u16) - .children(widths.into_iter().enumerate().map(|(index, width)| { - div() - .debug_selector(move || format!("cell-{index}")) - .w(width) - .h(px(10.)) - })), - ) - } - } - - #[gpui::test] - fn grid_cols_max_content_sizes_columns_to_their_content(cx: &mut TestAppContext) { - let window = cx.add_window(|_, _| ContentSizedGrid); - cx.update_window(window.into(), |_, window, cx| window.draw(cx).clear(cx)) - .unwrap(); - - let mut bounds = |selector: &'static str| { - cx.update_window(window.into(), |_, window, _| { - window.rendered_frame.debug_bounds.get(selector).copied() - }) - .unwrap() - .unwrap_or_else(|| panic!("{selector} was not rendered")) - }; - - assert_eq!(bounds("cell-0").origin.x, px(0.)); - assert_eq!(bounds("cell-1").origin.x, px(100.)); - assert_eq!(bounds("cell-2").origin.x, px(300.)); - } -} diff --git a/crates/gpui_pre/src/elements/image_cache.rs b/crates/gpui_pre/src/elements/image_cache.rs deleted file mode 100644 index ee14361..0000000 --- a/crates/gpui_pre/src/elements/image_cache.rs +++ /dev/null @@ -1,353 +0,0 @@ -use crate::{ - AnyElement, AnyEntity, App, AppContext, Asset, AssetLogger, Bounds, Element, ElementId, Entity, - GlobalElementId, ImageAssetLoader, ImageCacheError, InspectorElementId, IntoElement, LayoutId, - ParentElement, Pixels, RenderImage, Resource, Style, StyleRefinement, Styled, Task, Window, - hash, -}; - -use futures::{FutureExt, future::Shared}; -use refineable::Refineable; -use smallvec::SmallVec; -use std::{collections::HashMap, fmt, sync::Arc}; - -/// An image cache element, all its child img elements will use the cache specified by this element. -/// Note that this could as simple as passing an `Entity` -pub fn image_cache(image_cache_provider: impl ImageCacheProvider) -> ImageCacheElement { - ImageCacheElement { - image_cache_provider: Box::new(image_cache_provider), - style: StyleRefinement::default(), - children: SmallVec::default(), - } -} - -/// A dynamically typed image cache, which can be used to store any image cache -#[derive(Clone)] -pub struct AnyImageCache { - image_cache: AnyEntity, - load_fn: fn( - image_cache: &AnyEntity, - resource: &Resource, - window: &mut Window, - cx: &mut App, - ) -> Option, ImageCacheError>>, -} - -impl From> for AnyImageCache { - fn from(image_cache: Entity) -> Self { - Self { - image_cache: image_cache.into_any(), - load_fn: any_image_cache::load::, - } - } -} - -impl AnyImageCache { - /// Load an image given a resource - /// returns the result of loading the image if it has finished loading, or None if it is still loading - pub fn load( - &self, - resource: &Resource, - window: &mut Window, - cx: &mut App, - ) -> Option, ImageCacheError>> { - (self.load_fn)(&self.image_cache, resource, window, cx) - } -} - -mod any_image_cache { - use super::*; - - pub(crate) fn load( - image_cache: &AnyEntity, - resource: &Resource, - window: &mut Window, - cx: &mut App, - ) -> Option, ImageCacheError>> { - let image_cache = image_cache.clone().downcast::().unwrap(); - image_cache.update(cx, |image_cache, cx| image_cache.load(resource, window, cx)) - } -} - -/// An image cache element. -pub struct ImageCacheElement { - image_cache_provider: Box, - style: StyleRefinement, - children: SmallVec<[AnyElement; 2]>, -} - -impl ParentElement for ImageCacheElement { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl Styled for ImageCacheElement { - fn style(&mut self) -> &mut StyleRefinement { - &mut self.style - } -} - -impl IntoElement for ImageCacheElement { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -impl Element for ImageCacheElement { - type RequestLayoutState = SmallVec<[LayoutId; 4]>; - type PrepaintState = (); - - fn id(&self) -> Option { - None - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - let image_cache = self.image_cache_provider.provide(window, cx); - window.with_image_cache(Some(image_cache), |window| { - let child_layout_ids = self - .children - .iter_mut() - .map(|child| child.request_layout(window, cx)) - .collect::>(); - let mut style = Style::default(); - style.refine(&self.style); - let layout_id = window.request_layout(style, child_layout_ids.iter().copied(), cx); - (layout_id, child_layout_ids) - }) - } - - fn prepaint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState { - for child in &mut self.children { - child.prepaint(window, cx); - } - } - - fn paint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - _prepaint: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - let image_cache = self.image_cache_provider.provide(window, cx); - window.with_image_cache(Some(image_cache), |window| { - for child in &mut self.children { - child.paint(window, cx); - } - }) - } -} - -/// An image loading task associated with an image cache. -pub type ImageLoadingTask = Shared, ImageCacheError>>>; - -/// An image cache item -pub enum ImageCacheItem { - /// The associated image is currently loading - Loading(ImageLoadingTask), - /// This item has loaded an image. - Loaded(Result, ImageCacheError>), -} - -impl std::fmt::Debug for ImageCacheItem { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let status = match self { - ImageCacheItem::Loading(_) => &"Loading...".to_string(), - ImageCacheItem::Loaded(render_image) => &format!("{:?}", render_image), - }; - f.debug_struct("ImageCacheItem") - .field("status", status) - .finish() - } -} - -impl ImageCacheItem { - /// Attempt to get the image from the cache item. - pub fn get(&mut self) -> Option, ImageCacheError>> { - match self { - ImageCacheItem::Loading(task) => { - let res = task.now_or_never()?; - *self = ImageCacheItem::Loaded(res.clone()); - Some(res) - } - ImageCacheItem::Loaded(res) => Some(res.clone()), - } - } -} - -/// An object that can handle the caching and unloading of images. -/// Implementations of this trait should ensure that images are removed from all windows when they are no longer needed. -pub trait ImageCache: 'static { - /// Load an image given a resource - /// returns the result of loading the image if it has finished loading, or None if it is still loading - fn load( - &mut self, - resource: &Resource, - window: &mut Window, - cx: &mut App, - ) -> Option, ImageCacheError>>; -} - -/// An object that can create an ImageCache during the render phase. -/// See the ImageCache trait for more information. -pub trait ImageCacheProvider: 'static { - /// Called during the request_layout phase to create an ImageCache. - fn provide(&mut self, _window: &mut Window, _cx: &mut App) -> AnyImageCache; -} - -impl ImageCacheProvider for Entity { - fn provide(&mut self, _window: &mut Window, _cx: &mut App) -> AnyImageCache { - self.clone().into() - } -} - -/// An implementation of ImageCache, that uses an LRU caching strategy to unload images when the cache is full -pub struct RetainAllImageCache(HashMap); - -impl fmt::Debug for RetainAllImageCache { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("HashMapImageCache") - .field("num_images", &self.0.len()) - .finish() - } -} - -impl RetainAllImageCache { - /// Create a new image cache. - #[inline] - pub fn new(cx: &mut App) -> Entity { - let e = cx.new(|_cx| RetainAllImageCache(HashMap::new())); - cx.observe_release(&e, |image_cache, cx| { - for (_, mut item) in std::mem::replace(&mut image_cache.0, HashMap::new()) { - if let Some(Ok(image)) = item.get() { - cx.drop_image(image, None); - } - } - }) - .detach(); - e - } - - /// Load an image from the given source. - /// - /// Returns `None` if the image is loading. - pub fn load( - &mut self, - source: &Resource, - window: &mut Window, - cx: &mut App, - ) -> Option, ImageCacheError>> { - let hash = hash(source); - - if let Some(item) = self.0.get_mut(&hash) { - return item.get(); - } - - let fut = AssetLogger::::load(source.clone(), cx); - let task = cx.background_executor().spawn(fut).shared(); - self.0.insert(hash, ImageCacheItem::Loading(task.clone())); - - let entity = window.current_view(); - window - .spawn(cx, { - async move |cx| { - _ = task.await; - cx.on_next_frame(move |_, cx| { - cx.notify(entity); - }); - } - }) - .detach(); - - None - } - - /// Clear the image cache. - pub fn clear(&mut self, window: &mut Window, cx: &mut App) { - for (_, mut item) in std::mem::replace(&mut self.0, HashMap::new()) { - if let Some(Ok(image)) = item.get() { - cx.drop_image(image, Some(window)); - } - } - } - - /// Remove the image from the cache by the given source. - pub fn remove(&mut self, source: &Resource, window: &mut Window, cx: &mut App) { - let hash = hash(source); - if let Some(mut item) = self.0.remove(&hash) - && let Some(Ok(image)) = item.get() - { - cx.drop_image(image, Some(window)); - } - } - - /// Returns the number of images in the cache. - pub fn len(&self) -> usize { - self.0.len() - } - - /// Returns true if the cache is empty. - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } -} - -impl ImageCache for RetainAllImageCache { - fn load( - &mut self, - resource: &Resource, - window: &mut Window, - cx: &mut App, - ) -> Option, ImageCacheError>> { - RetainAllImageCache::load(self, resource, window, cx) - } -} - -/// Constructs a retain-all image cache that uses the element state associated with the given ID. -pub fn retain_all(id: impl Into) -> RetainAllImageCacheProvider { - RetainAllImageCacheProvider { id: id.into() } -} - -/// A provider struct for creating a retain-all image cache inline -pub struct RetainAllImageCacheProvider { - id: ElementId, -} - -impl ImageCacheProvider for RetainAllImageCacheProvider { - fn provide(&mut self, window: &mut Window, cx: &mut App) -> AnyImageCache { - window - .with_global_id(self.id.clone(), |global_id, window| { - window.with_element_state::, _>( - global_id, - |cache, _window| { - let mut cache = cache.unwrap_or_else(|| RetainAllImageCache::new(cx)); - (cache.clone(), cache) - }, - ) - }) - .into() - } -} diff --git a/crates/gpui_pre/src/elements/img.rs b/crates/gpui_pre/src/elements/img.rs deleted file mode 100644 index 2c77e5f..0000000 --- a/crates/gpui_pre/src/elements/img.rs +++ /dev/null @@ -1,983 +0,0 @@ -use crate::{ - AnyElement, AnyImageCache, App, Asset, AssetLogger, Bounds, DefiniteLength, Element, ElementId, - Entity, GlobalElementId, Hitbox, Image, ImageCache, InspectorElementId, InteractiveElement, - Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource, - SharedString, SharedUri, StyleRefinement, Styled, Task, Window, decode_static_image, - decode_static_image_from_decoder, px, -}; -use anyhow::Result; - -use futures::Future; -use gpui_util::ResultExt; -use image::{ - AnimationDecoder, ImageError, ImageFormat, Rgba, - codecs::{gif::GifDecoder, webp::WebPDecoder}, -}; -use scheduler::Instant; -use smallvec::SmallVec; -use std::{ - fs, - io::{self, Cursor}, - ops::{Deref, DerefMut}, - path::{Path, PathBuf}, - str::FromStr, - sync::Arc, - time::Duration, -}; -use thiserror::Error; - -use super::{Stateful, StatefulInteractiveElement}; - -/// The delay before showing the loading state. -pub const LOADING_DELAY: Duration = Duration::from_millis(200); - -/// A type alias to the resource loader that the `img()` element uses. -/// -/// Note: that this is only for Resources, like URLs or file paths. -/// Custom loaders, or external images will not use this asset loader -pub type ImgResourceLoader = AssetLogger; - -/// A source of image content. -#[derive(Clone)] -pub enum ImageSource { - /// The image content will be loaded from some resource location - Resource(Resource), - /// Cached image data - Render(Arc), - /// Cached image data - Image(Arc), - /// A custom loading function to use - Custom(Arc Option, ImageCacheError>>>), -} - -fn is_uri(uri: &str) -> bool { - url::Url::from_str(uri).is_ok() -} - -impl From for ImageSource { - fn from(value: SharedUri) -> Self { - Self::Resource(Resource::Uri(value)) - } -} - -impl<'a> From<&'a str> for ImageSource { - fn from(s: &'a str) -> Self { - if is_uri(s) { - Self::Resource(Resource::Uri(s.to_string().into())) - } else { - Self::Resource(Resource::Embedded(s.to_string().into())) - } - } -} - -impl From for ImageSource { - fn from(s: String) -> Self { - if is_uri(&s) { - Self::Resource(Resource::Uri(s.into())) - } else { - Self::Resource(Resource::Embedded(s.into())) - } - } -} - -impl From for ImageSource { - fn from(s: SharedString) -> Self { - s.as_ref().into() - } -} - -impl From<&Path> for ImageSource { - fn from(value: &Path) -> Self { - Self::Resource(value.to_path_buf().into()) - } -} - -impl From> for ImageSource { - fn from(value: Arc) -> Self { - Self::Resource(value.into()) - } -} - -impl From for ImageSource { - fn from(value: PathBuf) -> Self { - Self::Resource(value.into()) - } -} - -impl From> for ImageSource { - fn from(value: Arc) -> Self { - Self::Render(value) - } -} - -impl From> for ImageSource { - fn from(value: Arc) -> Self { - Self::Image(value) - } -} - -impl From for ImageSource -where - F: Fn(&mut Window, &mut App) -> Option, ImageCacheError>> + 'static, -{ - fn from(value: F) -> Self { - Self::Custom(Arc::new(value)) - } -} - -/// The style of an image element. -pub struct ImageStyle { - grayscale: bool, - object_fit: ObjectFit, - loading: Option AnyElement>>, - fallback: Option AnyElement>>, -} - -impl Default for ImageStyle { - fn default() -> Self { - Self { - grayscale: false, - object_fit: ObjectFit::Contain, - loading: None, - fallback: None, - } - } -} - -/// Style an image element. -pub trait StyledImage: Sized { - /// Get a mutable [ImageStyle] from the element. - fn image_style(&mut self) -> &mut ImageStyle; - - /// Set the image to be displayed in grayscale. - fn grayscale(mut self, grayscale: bool) -> Self { - self.image_style().grayscale = grayscale; - self - } - - /// Set the object fit for the image. - fn object_fit(mut self, object_fit: ObjectFit) -> Self { - self.image_style().object_fit = object_fit; - self - } - - /// Set a fallback function that will be invoked to render an error view should - /// the image fail to load. - fn with_fallback(mut self, fallback: impl Fn() -> AnyElement + 'static) -> Self { - self.image_style().fallback = Some(Box::new(fallback)); - self - } - - /// Set a fallback function that will be invoked to render a view while the image - /// is still being loaded. - fn with_loading(mut self, loading: impl Fn() -> AnyElement + 'static) -> Self { - self.image_style().loading = Some(Box::new(loading)); - self - } -} - -impl StyledImage for Img { - fn image_style(&mut self) -> &mut ImageStyle { - &mut self.style - } -} - -impl StyledImage for Stateful { - fn image_style(&mut self) -> &mut ImageStyle { - &mut self.element.style - } -} - -/// An image element. -pub struct Img { - interactivity: Interactivity, - source: ImageSource, - style: ImageStyle, - image_cache: Option, -} - -/// Create a new image element. -#[track_caller] -pub fn img(source: impl Into) -> Img { - Img { - interactivity: Interactivity::new(), - source: source.into(), - style: ImageStyle::default(), - image_cache: None, - } -} - -impl Img { - /// A list of all format extensions currently supported by this img element - pub fn extensions() -> &'static [&'static str] { - // This is the list in [image::ImageFormat::from_extension] + `svg` - &[ - "avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico", - "hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg", - ] - } - - /// Sets the image cache for the current node. - /// - /// If the `image_cache` is not explicitly provided, the function will determine the image cache by: - /// - /// 1. Checking if any ancestor node of the current node contains an `ImageCacheElement`, If such a node exists, the image cache specified by that ancestor will be used. - /// 2. If no ancestor node contains an `ImageCacheElement`, the global image cache will be used as a fallback. - /// - /// This mechanism provides a flexible way to manage image caching, allowing precise control when needed, - /// while ensuring a default behavior when no cache is explicitly specified. - #[inline] - pub fn image_cache(self, image_cache: &Entity) -> Self { - Self { - image_cache: Some(image_cache.clone().into()), - ..self - } - } -} - -impl Deref for Stateful { - type Target = Img; - - fn deref(&self) -> &Self::Target { - &self.element - } -} - -impl DerefMut for Stateful { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.element - } -} - -/// The image state between frames -struct ImgState { - frame_index: usize, - last_frame_time: Option, - started_loading: Option<(Instant, Task<()>)>, -} - -/// The image layout state between frames -pub struct ImgLayoutState { - frame_index: usize, - replacement: Option, -} - -impl Element for Img { - type RequestLayoutState = ImgLayoutState; - type PrepaintState = Option; - - fn id(&self) -> Option { - self.interactivity.element_id.clone() - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - self.interactivity.source_location() - } - - fn request_layout( - &mut self, - global_id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - let mut layout_state = ImgLayoutState { - frame_index: 0, - replacement: None, - }; - - window.with_optional_element_state(global_id, |state, window| { - let mut state = state.map(|state| { - state.unwrap_or(ImgState { - frame_index: 0, - last_frame_time: None, - started_loading: None, - }) - }); - - let mut frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0); - - let layout_id = self.interactivity.request_layout( - global_id, - inspector_id, - window, - cx, - |mut style, window, cx| { - let mut replacement_id = None; - - match self.source.use_data( - self.image_cache - .clone() - .or_else(|| window.image_cache_stack.last().cloned()), - window, - cx, - ) { - Some(Ok(data)) => { - let frame_count = data.frame_count(); - let max_frame_index = frame_count.saturating_sub(1); - - if let Some(state) = &mut state { - state.frame_index = state.frame_index.min(max_frame_index); - if frame_count > 1 && !cx.reduce_motion() { - if window.is_window_active() { - let current_time = Instant::now(); - if let Some(last_frame_time) = state.last_frame_time { - let elapsed = current_time - last_frame_time; - let frame_duration = - Duration::from(data.delay(state.frame_index)); - - if elapsed >= frame_duration { - state.frame_index = - (state.frame_index + 1) % frame_count; - state.last_frame_time = - Some(current_time - (elapsed - frame_duration)); - } - } else { - state.last_frame_time = Some(current_time); - } - } else { - state.last_frame_time = None; - } - } else { - state.last_frame_time = None; - } - state.started_loading = None; - frame_index = state.frame_index; - } - - let image_size = data.render_size(frame_index); - - if style.aspect_ratio.is_none() { - style.aspect_ratio = Some(image_size.width / image_size.height); - } - - if let Length::Auto = style.size.width { - style.size.width = match style.size.height { - Length::Definite(DefiniteLength::Absolute(abs_length)) => { - let height_px = abs_length.to_pixels(window.rem_size()); - Length::Definite( - px(image_size.width.0 * height_px.0 - / image_size.height.0) - .into(), - ) - } - _ => Length::Definite(image_size.width.into()), - }; - } - - if let Length::Auto = style.size.height { - style.size.height = match style.size.width { - Length::Definite(DefiniteLength::Absolute(abs_length)) => { - let width_px = abs_length.to_pixels(window.rem_size()); - Length::Definite( - px(image_size.height.0 * width_px.0 - / image_size.width.0) - .into(), - ) - } - _ => Length::Definite(image_size.height.into()), - }; - } - - if global_id.is_some() - && data.frame_count() > 1 - && window.is_window_active() - && !cx.reduce_motion() - { - window.request_animation_frame(); - } - } - Some(_err) => { - if let Some(fallback) = self.style.fallback.as_ref() { - let mut element = fallback(); - replacement_id = Some(element.request_layout(window, cx)); - layout_state.replacement = Some(element); - } - if let Some(state) = &mut state { - state.started_loading = None; - } - } - None => { - if let Some(state) = &mut state { - if let Some((started_loading, _)) = state.started_loading { - if started_loading.elapsed() > LOADING_DELAY - && let Some(loading) = self.style.loading.as_ref() - { - let mut element = loading(); - replacement_id = Some(element.request_layout(window, cx)); - layout_state.replacement = Some(element); - } - } else { - let current_view = window.current_view(); - let task = window.spawn(cx, async move |cx| { - cx.background_executor().timer(LOADING_DELAY).await; - cx.update(move |_, cx| { - cx.notify(current_view); - }) - .ok(); - }); - state.started_loading = Some((Instant::now(), task)); - } - } - } - } - - window.request_layout(style, replacement_id, cx) - }, - ); - - layout_state.frame_index = frame_index; - - ((layout_id, layout_state), state) - }) - } - - fn prepaint( - &mut self, - global_id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState { - self.interactivity.prepaint( - global_id, - inspector_id, - bounds, - bounds.size, - window, - cx, - |_, _, hitbox, window, cx| { - if let Some(replacement) = &mut request_layout.replacement { - replacement.prepaint(window, cx); - } - - hitbox - }, - ) - } - - fn paint( - &mut self, - global_id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - layout_state: &mut Self::RequestLayoutState, - hitbox: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - let source = self.source.clone(); - self.interactivity.paint( - global_id, - inspector_id, - bounds, - hitbox.as_ref(), - window, - cx, - |style, window, cx| { - if let Some(Ok(data)) = source.use_data( - self.image_cache - .clone() - .or_else(|| window.image_cache_stack.last().cloned()), - window, - cx, - ) { - if data.frame_count() == 0 { - return; - } - let new_bounds = self - .style - .object_fit - .get_bounds(bounds, data.size(layout_state.frame_index)); - let corner_radii = style.corner_radii.to_pixels(window.rem_size()); - window - .paint_image( - bounds, - new_bounds, - corner_radii, - data, - layout_state.frame_index, - self.style.grayscale, - ) - .log_err(); - } else if let Some(replacement) = &mut layout_state.replacement { - replacement.paint(window, cx); - } - }, - ) - } -} - -impl Styled for Img { - fn style(&mut self) -> &mut StyleRefinement { - &mut self.interactivity.base_style - } -} - -impl InteractiveElement for Img { - fn interactivity(&mut self) -> &mut Interactivity { - &mut self.interactivity - } -} - -impl IntoElement for Img { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -impl StatefulInteractiveElement for Img {} - -impl ImageSource { - pub(crate) fn use_data( - &self, - cache: Option, - window: &mut Window, - cx: &mut App, - ) -> Option, ImageCacheError>> { - match self { - ImageSource::Resource(resource) => { - if let Some(cache) = cache { - cache.load(resource, window, cx) - } else { - window.use_asset::(resource, cx) - } - } - ImageSource::Custom(loading_fn) => loading_fn(window, cx), - ImageSource::Render(data) => Some(Ok(data.to_owned())), - ImageSource::Image(data) => window.use_asset::>(data, cx), - } - } - - pub(crate) fn get_data( - &self, - cache: Option, - window: &mut Window, - cx: &mut App, - ) -> Option, ImageCacheError>> { - match self { - ImageSource::Resource(resource) => { - if let Some(cache) = cache { - cache.load(resource, window, cx) - } else { - window.get_asset::(resource, cx) - } - } - ImageSource::Custom(loading_fn) => loading_fn(window, cx), - ImageSource::Render(data) => Some(Ok(data.to_owned())), - ImageSource::Image(data) => window.get_asset::>(data, cx), - } - } - - /// Remove this image source from the asset system - pub fn remove_asset(&self, cx: &mut App) { - match self { - ImageSource::Resource(resource) => { - cx.remove_asset::(resource); - } - ImageSource::Custom(_) | ImageSource::Render(_) => {} - ImageSource::Image(data) => cx.remove_asset::>(data), - } - } - - /// Check whether this image source is present in the asset system (loading - /// or loaded), without fetching it. - #[cfg(any(test, feature = "test-support"))] - pub fn is_asset_cached(&self, cx: &App) -> bool { - match self { - ImageSource::Resource(resource) => cx.has_asset::(resource), - ImageSource::Custom(_) | ImageSource::Render(_) => false, - ImageSource::Image(data) => cx.has_asset::>(data), - } - } -} - -#[derive(Clone)] -enum ImageDecoder {} - -impl Asset for ImageDecoder { - type Source = Arc; - type Output = Result, ImageCacheError>; - - fn load( - source: Self::Source, - cx: &mut App, - ) -> impl Future + Send + 'static { - let renderer = cx.svg_renderer(); - async move { source.to_image_data(renderer).map_err(Into::into) } - } -} - -/// An image loader for the GPUI asset system -#[derive(Clone)] -pub enum ImageAssetLoader {} - -impl Asset for ImageAssetLoader { - type Source = Resource; - type Output = Result, ImageCacheError>; - - fn load( - source: Self::Source, - cx: &mut App, - ) -> impl Future + Send + 'static { - let client = cx.http_client(); - // TODO: Can we make SVGs always rescale? - // let scale_factor = cx.scale_factor(); - let svg_renderer = cx.svg_renderer(); - let asset_source = cx.asset_source().clone(); - async move { - let bytes = match source.clone() { - Resource::Path(uri) => fs::read(uri.as_ref())?, - Resource::Uri(uri) => { - use anyhow::Context as _; - use futures::AsyncReadExt as _; - - let mut response = client - .get(uri.as_ref(), ().into(), true) - .await - .with_context(|| format!("loading image asset from {uri:?}"))?; - let mut body = Vec::new(); - response.body_mut().read_to_end(&mut body).await?; - if !response.status().is_success() { - let mut body = String::from_utf8_lossy(&body).into_owned(); - let first_line = body.lines().next().unwrap_or("").trim_end(); - body.truncate(first_line.len()); - return Err(ImageCacheError::BadStatus { - uri, - status: response.status(), - body, - }); - } - body - } - Resource::Embedded(path) => { - let data = asset_source.load(&path).ok().flatten(); - if let Some(data) = data { - data.to_vec() - } else { - return Err(ImageCacheError::Asset( - format!("Embedded resource not found: {}", path).into(), - )); - } - } - }; - - if let Ok(format) = image::guess_format(&bytes) { - let data = match format { - ImageFormat::Gif => { - let decoder = GifDecoder::new(Cursor::new(&bytes))?; - let mut frames = SmallVec::new(); - - for frame in decoder.into_frames() { - match frame { - Ok(mut frame) => { - // Convert from RGBA to BGRA. - for pixel in frame.buffer_mut().chunks_exact_mut(4) { - pixel.swap(0, 2); - } - frames.push(frame); - } - Err(err) => { - log::debug!( - "Skipping GIF frame in {source:?} due to decode error: {err}" - ); - } - } - } - - if frames.is_empty() { - return Err(ImageCacheError::Other(Arc::new(anyhow::anyhow!( - "GIF could not be decoded: all frames failed ({source:?})" - )))); - } - - frames - } - ImageFormat::WebP => { - let mut decoder = WebPDecoder::new(Cursor::new(&bytes))?; - - if decoder.has_animation() { - let _ = decoder.set_background_color(Rgba([0, 0, 0, 0])); - let mut frames = SmallVec::new(); - - for frame in decoder.into_frames() { - match frame { - Ok(mut frame) => { - // Convert from RGBA to BGRA. - for pixel in frame.buffer_mut().chunks_exact_mut(4) { - pixel.swap(0, 2); - } - frames.push(frame); - } - Err(err) => { - log::debug!( - "Skipping WebP frame in {source:?} due to decode error: {err}" - ); - } - } - } - - if frames.is_empty() { - return Err(ImageCacheError::Other(Arc::new(anyhow::anyhow!( - "WebP could not be decoded: all frames failed ({source:?})" - )))); - } - - frames - } else { - decode_static_image_from_decoder(decoder)? - } - } - _ => decode_static_image(&bytes, format)?, - }; - - Ok(Arc::new(RenderImage::new(data))) - } else { - svg_renderer - .render_single_frame(&bytes, 1.0) - .map_err(Into::into) - } - } - } -} - -/// An error that can occur when interacting with the image cache. -#[derive(Debug, Error, Clone)] -pub enum ImageCacheError { - /// Some other kind of error occurred - #[error("error: {0}")] - Other(#[from] Arc), - /// An error that occurred while reading the image from disk. - #[error("IO error: {0}")] - Io(Arc), - /// An error that occurred while processing an image. - #[error("unexpected http status for {uri}: {status}, body: {body}")] - BadStatus { - /// The URI of the image. - uri: SharedUri, - /// The HTTP status code. - status: http_client::StatusCode, - /// The HTTP response body. - body: String, - }, - /// An error that occurred while processing an asset. - #[error("asset error: {0}")] - Asset(SharedString), - /// An error that occurred while processing an image. - #[error("image error: {0}")] - Image(Arc), - /// An error that occurred while processing an SVG. - #[error("svg error: {0}")] - Usvg(Arc), -} - -impl From for ImageCacheError { - fn from(value: anyhow::Error) -> Self { - Self::Other(Arc::new(value)) - } -} - -impl From for ImageCacheError { - fn from(value: io::Error) -> Self { - Self::Io(Arc::new(value)) - } -} - -impl From for ImageCacheError { - fn from(value: usvg::Error) -> Self { - Self::Usvg(Arc::new(value)) - } -} - -impl From for ImageCacheError { - fn from(value: image::ImageError) -> Self { - Self::Image(Arc::new(value)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ParentElement as _, TestAppContext, canvas, div, point, px, size}; - use image::{Frame, ImageBuffer, Rgba}; - - const TEST_IMG_ID: &str = "test-img"; - - fn test_image(frame_count: usize) -> Arc { - let frame = Frame::new(ImageBuffer::from_pixel(1, 1, Rgba([0, 0, 0, 0]))); - Arc::new(RenderImage::new(SmallVec::from_iter( - (0..frame_count).map(|_| frame.clone()), - ))) - } - - fn test_image_with_size(width: u32, height: u32) -> Arc { - let frame = Frame::new(ImageBuffer::from_pixel(width, height, Rgba([0, 0, 0, 0]))); - Arc::new(RenderImage::new(SmallVec::from_elem(frame, 1))) - } - - /// Overwrites the cached `frame_index` of the sibling `img` during paint. - fn seed_frame_index(frame_index: usize) -> impl IntoElement { - canvas( - |_, _, _| (), - move |_, _, window, _| { - window.with_global_id(TEST_IMG_ID.into(), |id, window| { - window.with_element_state::(id, |state, _| { - let mut state = state.expect("img state should be initialized"); - state.frame_index = frame_index; - ((), state) - }); - }); - }, - ) - } - - #[gpui::test] - fn zero_frame_image_does_not_panic_on_paint(cx: &mut TestAppContext) { - cx.add_empty_window() - .draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| { - img(ImageSource::Render(test_image(0))).into_any_element() - }); - } - - #[gpui::test] - fn image_object_fit_cover_crops_to_element_bounds(cx: &mut TestAppContext) { - let window = cx.add_empty_window(); - let image = test_image_with_size(200, 100); - window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| { - img(ImageSource::Render(image.clone())) - .size_full() - .object_fit(ObjectFit::Fill) - .into_any_element() - }); - let full_tile_bounds = window.update(|window, _| { - window - .rendered_frame - .scene - .polychrome_sprites - .last() - .expect("fill image should paint a sprite") - .tile - .bounds - }); - - window.draw(point(px(10.), px(20.)), size(px(100.), px(100.)), |_, _| { - img(ImageSource::Render(image)) - .size_full() - .object_fit(ObjectFit::Cover) - .into_any_element() - }); - - let (rendered_bounds, rendered_tile_bounds, scale_factor) = window.update(|window, _| { - let sprite = window - .rendered_frame - .scene - .polychrome_sprites - .last() - .expect("cover image should paint a sprite"); - (sprite.bounds, sprite.tile.bounds, window.scale_factor()) - }); - assert_eq!( - rendered_bounds, - Bounds { - origin: point(px(10.).scale(scale_factor), px(20.).scale(scale_factor)), - size: size(px(100.).scale(scale_factor), px(100.).scale(scale_factor)), - } - ); - assert_eq!( - ( - rendered_tile_bounds.origin.x.0 - full_tile_bounds.origin.x.0, - rendered_tile_bounds.origin.y.0 - full_tile_bounds.origin.y.0, - rendered_tile_bounds.size.width.0, - rendered_tile_bounds.size.height.0, - ), - (50, 0, 100, 100), - ); - } - - #[gpui::test] - fn explicit_aspect_ratio_is_not_overridden_by_intrinsic_ratio(cx: &mut TestAppContext) { - let window = cx.add_empty_window(); - - // A portrait image in a square container - window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| { - div() - .size(px(100.)) - .overflow_hidden() - .child( - img(ImageSource::Render(test_image_with_size(100, 200))) - .size_full() - .aspect_square() - .object_fit(ObjectFit::Contain), - ) - .into_any_element() - }); - - let (rendered_bounds, scale_factor) = window.update(|window, _| { - let sprite = window - .rendered_frame - .scene - .polychrome_sprites - .last() - .expect("contained image should paint a sprite"); - (sprite.bounds, window.scale_factor()) - }); - - // The element stays 100x100, so the image is letterboxed horizontally - assert_eq!( - rendered_bounds, - Bounds { - origin: point(px(25.).scale(scale_factor), px(0.).scale(scale_factor)), - size: size(px(50.).scale(scale_factor), px(100.).scale(scale_factor)), - } - ); - } - - #[gpui::test] - fn image_object_fit_cover_clamps_corner_radii_to_visible_bounds(cx: &mut TestAppContext) { - let window = cx.add_empty_window(); - window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| { - img(ImageSource::Render(test_image_with_size(200, 100))) - .size_full() - .rounded(px(100.)) - .object_fit(ObjectFit::Cover) - .into_any_element() - }); - - let (corner_radius, expected_corner_radius) = window.update(|window, _| { - ( - window - .rendered_frame - .scene - .polychrome_sprites - .last() - .map(|sprite| sprite.corner_radii.top_left), - px(50.).scale(window.scale_factor()), - ) - }); - assert_eq!(corner_radius, Some(expected_corner_radius)); - } - - #[gpui::test] - fn stale_frame_index_is_clamped_when_image_changes(cx: &mut TestAppContext) { - let window = cx.add_empty_window(); - - // Assert that a cached frame_index from a previous multi-frame image - // does not cause an out-of-bounds panic when the image is replaced - // with one that has fewer frames. - window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| { - div() - .child(img(ImageSource::Render(test_image(5))).id(TEST_IMG_ID)) - .child(seed_frame_index(4)) - .into_any_element() - }); - window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| { - img(ImageSource::Render(test_image(1))) - .id(TEST_IMG_ID) - .into_any_element() - }); - } -} diff --git a/crates/gpui_pre/src/elements/list.rs b/crates/gpui_pre/src/elements/list.rs deleted file mode 100644 index 730aa6e..0000000 --- a/crates/gpui_pre/src/elements/list.rs +++ /dev/null @@ -1,2985 +0,0 @@ -//! A list element that can be used to render a large number of differently sized elements -//! efficiently. Clients of this API need to ensure that elements outside of the scrolled -//! area do not change their height for this element to function correctly. If your elements -//! do change height, notify the list element via [`ListState::splice`] or [`ListState::reset`]. -//! In order to minimize re-renders, this element's state is stored intrusively -//! on your own views, so that your code can coordinate directly with the list element's cached state. -//! -//! If all of your elements are the same height, see [`crate::UniformList`] for a simpler API - -use crate::{ - AnyElement, App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, Element, EntityId, - FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, - Overflow, Pixels, Point, ScrollDelta, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, - Window, point, px, size, -}; -use collections::VecDeque; -use refineable::Refineable as _; -use std::{cell::RefCell, ops::Range, rc::Rc}; -use sum_tree::{Bias, Dimensions, SumTree}; - -type RenderItemFn = dyn FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static; - -/// Construct a new list element -pub fn list( - state: ListState, - render_item: impl FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static, -) -> List { - List { - state, - render_item: Box::new(render_item), - style: StyleRefinement::default(), - sizing_behavior: ListSizingBehavior::default(), - } -} - -/// A list element -pub struct List { - state: ListState, - render_item: Box, - style: StyleRefinement, - sizing_behavior: ListSizingBehavior, -} - -impl List { - /// Set the sizing behavior for the list. - pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self { - self.sizing_behavior = behavior; - self - } -} - -/// The list state that views must hold on behalf of the list element. -#[derive(Clone)] -pub struct ListState(Rc>); - -impl std::fmt::Debug for ListState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("ListState") - } -} - -struct StateInner { - last_layout_bounds: Option>, - last_padding: Option>, - items: SumTree, - logical_scroll_top: Option, - alignment: ListAlignment, - overdraw: Pixels, - reset: bool, - #[allow(clippy::type_complexity)] - scroll_handler: Option>, - scrollbar_drag_start_height: Option, - measuring_behavior: ListMeasuringBehavior, - pending_scroll: Option, - follow_state: FollowState, -} - -/// Deferred scroll adjustment applied after the scroll-top item has been remeasured. -/// -/// An absolute pending scroll preserves the same pixel offset into the item, which keeps -/// visible text stable while content is appended to or removed from that item. A -/// proportional pending scroll preserves the same fractional position within the item, -/// which is useful when the whole list is being resized and each item scales similarly. -#[derive(Clone)] -enum PendingScroll { - /// Preserve the same pixel offset into the item after it is remeasured. - Absolute { item_ix: usize, offset: Pixels }, - /// Preserve the same fractional offset into the item after it is remeasured. - Proportional(PendingScrollFraction), -} - -/// Keeps track of a fractional scroll position within an item for restoration -/// after remeasurement. -#[derive(Clone)] -struct PendingScrollFraction { - /// The index of the item to scroll within. - item_ix: usize, - /// Fractional offset (0.0 to 1.0) within the item's height. - fraction: f32, -} - -/// Determines how remeasurement preserves the scroll position when the scroll-top item -/// changes height. -enum ScrollAnchor { - /// Preserve the same pixel offset into the scroll-top item. - Absolute, - /// Preserve the same fractional position within the scroll-top item. - Proportional, -} - -/// Controls whether the list automatically follows new content at the end. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -pub enum FollowMode { - /// Normal scrolling — no automatic following. - #[default] - Normal, - /// The list should auto-scroll along with the tail, when scrolled to bottom. - Tail, -} - -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -enum FollowState { - #[default] - Normal, - Tail { - is_following: bool, - }, -} - -impl FollowState { - fn is_following(&self) -> bool { - matches!(self, FollowState::Tail { is_following: true }) - } - - fn has_stopped_following(&self) -> bool { - matches!( - self, - FollowState::Tail { - is_following: false - } - ) - } - - fn start_following(&mut self) { - if let FollowState::Tail { - is_following: false, - } = self - { - *self = FollowState::Tail { is_following: true }; - } - } - - fn stop_following(&mut self) { - if let FollowState::Tail { is_following: true } = self { - *self = FollowState::Tail { - is_following: false, - }; - } - } -} - -/// Whether the list is scrolling from top to bottom or bottom to top. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum ListAlignment { - /// The list is scrolling from top to bottom, like most lists. - Top, - /// The list is scrolling from bottom to top, like a chat log. - Bottom, -} - -/// A scroll event that has been converted to be in terms of the list's items. -pub struct ListScrollEvent { - /// The range of items currently visible in the list, after applying the scroll event. - pub visible_range: Range, - - /// The number of items that are currently visible in the list, after applying the scroll event. - pub count: usize, - - /// Whether the list has been scrolled. - pub is_scrolled: bool, - - /// Whether the list is currently in follow-tail mode (auto-scrolling to end). - pub is_following_tail: bool, -} - -/// The sizing behavior to apply during layout. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum ListSizingBehavior { - /// The list should calculate its size based on the size of its items. - Infer, - /// The list should not calculate a fixed size. - #[default] - Auto, -} - -/// The measuring behavior to apply during layout. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum ListMeasuringBehavior { - /// Measure all items in the list. - /// Note: This can be expensive for the first frame in a large list. - Measure(bool), - /// Only measure visible items - #[default] - Visible, -} - -impl ListMeasuringBehavior { - fn reset(&mut self) { - match self { - ListMeasuringBehavior::Measure(has_measured) => *has_measured = false, - ListMeasuringBehavior::Visible => {} - } - } -} - -/// The horizontal sizing behavior to apply during layout. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum ListHorizontalSizingBehavior { - /// List items' width can never exceed the width of the list. - #[default] - FitList, - /// List items' width may go over the width of the list, if any item is wider. - Unconstrained, -} - -struct LayoutItemsResponse { - max_item_width: Pixels, - scroll_top: ListOffset, - item_layouts: VecDeque, -} - -struct ItemLayout { - index: usize, - element: AnyElement, - size: Size, -} - -/// Frame state used by the [List] element after layout. -pub struct ListPrepaintState { - hitbox: Hitbox, - layout: LayoutItemsResponse, -} - -#[derive(Clone)] -enum ListItem { - Unmeasured { - size_hint: Option>, - focus_handle: Option, - }, - Measured { - size: Size, - focus_handle: Option, - }, -} - -impl ListItem { - fn size(&self) -> Option> { - if let ListItem::Measured { size, .. } = self { - Some(*size) - } else { - None - } - } - - fn size_hint(&self) -> Option> { - match self { - ListItem::Measured { size, .. } => Some(*size), - ListItem::Unmeasured { size_hint, .. } => *size_hint, - } - } - - fn focus_handle(&self) -> Option { - match self { - ListItem::Unmeasured { focus_handle, .. } | ListItem::Measured { focus_handle, .. } => { - focus_handle.clone() - } - } - } - - fn contains_focused(&self, window: &Window, cx: &App) -> bool { - match self { - ListItem::Unmeasured { focus_handle, .. } | ListItem::Measured { focus_handle, .. } => { - focus_handle - .as_ref() - .is_some_and(|handle| handle.contains_focused(window, cx)) - } - } - } -} - -#[derive(Clone, Debug, Default, PartialEq)] -struct ListItemSummary { - count: usize, - rendered_count: usize, - unrendered_count: usize, - height: Pixels, - has_focus_handles: bool, - has_unknown_height: bool, -} - -#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] -struct Count(usize); - -#[derive(Clone, Debug, Default)] -struct Height(Pixels); - -impl ListState { - /// Construct a new list state, for storage on a view. - /// - /// The overdraw parameter controls how much extra space is rendered - /// above and below the visible area. Elements within this area will - /// be measured even though they are not visible. This can help ensure - /// that the list doesn't flicker or pop in when scrolling. - pub fn new(item_count: usize, alignment: ListAlignment, overdraw: Pixels) -> Self { - let this = Self(Rc::new(RefCell::new(StateInner { - last_layout_bounds: None, - last_padding: None, - items: SumTree::default(), - logical_scroll_top: None, - alignment, - overdraw, - scroll_handler: None, - reset: false, - scrollbar_drag_start_height: None, - measuring_behavior: ListMeasuringBehavior::default(), - pending_scroll: None, - follow_state: FollowState::default(), - }))); - this.splice(0..0, item_count); - this - } - - /// Set the list to measure all items in the list in the first layout phase. - /// - /// This is useful for ensuring that the scrollbar size is correct instead of based on only rendered elements. - pub fn measure_all(self) -> Self { - self.0.borrow_mut().measuring_behavior = ListMeasuringBehavior::Measure(false); - self - } - - /// Pre-populate every unmeasured item with a uniform height hint so the scrollbar thumb - /// is correctly sized from the first frame, without measuring all items up front. - /// - /// As items are actually rendered their real heights replace the hint, so the scrollbar - /// converges to the exact size over time. This is a cheaper alternative to [`Self::measure_all`] - /// for lists where items have roughly uniform heights (e.g. table rows). - pub fn with_uniform_item_height(self, height: Pixels) -> Self { - self.apply_uniform_item_height(height); - self - } - - /// Reset this instantiation of the list state. - /// - /// Note that this will cause scroll events to be dropped until the next paint. - pub fn reset(&self, element_count: usize) { - let old_count = { - let state = &mut *self.0.borrow_mut(); - state.reset = true; - state.measuring_behavior.reset(); - state.logical_scroll_top = None; - state.pending_scroll = None; - state.scrollbar_drag_start_height = None; - state.items.summary().count - }; - - self.splice(0..old_count, element_count); - } - - /// Reset the list to `element_count` items, pre-populating every item with a - /// uniform height hint so the scrollbar thumb is correctly sized from the first - /// frame even for off-screen items. - pub fn reset_with_uniform_height(&self, element_count: usize, height: Pixels) { - self.reset(element_count); - self.apply_uniform_item_height(height); - } - - fn apply_uniform_item_height(&self, height: Pixels) { - let size_hint = Size { - width: px(0.), - height, - }; - let mut state = self.0.borrow_mut(); - let new_items = state - .items - .iter() - .map(|item| ListItem::Unmeasured { - size_hint: Some(item.size_hint().unwrap_or(size_hint)), - focus_handle: item.focus_handle(), - }) - .collect::>(); - let mut tree = SumTree::default(); - tree.extend(new_items, ()); - state.items = tree; - } - - /// Remeasure all items while preserving proportional scroll position. - /// - /// Use this when item heights may have changed (e.g., font size changes) - /// but the number and identity of items remains the same. - pub fn remeasure(&self) { - let count = self.item_count(); - self.remeasure_items_with_scroll_anchor(0..count, ScrollAnchor::Proportional); - } - - /// Mark items in `range` as needing remeasurement while preserving - /// the current scroll position. Unlike [`Self::splice`], this does - /// not change the number of items or blow away `logical_scroll_top`. - /// - /// Use this when an item's content has changed and its rendered - /// height may be different (e.g., streaming text, tool results - /// loading), but the item itself still exists at the same index. - pub fn remeasure_items(&self, range: Range) { - self.remeasure_items_with_scroll_anchor(range, ScrollAnchor::Absolute); - } - - fn remeasure_items_with_scroll_anchor(&self, range: Range, scroll_anchor: ScrollAnchor) { - let state = &mut *self.0.borrow_mut(); - - if let Some(scroll_top) = state.logical_scroll_top { - if range.contains(&scroll_top.item_ix) { - state.pending_scroll = match scroll_anchor { - ScrollAnchor::Absolute => Some(PendingScroll::Absolute { - item_ix: scroll_top.item_ix, - offset: scroll_top.offset_in_item, - }), - ScrollAnchor::Proportional => { - // If the scroll-top item falls within the remeasured range, - // store a fractional offset so the layout can restore the - // proportional scroll position after the item is re-rendered - // at its new height. - let mut cursor = state.items.cursor::(()); - cursor.seek(&Count(scroll_top.item_ix), Bias::Right); - - cursor - .item() - .and_then(|item| { - item.size().map(|size| { - let fraction = if size.height.0 > 0.0 { - (scroll_top.offset_in_item.0 / size.height.0) - .clamp(0.0, 1.0) - } else { - 0.0 - }; - - PendingScroll::Proportional(PendingScrollFraction { - item_ix: scroll_top.item_ix, - fraction, - }) - }) - }) - .or_else(|| state.pending_scroll.clone()) - } - }; - } - } - - // Rebuild the tree, replacing items in the range with - // Unmeasured copies that keep their focus handles. - let new_items = { - let mut cursor = state.items.cursor::(()); - let mut new_items = cursor.slice(&Count(range.start), Bias::Right); - let invalidated = cursor.slice(&Count(range.end), Bias::Right); - new_items.extend( - invalidated.iter().map(|item| ListItem::Unmeasured { - size_hint: item.size_hint(), - focus_handle: item.focus_handle(), - }), - (), - ); - new_items.append(cursor.suffix(), ()); - new_items - }; - state.items = new_items; - state.measuring_behavior.reset(); - } - - /// The number of items in this list. - pub fn item_count(&self) -> usize { - self.0.borrow().items.summary().count - } - - /// Whether the list is scrolled to the end, or `None` if the list is - /// not scrollable or the total content height is not yet known. - pub fn is_scrolled_to_end(&self) -> Option { - let state = self.0.borrow(); - let bounds = state.last_layout_bounds?; - let summary = state.items.summary(); - if summary.has_unknown_height { - return None; - } - let padding = state.last_padding.unwrap_or_default(); - let content_height = summary.height + padding.top + padding.bottom; - let scroll_max = (content_height - bounds.size.height).max(px(0.)); - if scroll_max <= px(0.) { - return None; - } - let scroll_top = state.scroll_top(&state.logical_scroll_top()); - Some(scroll_top >= scroll_max) - } - - /// Inform the list state that the items in `old_range` have been replaced - /// by `count` new items that must be recalculated. - pub fn splice(&self, old_range: Range, count: usize) { - self.splice_focusable(old_range, (0..count).map(|_| None)) - } - - /// Register with the list state that the items in `old_range` have been replaced - /// by new items. As opposed to [`Self::splice`], this method allows an iterator of optional focus handles - /// to be supplied to properly integrate with items in the list that can be focused. If a focused item - /// is scrolled out of view, the list will continue to render it to allow keyboard interaction. - pub fn splice_focusable( - &self, - old_range: Range, - focus_handles: impl IntoIterator>, - ) { - let state = &mut *self.0.borrow_mut(); - - let mut old_items = state.items.cursor::(()); - let mut new_items = old_items.slice(&Count(old_range.start), Bias::Right); - old_items.seek_forward(&Count(old_range.end), Bias::Right); - - let mut spliced_count = 0; - new_items.extend( - focus_handles.into_iter().map(|focus_handle| { - spliced_count += 1; - ListItem::Unmeasured { - size_hint: None, - focus_handle, - } - }), - (), - ); - new_items.append(old_items.suffix(), ()); - drop(old_items); - state.items = new_items; - - if let Some(ListOffset { - item_ix, - offset_in_item, - }) = state.logical_scroll_top.as_mut() - { - if old_range.contains(item_ix) { - *item_ix = old_range.start; - *offset_in_item = px(0.); - } else if old_range.end <= *item_ix { - *item_ix = *item_ix - (old_range.end - old_range.start) + spliced_count; - } - } - } - - /// Set a handler that will be called when the list is scrolled. - pub fn set_scroll_handler( - &self, - handler: impl FnMut(&ListScrollEvent, &mut Window, &mut App) + 'static, - ) { - self.0.borrow_mut().scroll_handler = Some(Box::new(handler)) - } - - /// Get the current scroll offset, in terms of the list's items. - pub fn logical_scroll_top(&self) -> ListOffset { - self.0.borrow().logical_scroll_top() - } - - /// Scroll the list by the given offset - pub fn scroll_by(&self, distance: Pixels) { - if distance == px(0.) { - return; - } - - let current_offset = self.logical_scroll_top(); - let state = &mut *self.0.borrow_mut(); - - if distance < px(0.) { - state.follow_state.stop_following(); - } - - let mut cursor = state.items.cursor::(()); - cursor.seek(&Count(current_offset.item_ix), Bias::Right); - - let start_pixel_offset = cursor.start().height + current_offset.offset_in_item; - let new_pixel_offset = (start_pixel_offset + distance).max(px(0.)); - if new_pixel_offset > start_pixel_offset { - cursor.seek_forward(&Height(new_pixel_offset), Bias::Right); - } else { - cursor.seek(&Height(new_pixel_offset), Bias::Right); - } - - let scroll_top = ListOffset { - item_ix: cursor.start().count, - offset_in_item: new_pixel_offset - cursor.start().height, - }; - drop(cursor); - state.rebase_pending_scroll(scroll_top); - state.logical_scroll_top = Some(scroll_top); - } - - /// Scroll the list to the very end (past the last item). - /// - /// Unlike [`scroll_to_reveal_item`], this uses the total item count as the - /// anchor, so the list's layout pass will walk backwards from the end and - /// always show the bottom of the last item — even when that item is still - /// growing (e.g. during streaming). - pub fn scroll_to_end(&self) { - let state = &mut *self.0.borrow_mut(); - let item_count = state.items.summary().count; - state.pending_scroll = None; - state.logical_scroll_top = Some(ListOffset { - item_ix: item_count, - offset_in_item: px(0.), - }); - } - - /// Set the follow mode for the list. In `Tail` mode, the list - /// will auto-scroll to the end and re-engage after the user - /// scrolls back to the bottom. In `Normal` mode, no automatic - /// following occurs. - pub fn set_follow_mode(&self, mode: FollowMode) { - let state = &mut *self.0.borrow_mut(); - - match mode { - FollowMode::Normal => { - state.follow_state = FollowState::Normal; - } - FollowMode::Tail => { - state.follow_state = FollowState::Tail { is_following: true }; - if matches!(mode, FollowMode::Tail) { - let item_count = state.items.summary().count; - state.logical_scroll_top = Some(ListOffset { - item_ix: item_count, - offset_in_item: px(0.), - }); - } - } - } - } - - /// Pause tail-following, freezing the list at its current scroll - /// position. Unlike [`Self::set_follow_mode`] with [`FollowMode::Normal`], - /// this keeps the list in `Tail` mode, so it will resume following - /// automatically once the view returns to the bottom. No-op when the list - /// isn't currently following. - /// - /// Useful when something other than the user grows an item (e.g. zooming a - /// diagram) and the current position should stay put rather than snapping - /// to the end. - pub fn pause_following_tail(&self) { - self.0.borrow_mut().follow_state.stop_following(); - } - - /// Returns whether the list is currently actively following the - /// tail (snapping to the end on each layout). - pub fn is_following_tail(&self) -> bool { - matches!( - self.0.borrow().follow_state, - FollowState::Tail { is_following: true } - ) - } - - /// Scroll the list to the given offset - pub fn scroll_to(&self, mut scroll_top: ListOffset) { - let state = &mut *self.0.borrow_mut(); - let item_count = state.items.summary().count; - if scroll_top.item_ix >= item_count { - scroll_top.item_ix = item_count; - scroll_top.offset_in_item = px(0.); - } - - if scroll_top.item_ix < item_count { - state.follow_state.stop_following(); - } - - state.rebase_pending_scroll(scroll_top); - state.logical_scroll_top = Some(scroll_top); - } - - /// Scroll the list to the given item, such that the item is fully visible. - pub fn scroll_to_reveal_item(&self, ix: usize) { - let state = &mut *self.0.borrow_mut(); - - let mut scroll_top = state.logical_scroll_top(); - let height = state - .last_layout_bounds - .map_or(px(0.), |bounds| bounds.size.height); - let padding = state.last_padding.unwrap_or_default(); - - if ix <= scroll_top.item_ix { - scroll_top.item_ix = ix; - scroll_top.offset_in_item = px(0.); - } else { - let mut cursor = state.items.cursor::(()); - cursor.seek(&Count(ix + 1), Bias::Right); - let bottom = cursor.start().height + padding.top; - let goal_top = px(0.).max(bottom - height + padding.bottom); - - cursor.seek(&Height(goal_top), Bias::Left); - let start_ix = cursor.start().count; - let start_item_top = cursor.start().height; - - if start_ix >= scroll_top.item_ix { - scroll_top.item_ix = start_ix; - scroll_top.offset_in_item = goal_top - start_item_top; - } - } - - state.rebase_pending_scroll(scroll_top); - state.logical_scroll_top = Some(scroll_top); - } - - /// Get the bounds for the given item in window coordinates, if it's - /// been rendered. - pub fn bounds_for_item(&self, ix: usize) -> Option> { - let state = &*self.0.borrow(); - - let bounds = state.last_layout_bounds.unwrap_or_default(); - let scroll_top = state.logical_scroll_top(); - if ix < scroll_top.item_ix { - return None; - } - - let mut cursor = state.items.cursor::>(()); - cursor.seek(&Count(scroll_top.item_ix), Bias::Right); - - let scroll_top = cursor.start().1.0 + scroll_top.offset_in_item; - - cursor.seek_forward(&Count(ix), Bias::Right); - if let Some(&ListItem::Measured { size, .. }) = cursor.item() { - let &Dimensions(Count(count), Height(top), _) = cursor.start(); - if count == ix { - let top = bounds.top() + top - scroll_top; - return Some(Bounds::from_corners( - point(bounds.left(), top), - point(bounds.right(), top + size.height), - )); - } - } - None - } - - /// Call this method when the user starts dragging the scrollbar. - /// - /// This will prevent the height reported to the scrollbar from changing during the drag - /// as items in the overdraw get measured, and help offset scroll position changes accordingly. - pub fn scrollbar_drag_started(&self) { - let mut state = self.0.borrow_mut(); - state.scrollbar_drag_start_height = Some(state.items.summary().height); - } - - /// Called when the user stops dragging the scrollbar. - /// - /// See `scrollbar_drag_started`. - pub fn scrollbar_drag_ended(&self) { - self.0.borrow_mut().scrollbar_drag_start_height.take(); - } - - /// Returns `true` if the scrollbar is currently being dragged. - /// - /// This is set between [`scrollbar_drag_started`](Self::scrollbar_drag_started) - /// and [`scrollbar_drag_ended`](Self::scrollbar_drag_ended) calls. Useful for - /// consumers that need to distinguish scrollbar drags from wheel/trackpad scrolls, - /// e.g. to suppress auto-scroll behavior during manual positioning. - pub fn is_scrollbar_dragging(&self) -> bool { - self.0.borrow().scrollbar_drag_start_height.is_some() - } - - /// Set the offset from the scrollbar - pub fn set_offset_from_scrollbar(&self, point: Point) { - self.0.borrow_mut().set_offset_from_scrollbar(point); - } - - /// Returns the maximum scroll offset according to the items we have measured. - /// This value remains constant while dragging to prevent the scrollbar from moving away unexpectedly. - pub fn max_offset_for_scrollbar(&self) -> Point { - let state = self.0.borrow(); - point(Pixels::ZERO, state.max_scroll_offset()) - } - - /// Returns the current scroll offset adjusted for the scrollbar. - /// - /// The returned offset has a negative `y` component representing - /// how far the content has scrolled. - pub fn scroll_px_offset_for_scrollbar(&self) -> Point { - let state = &self.0.borrow(); - - if state.logical_scroll_top.is_none() && state.alignment == ListAlignment::Bottom { - return Point::new(px(0.), -state.max_scroll_offset()); - } - - let logical_scroll_top = state.logical_scroll_top(); - - let mut cursor = state.items.cursor::(()); - let summary: ListItemSummary = - cursor.summary(&Count(logical_scroll_top.item_ix), Bias::Right); - let offset = summary.height + logical_scroll_top.offset_in_item; - - Point::new(px(0.), -offset) - } - - /// Return the bounds of the viewport in pixels. - pub fn viewport_bounds(&self) -> Bounds { - self.0.borrow().last_layout_bounds.unwrap_or_default() - } - - /// Returns whether the item is entirely above the viewport, or `None` if - /// the list has not measured enough layout to know. - /// - /// A zero-height viewport still yields a definitive answer: callers may - /// size sibling UI based on this query (potentially squeezing the list - /// itself to zero height), so returning `None` in that case would make - /// the answer oscillate from frame to frame. - pub fn item_is_above_viewport(&self, ix: usize) -> Option { - let viewport_bounds = self.0.borrow().last_layout_bounds?; - - let scroll_top = self.logical_scroll_top(); - if ix < scroll_top.item_ix { - // Rows before the logical scroll top have no item bounds, but - // their position relative to the viewport is known from scroll state. - return Some(true); - } - - let item_bounds = self.bounds_for_item(ix)?; - Some(item_bounds.bottom() <= viewport_bounds.top()) - } - - /// Returns whether the item is entirely below the viewport, or `None` if - /// the list has not measured enough layout to know. - /// - /// See [`Self::item_is_above_viewport`] for why a zero-height viewport - /// still yields a definitive answer. - pub fn item_is_below_viewport(&self, ix: usize) -> Option { - let viewport_bounds = self.0.borrow().last_layout_bounds?; - - let scroll_top = self.logical_scroll_top(); - if ix < scroll_top.item_ix { - // Rows before the logical scroll top have no item bounds, but - // their position relative to the viewport is known from scroll state. - return Some(false); - } - - let item_bounds = self.bounds_for_item(ix)?; - Some(item_bounds.top() >= viewport_bounds.bottom()) - } -} - -impl StateInner { - /// Re-anchor a pending scroll adjustment from a remeasure onto a newly set - /// scroll position, so it clamps to the remeasured item's new height on - /// the next layout instead of reverting the scroll. - fn rebase_pending_scroll(&mut self, scroll_top: ListOffset) { - let Some(pending) = self.pending_scroll.take() else { - return; - }; - if scroll_top.item_ix >= self.items.summary().count { - return; - } - - self.pending_scroll = match pending { - PendingScroll::Absolute { .. } => Some(PendingScroll::Absolute { - item_ix: scroll_top.item_ix, - offset: scroll_top.offset_in_item, - }), - PendingScroll::Proportional(_) => { - let mut cursor = self.items.cursor::(()); - cursor.seek(&Count(scroll_top.item_ix), Bias::Right); - cursor - .item() - .and_then(|item| item.size_hint()) - .filter(|size| size.height.0 > 0.0) - .map(|size| { - PendingScroll::Proportional(PendingScrollFraction { - item_ix: scroll_top.item_ix, - fraction: (scroll_top.offset_in_item.0 / size.height.0).clamp(0.0, 1.0), - }) - }) - } - }; - } - - fn max_scroll_offset(&self) -> Pixels { - let bounds = self.last_layout_bounds.unwrap_or_default(); - let height = self - .scrollbar_drag_start_height - .unwrap_or_else(|| self.items.summary().height); - (height - bounds.size.height).max(px(0.)) - } - - fn visible_range( - items: &SumTree, - height: Pixels, - scroll_top: &ListOffset, - ) -> Range { - let mut cursor = items.cursor::(()); - cursor.seek(&Count(scroll_top.item_ix), Bias::Right); - let start_y = cursor.start().height + scroll_top.offset_in_item; - cursor.seek_forward(&Height(start_y + height), Bias::Left); - scroll_top.item_ix..cursor.start().count + 1 - } - - fn scroll( - &mut self, - scroll_top: &ListOffset, - height: Pixels, - delta: Point, - current_view: EntityId, - window: &mut Window, - cx: &mut App, - ) { - // Drop scroll events after a reset, since we can't calculate - // the new logical scroll top without the item heights - if self.reset { - return; - } - - let padding = self.last_padding.unwrap_or_default(); - let scroll_max = - (self.items.summary().height + padding.top + padding.bottom - height).max(px(0.)); - let new_scroll_top = (self.scroll_top(scroll_top) - delta.y) - .max(px(0.)) - .min(scroll_max); - - if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max { - self.pending_scroll = None; - self.logical_scroll_top = None; - } else { - let (start, ..) = - self.items - .find::((), &Height(new_scroll_top), Bias::Right); - let scroll_top = ListOffset { - item_ix: start.count, - offset_in_item: new_scroll_top - start.height, - }; - // The user's scroll supersedes the position stashed by a - // remeasure; re-anchor the pending adjustment so it doesn't revert - // this scroll on the next layout. - self.rebase_pending_scroll(scroll_top); - self.logical_scroll_top = Some(scroll_top); - } - - if delta.y > px(0.) { - self.follow_state.stop_following(); - } - - if let Some(handler) = self.scroll_handler.as_mut() { - let visible_range = Self::visible_range(&self.items, height, scroll_top); - handler( - &ListScrollEvent { - visible_range, - count: self.items.summary().count, - is_scrolled: self.logical_scroll_top.is_some(), - is_following_tail: matches!( - self.follow_state, - FollowState::Tail { is_following: true } - ), - }, - window, - cx, - ); - } - - cx.notify(current_view); - } - - fn logical_scroll_top(&self) -> ListOffset { - self.logical_scroll_top - .unwrap_or_else(|| match self.alignment { - ListAlignment::Top => ListOffset { - item_ix: 0, - offset_in_item: px(0.), - }, - ListAlignment::Bottom => ListOffset { - item_ix: self.items.summary().count, - offset_in_item: px(0.), - }, - }) - } - - fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels { - let (start, ..) = self.items.find::( - (), - &Count(logical_scroll_top.item_ix), - Bias::Right, - ); - start.height + logical_scroll_top.offset_in_item - } - - fn layout_all_items( - &mut self, - available_width: Pixels, - render_item: &mut RenderItemFn, - window: &mut Window, - cx: &mut App, - ) { - match &mut self.measuring_behavior { - ListMeasuringBehavior::Visible => { - return; - } - ListMeasuringBehavior::Measure(has_measured) => { - if *has_measured { - return; - } - *has_measured = true; - } - } - - let mut cursor = self.items.cursor::(()); - let available_item_space = size( - AvailableSpace::Definite(available_width), - AvailableSpace::MinContent, - ); - - let mut measured_items = Vec::default(); - - for (ix, item) in cursor.enumerate() { - let size = item.size().unwrap_or_else(|| { - let mut element = render_item(ix, window, cx); - element.layout_as_root(available_item_space, window, cx) - }); - - measured_items.push(ListItem::Measured { - size, - focus_handle: item.focus_handle(), - }); - } - - self.items = SumTree::from_iter(measured_items, ()); - } - - fn layout_items( - &mut self, - available_width: Option, - available_height: Pixels, - padding: &Edges, - render_item: &mut RenderItemFn, - window: &mut Window, - cx: &mut App, - ) -> LayoutItemsResponse { - let old_items = self.items.clone(); - let mut measured_items = VecDeque::new(); - let mut item_layouts = VecDeque::new(); - let mut rendered_height = padding.top; - let mut max_item_width = px(0.); - let mut scroll_top = self.logical_scroll_top(); - - if self.follow_state.is_following() { - scroll_top = ListOffset { - item_ix: self.items.summary().count, - offset_in_item: px(0.), - }; - self.logical_scroll_top = Some(scroll_top); - } - - let mut rendered_focused_item = false; - - let available_item_space = size( - available_width.map_or(AvailableSpace::MaxContent, |width| { - AvailableSpace::Definite(width) - }), - AvailableSpace::MinContent, - ); - - let mut cursor = old_items.cursor::(()); - - // Render items after the scroll top, including those in the trailing overdraw - cursor.seek(&Count(scroll_top.item_ix), Bias::Right); - for (ix, item) in cursor.by_ref().enumerate() { - let visible_height = rendered_height - scroll_top.offset_in_item; - if visible_height >= available_height + self.overdraw { - break; - } - - // Use the previously cached height and focus handle if available - let mut size = item.size(); - - // If we're within the visible area or the height wasn't cached, render and measure the item's element - if visible_height < available_height || size.is_none() { - let item_index = scroll_top.item_ix + ix; - let mut element = render_item(item_index, window, cx); - let element_size = element.layout_as_root(available_item_space, window, cx); - size = Some(element_size); - - // If there's a pending scroll adjustment for the scroll-top - // item, apply it. - if ix == 0 { - if let Some(pending_scroll) = self.pending_scroll.take() { - match pending_scroll { - PendingScroll::Absolute { item_ix, offset } - if item_ix == scroll_top.item_ix => - { - scroll_top.offset_in_item = offset.min(element_size.height); - self.logical_scroll_top = Some(scroll_top); - } - PendingScroll::Proportional(pending_scroll) - if pending_scroll.item_ix == scroll_top.item_ix => - { - // Ensuring proportional scroll position is - // maintained after re-measuring. - scroll_top.offset_in_item = - Pixels(pending_scroll.fraction * element_size.height.0); - self.logical_scroll_top = Some(scroll_top); - } - _ => {} - } - } - } - - if visible_height < available_height { - item_layouts.push_back(ItemLayout { - index: item_index, - element, - size: element_size, - }); - if item.contains_focused(window, cx) { - rendered_focused_item = true; - } - } - } - - let size = size.unwrap(); - rendered_height += size.height; - max_item_width = max_item_width.max(size.width); - measured_items.push_back(ListItem::Measured { - size, - focus_handle: item.focus_handle(), - }); - } - rendered_height += padding.bottom; - - // Prepare to start walking upward from the item at the scroll top. - cursor.seek(&Count(scroll_top.item_ix), Bias::Right); - - // If the rendered items do not fill the visible region, then adjust - // the scroll top upward. - if rendered_height - scroll_top.offset_in_item < available_height { - while rendered_height < available_height { - cursor.prev(); - if let Some(item) = cursor.item() { - let item_index = cursor.start().0; - let mut element = render_item(item_index, window, cx); - let element_size = element.layout_as_root(available_item_space, window, cx); - let focus_handle = item.focus_handle(); - rendered_height += element_size.height; - measured_items.push_front(ListItem::Measured { - size: element_size, - focus_handle, - }); - item_layouts.push_front(ItemLayout { - index: item_index, - element, - size: element_size, - }); - if item.contains_focused(window, cx) { - rendered_focused_item = true; - } - } else { - break; - } - } - - scroll_top = ListOffset { - item_ix: cursor.start().0, - offset_in_item: rendered_height - available_height, - }; - - match self.alignment { - ListAlignment::Top => { - scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.)); - self.logical_scroll_top = Some(scroll_top); - } - ListAlignment::Bottom => { - scroll_top = ListOffset { - item_ix: cursor.start().0, - offset_in_item: rendered_height - available_height, - }; - self.logical_scroll_top = None; - } - }; - } - - // Measure items in the leading overdraw - let mut leading_overdraw = scroll_top.offset_in_item; - while leading_overdraw < self.overdraw { - cursor.prev(); - if let Some(item) = cursor.item() { - let size = if let ListItem::Measured { size, .. } = item { - *size - } else { - let mut element = render_item(cursor.start().0, window, cx); - element.layout_as_root(available_item_space, window, cx) - }; - - leading_overdraw += size.height; - measured_items.push_front(ListItem::Measured { - size, - focus_handle: item.focus_handle(), - }); - } else { - break; - } - } - - let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len()); - let mut cursor = old_items.cursor::(()); - let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right); - new_items.extend(measured_items, ()); - cursor.seek(&Count(measured_range.end), Bias::Right); - new_items.append(cursor.suffix(), ()); - self.items = new_items; - - // If follow_tail mode is on but the user scrolled away - // (is_following is false), check whether the current scroll - // position has returned to the bottom. - if self.follow_state.has_stopped_following() { - let padding = self.last_padding.unwrap_or_default(); - let total_height = self.items.summary().height + padding.top + padding.bottom; - let scroll_offset = self.scroll_top(&scroll_top); - if scroll_offset + available_height >= total_height - px(1.0) { - self.follow_state.start_following(); - } - } - - // If none of the visible items are focused, check if an off-screen item is focused - // and include it to be rendered after the visible items so keyboard interaction continues - // to work for it. - if !rendered_focused_item { - let mut cursor = self - .items - .filter::<_, Count>((), |summary| summary.has_focus_handles); - cursor.next(); - while let Some(item) = cursor.item() { - if item.contains_focused(window, cx) { - let item_index = cursor.start().0; - let mut element = render_item(cursor.start().0, window, cx); - let size = element.layout_as_root(available_item_space, window, cx); - item_layouts.push_back(ItemLayout { - index: item_index, - element, - size, - }); - break; - } - cursor.next(); - } - } - - LayoutItemsResponse { - max_item_width, - scroll_top, - item_layouts, - } - } - - fn prepaint_items( - &mut self, - bounds: Bounds, - padding: Edges, - autoscroll: bool, - render_item: &mut RenderItemFn, - window: &mut Window, - cx: &mut App, - ) -> Result { - window.transact(|window| { - match self.measuring_behavior { - ListMeasuringBehavior::Measure(has_measured) if !has_measured => { - self.layout_all_items(bounds.size.width, render_item, window, cx); - } - _ => {} - } - - let mut layout_response = self.layout_items( - Some(bounds.size.width), - bounds.size.height, - &padding, - render_item, - window, - cx, - ); - - // Avoid honoring autoscroll requests from elements other than our children. - window.take_autoscroll(); - - // Only paint the visible items, if there is actually any space for them (taking padding into account) - if bounds.size.height > padding.top + padding.bottom { - let mut item_origin = bounds.origin + Point::new(px(0.), padding.top); - item_origin.y -= layout_response.scroll_top.offset_in_item; - for item in &mut layout_response.item_layouts { - window.with_content_mask( - Some(ContentMask { - bounds, - ..Default::default() - }), - |window| { - item.element.prepaint_at(item_origin, window, cx); - }, - ); - - if let Some(autoscroll_bounds) = window.take_autoscroll() - && autoscroll - { - if autoscroll_bounds.top() < bounds.top() { - let mut item_ix = item.index; - let mut offset_in_item = autoscroll_bounds.top() - item_origin.y; - - // The requested top can sit above this item's own - // top. Walk into earlier items so the offset stays - // non-negative and no blank space appears above the - // list. - if offset_in_item < Pixels::ZERO { - let mut cursor = self.items.cursor::(()); - cursor.seek(&Count(item_ix), Bias::Right); - while offset_in_item < Pixels::ZERO { - cursor.prev(); - let Some(prev_item) = cursor.item() else { - offset_in_item = Pixels::ZERO; - break; - }; - let size = prev_item.size().unwrap_or_else(|| { - let mut element = render_item(cursor.start().0, window, cx); - let item_available_size = size( - bounds.size.width.into(), - AvailableSpace::MinContent, - ); - element.layout_as_root(item_available_size, window, cx) - }); - item_ix = cursor.start().0; - offset_in_item += size.height; - } - } - - return Err(ListOffset { - item_ix, - offset_in_item, - }); - } else if autoscroll_bounds.bottom() > bounds.bottom() { - let mut cursor = self.items.cursor::(()); - cursor.seek(&Count(item.index), Bias::Right); - let mut height = bounds.size.height - padding.top - padding.bottom; - - // Account for the height of the element down until the autoscroll bottom. - height -= autoscroll_bounds.bottom() - item_origin.y; - - // Keep decreasing the scroll top until we fill all the available space. - while height > Pixels::ZERO { - cursor.prev(); - let Some(item) = cursor.item() else { break }; - - let size = item.size().unwrap_or_else(|| { - let mut item = render_item(cursor.start().0, window, cx); - let item_available_size = - size(bounds.size.width.into(), AvailableSpace::MinContent); - item.layout_as_root(item_available_size, window, cx) - }); - height -= size.height; - } - - return Err(ListOffset { - item_ix: cursor.start().0, - offset_in_item: if height < Pixels::ZERO { - -height - } else { - Pixels::ZERO - }, - }); - } - } - - item_origin.y += item.size.height; - } - } else { - layout_response.item_layouts.clear(); - } - - Ok(layout_response) - }) - } - - // Scrollbar support - - fn set_offset_from_scrollbar(&mut self, point: Point) { - let Some(bounds) = self.last_layout_bounds else { - return; - }; - let height = bounds.size.height; - - let padding = self.last_padding.unwrap_or_default(); - // Scrollbar drag positions are computed from the content height - // captured at drag start, so map them back using the same height. - let content_height = self - .scrollbar_drag_start_height - .unwrap_or_else(|| self.items.summary().height); - let scroll_max = (content_height + padding.top + padding.bottom - height).max(px(0.)); - let new_scroll_top = (-point.y).max(px(0.)).min(scroll_max); - - // If content grew during the drag, the frozen bottom is below the - // live bottom. Treat dragging to the frozen end as resuming tail follow. - let dragged_to_end = - scroll_max > px(0.) && new_scroll_top >= (scroll_max - px(1.0)).max(px(0.)); - if dragged_to_end && matches!(self.follow_state, FollowState::Tail { .. }) { - self.follow_state = FollowState::Tail { is_following: true }; - let item_count = self.items.summary().count; - self.pending_scroll = None; - self.logical_scroll_top = Some(ListOffset { - item_ix: item_count, - offset_in_item: px(0.), - }); - return; - } - - self.follow_state.stop_following(); - - if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max { - self.pending_scroll = None; - self.logical_scroll_top = None; - } else { - let (start, _, _) = - self.items - .find::((), &Height(new_scroll_top), Bias::Right); - - let scroll_top = ListOffset { - item_ix: start.count, - offset_in_item: new_scroll_top - start.height, - }; - self.rebase_pending_scroll(scroll_top); - self.logical_scroll_top = Some(scroll_top); - } - } -} - -impl std::fmt::Debug for ListItem { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Unmeasured { .. } => write!(f, "Unrendered"), - Self::Measured { size, .. } => f.debug_struct("Rendered").field("size", size).finish(), - } - } -} - -/// An offset into the list's items, in terms of the item index and the number -/// of pixels off the top left of the item. -#[derive(Debug, Clone, Copy, Default)] -pub struct ListOffset { - /// The index of an item in the list - pub item_ix: usize, - /// The number of pixels to offset from the item index. - pub offset_in_item: Pixels, -} - -impl Element for List { - type RequestLayoutState = (); - type PrepaintState = ListPrepaintState; - - fn id(&self) -> Option { - None - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (crate::LayoutId, Self::RequestLayoutState) { - let layout_id = match self.sizing_behavior { - ListSizingBehavior::Infer => { - let mut style = Style::default(); - style.overflow.y = Overflow::Scroll; - style.refine(&self.style); - window.with_text_style(style.text_style().cloned(), |window| { - let state = &mut *self.state.0.borrow_mut(); - - let available_height = if let Some(last_bounds) = state.last_layout_bounds { - last_bounds.size.height - } else { - // If we don't have the last layout bounds (first render), - // we might just use the overdraw value as the available height to layout enough items. - state.overdraw - }; - let padding = style.padding.to_pixels( - state.last_layout_bounds.unwrap_or_default().size.into(), - window.rem_size(), - ); - - let layout_response = state.layout_items( - None, - available_height, - &padding, - &mut self.render_item, - window, - cx, - ); - let max_element_width = layout_response.max_item_width; - - let summary = state.items.summary(); - let total_height = summary.height; - - window.request_measured_layout( - style, - move |known_dimensions, available_space, _window, _cx| { - let width = - known_dimensions - .width - .unwrap_or(match available_space.width { - AvailableSpace::Definite(x) => x, - AvailableSpace::MinContent | AvailableSpace::MaxContent => { - max_element_width - } - }); - let height = match available_space.height { - AvailableSpace::Definite(height) => total_height.min(height), - AvailableSpace::MinContent | AvailableSpace::MaxContent => { - total_height - } - }; - size(width, height) - }, - ) - }) - } - ListSizingBehavior::Auto => { - let mut style = Style::default(); - style.refine(&self.style); - window.with_text_style(style.text_style().cloned(), |window| { - window.request_layout(style, None, cx) - }) - } - }; - (layout_id, ()) - } - - fn prepaint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - _: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> ListPrepaintState { - let state = &mut *self.state.0.borrow_mut(); - state.reset = false; - - let mut style = Style::default(); - style.refine(&self.style); - - let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); - - // If the width of the list has changed, invalidate all cached item heights - if state - .last_layout_bounds - .is_none_or(|last_bounds| last_bounds.size.width != bounds.size.width) - { - let new_items = SumTree::from_iter( - state.items.iter().map(|item| ListItem::Unmeasured { - size_hint: None, - focus_handle: item.focus_handle(), - }), - (), - ); - - state.items = new_items; - state.measuring_behavior.reset(); - } - - let padding = style - .padding - .to_pixels(bounds.size.into(), window.rem_size()); - let layout = - match state.prepaint_items(bounds, padding, true, &mut self.render_item, window, cx) { - Ok(layout) => layout, - Err(autoscroll_request) => { - state.logical_scroll_top = Some(autoscroll_request); - state - .prepaint_items(bounds, padding, false, &mut self.render_item, window, cx) - .unwrap() - } - }; - - state.last_layout_bounds = Some(bounds); - state.last_padding = Some(padding); - ListPrepaintState { hitbox, layout } - } - - fn paint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - _: &mut Self::RequestLayoutState, - prepaint: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - let current_view = window.current_view(); - - // Register the scroll listener before painting children so that, in - // the bubble phase (which runs in reverse registration order), - // children's scroll-wheel handlers run first and can stop propagation - // to prevent the list from scrolling. This matches the ordering of - // div-based scroll containers. - let list_state = self.state.clone(); - let height = bounds.size.height; - let scroll_top = prepaint.layout.scroll_top; - let hitbox_id = prepaint.hitbox.id; - let mut accumulated_scroll_delta = ScrollDelta::default(); - window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| { - if phase == DispatchPhase::Bubble && hitbox_id.should_handle_scroll(window) { - accumulated_scroll_delta = accumulated_scroll_delta.coalesce(event.delta); - let pixel_delta = accumulated_scroll_delta.pixel_delta(px(20.)); - list_state.0.borrow_mut().scroll( - &scroll_top, - height, - pixel_delta, - current_view, - window, - cx, - ) - } - }); - - window.with_content_mask( - Some(ContentMask { - bounds, - ..Default::default() - }), - |window| { - for item in &mut prepaint.layout.item_layouts { - item.element.paint(window, cx); - } - }, - ); - } -} - -impl IntoElement for List { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -impl Styled for List { - fn style(&mut self) -> &mut StyleRefinement { - &mut self.style - } -} - -impl sum_tree::Item for ListItem { - type Summary = ListItemSummary; - - fn summary(&self, _: ()) -> Self::Summary { - match self { - ListItem::Unmeasured { - size_hint, - focus_handle, - } => ListItemSummary { - count: 1, - rendered_count: 0, - unrendered_count: 1, - height: if let Some(size) = size_hint { - size.height - } else { - px(0.) - }, - has_focus_handles: focus_handle.is_some(), - has_unknown_height: size_hint.is_none(), - }, - ListItem::Measured { - size, focus_handle, .. - } => ListItemSummary { - count: 1, - rendered_count: 1, - unrendered_count: 0, - height: size.height, - has_focus_handles: focus_handle.is_some(), - has_unknown_height: false, - }, - } - } -} - -impl sum_tree::ContextLessSummary for ListItemSummary { - fn zero() -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &Self) { - self.count += summary.count; - self.rendered_count += summary.rendered_count; - self.unrendered_count += summary.unrendered_count; - self.height += summary.height; - self.has_focus_handles |= summary.has_focus_handles; - self.has_unknown_height |= summary.has_unknown_height; - } -} - -impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) { - self.0 += summary.count; - } -} - -impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) { - self.0 += summary.height; - } -} - -impl sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Count { - fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering { - self.0.partial_cmp(&other.count).unwrap() - } -} - -impl sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Height { - fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering { - self.0.partial_cmp(&other.height).unwrap() - } -} - -#[cfg(test)] -mod test { - - use gpui::{ScrollDelta, ScrollWheelEvent}; - use std::cell::Cell; - use std::rc::Rc; - - use crate::{ - self as gpui, AppContext, Bounds, Context, Element, FollowMode, InteractiveElement, - IntoElement, ListState, Render, Styled, TestAppContext, Window, canvas, div, list, point, - px, size, - }; - - #[gpui::test] - fn test_autoscroll_above_item_top_renders_items_above(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - let state = ListState::new(5, crate::ListAlignment::Top, px(10.)); - state.scroll_to(gpui::ListOffset { - item_ix: 2, - offset_in_item: px(0.), - }); - - struct TestView(ListState); - impl Render for TestView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - list(self.0.clone(), |ix, _, _| { - if ix == 2 { - // Request an autoscroll whose top sits 30px above item 2's - // own top, mimicking a scroll-margin overshoot. - canvas( - |bounds, window, _| { - window.request_autoscroll(Bounds::from_corners( - point(bounds.left(), bounds.top() - px(30.)), - point(bounds.right(), bounds.top() + px(5.)), - )); - }, - |_, _, _, _| {}, - ) - .h(px(20.)) - .w_full() - .into_any() - } else { - div().h(px(20.)).w_full().into_any() - } - }) - .w_full() - .h_full() - } - } - - cx.draw(point(px(0.), px(0.)), size(px(100.), px(60.)), |_, cx| { - cx.new(|_| TestView(state.clone())).into_any_element() - }); - - // 30px above item 2's top, with 20px items, lands 10px into item 0. - let scroll_top = state.logical_scroll_top(); - assert!( - scroll_top.offset_in_item >= px(0.), - "offset_in_item must never be negative (would leave blank space above), got {:?}", - scroll_top.offset_in_item, - ); - assert_eq!(scroll_top.item_ix, 0); - assert_eq!(scroll_top.offset_in_item, px(10.)); - } - - #[gpui::test] - fn test_reset_after_paint_before_scroll(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - let state = ListState::new(5, crate::ListAlignment::Top, px(10.)); - - // Ensure that the list is scrolled to the top - state.scroll_to(gpui::ListOffset { - item_ix: 0, - offset_in_item: px(0.0), - }); - - struct TestView(ListState); - impl Render for TestView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - list(self.0.clone(), |_, _, _| { - div().h(px(10.)).w_full().into_any() - }) - .w_full() - .h_full() - } - } - - // Paint - cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| { - cx.new(|_| TestView(state.clone())).into_any_element() - }); - - // Reset - state.reset(5); - - // And then receive a scroll event _before_ the next paint - cx.simulate_event(ScrollWheelEvent { - position: point(px(1.), px(1.)), - delta: ScrollDelta::Pixels(point(px(0.), px(-500.))), - ..Default::default() - }); - - // Scroll position should stay at the top of the list - assert_eq!(state.logical_scroll_top().item_ix, 0); - assert_eq!(state.logical_scroll_top().offset_in_item, px(0.)); - } - - #[gpui::test] - fn test_scroll_by_positive_and_negative_distance(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - let state = ListState::new(5, crate::ListAlignment::Top, px(10.)); - - struct TestView(ListState); - impl Render for TestView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - list(self.0.clone(), |_, _, _| { - div().h(px(20.)).w_full().into_any() - }) - .w_full() - .h_full() - } - } - - // Paint - cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| { - cx.new(|_| TestView(state.clone())).into_any_element() - }); - - // Test positive distance: start at item 1, move down 30px - state.scroll_by(px(30.)); - - // Should move to item 2 - let offset = state.logical_scroll_top(); - assert_eq!(offset.item_ix, 1); - assert_eq!(offset.offset_in_item, px(10.)); - - // Test negative distance: start at item 2, move up 30px - state.scroll_by(px(-30.)); - - // Should move back to item 1 - let offset = state.logical_scroll_top(); - assert_eq!(offset.item_ix, 0); - assert_eq!(offset.offset_in_item, px(0.)); - - // Test zero distance - state.scroll_by(px(0.)); - let offset = state.logical_scroll_top(); - assert_eq!(offset.item_ix, 0); - assert_eq!(offset.offset_in_item, px(0.)); - } - - #[gpui::test] - fn test_child_scroll_handler_can_stop_list_scroll(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - let state = ListState::new(5, crate::ListAlignment::Top, px(10.)); - let child_saw_event = Rc::new(Cell::new(false)); - - struct TestView { - state: ListState, - child_saw_event: Rc>, - } - impl Render for TestView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - let child_saw_event = self.child_saw_event.clone(); - list(self.state.clone(), move |_, _, _| { - let child_saw_event = child_saw_event.clone(); - div() - .h(px(20.)) - .w_full() - .on_scroll_wheel(move |_, _, cx| { - child_saw_event.set(true); - cx.stop_propagation(); - }) - .into_any() - }) - .w_full() - .h_full() - } - } - - cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| { - cx.new(|_| TestView { - state: state.clone(), - child_saw_event: child_saw_event.clone(), - }) - .into_any_element() - }); - - cx.simulate_event(ScrollWheelEvent { - position: point(px(50.), px(10.)), - delta: ScrollDelta::Pixels(point(px(0.), px(-30.))), - ..Default::default() - }); - - assert!( - child_saw_event.get(), - "the child's scroll-wheel handler should run" - ); - // The child stopped propagation, so the list must not have scrolled. - let offset = state.logical_scroll_top(); - assert_eq!(offset.item_ix, 0); - assert_eq!(offset.offset_in_item, px(0.)); - } - - struct TestListView(ListState); - impl Render for TestListView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - list(self.0.clone(), |_, _, _| { - div().h(px(20.)).w_full().into_any() - }) - .w_full() - .h_full() - } - } - - #[gpui::test] - fn test_item_viewport_queries_return_none_before_layout(_cx: &mut TestAppContext) { - let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all(); - - assert_eq!(state.item_is_above_viewport(0), None); - assert_eq!(state.item_is_below_viewport(0), None); - } - - #[gpui::test] - fn test_item_viewport_queries_before_logical_scroll_top(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all(); - - state.scroll_to(gpui::ListOffset { - item_ix: 2, - offset_in_item: px(0.), - }); - cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| { - cx.new(|_| TestListView(state.clone())).into_any_element() - }); - - assert_eq!(state.item_is_above_viewport(1), Some(true)); - assert_eq!(state.item_is_below_viewport(1), Some(false)); - } - - #[gpui::test] - fn test_item_viewport_queries_measured_item_inside_viewport(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all(); - - state.scroll_to(gpui::ListOffset { - item_ix: 2, - offset_in_item: px(0.), - }); - cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| { - cx.new(|_| TestListView(state.clone())).into_any_element() - }); - - assert_eq!(state.item_is_above_viewport(2), Some(false)); - assert_eq!(state.item_is_below_viewport(2), Some(false)); - } - - #[gpui::test] - fn test_item_viewport_queries_measured_item_above_viewport(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all(); - - state.scroll_to(gpui::ListOffset { - item_ix: 2, - offset_in_item: px(20.), - }); - cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| { - cx.new(|_| TestListView(state.clone())).into_any_element() - }); - - assert_eq!(state.item_is_above_viewport(2), Some(true)); - assert_eq!(state.item_is_below_viewport(2), Some(false)); - } - - #[gpui::test] - fn test_item_viewport_queries_measured_item_below_viewport(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all(); - - state.scroll_to(gpui::ListOffset { - item_ix: 2, - offset_in_item: px(0.), - }); - cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| { - cx.new(|_| TestListView(state.clone())).into_any_element() - }); - - assert_eq!(state.item_is_above_viewport(3), Some(false)); - assert_eq!(state.item_is_below_viewport(3), Some(true)); - } - - #[gpui::test] - fn test_item_viewport_queries_remain_stable_with_zero_height_viewport(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all(); - - state.scroll_to(gpui::ListOffset { - item_ix: 2, - offset_in_item: px(0.), - }); - cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| { - cx.new(|_| TestListView(state.clone())).into_any_element() - }); - - assert_eq!(state.item_is_above_viewport(3), Some(false)); - assert_eq!(state.item_is_below_viewport(3), Some(true)); - - // Squeeze the list to zero height, e.g. because a sibling element - // (sized based on the queries above) consumed all the space. The - // answers must remain definitive rather than becoming `None`, - // otherwise the sibling's size can oscillate between frames. - cx.draw(point(px(0.), px(0.)), size(px(100.), px(0.)), |_, cx| { - cx.new(|_| TestListView(state.clone())).into_any_element() - }); - - assert_eq!(state.item_is_above_viewport(1), Some(true)); - assert_eq!(state.item_is_below_viewport(1), Some(false)); - assert_eq!(state.item_is_above_viewport(3), Some(false)); - assert_eq!(state.item_is_below_viewport(3), Some(true)); - } - - #[gpui::test] - fn test_item_viewport_queries_after_scroll_to_end_before_layout(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all(); - - cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| { - cx.new(|_| TestListView(state.clone())).into_any_element() - }); - - state.scroll_to_end(); - - assert_eq!(state.logical_scroll_top().item_ix, state.item_count()); - assert_eq!(state.item_is_above_viewport(0), Some(true)); - assert_eq!(state.item_is_below_viewport(0), Some(false)); - } - - #[gpui::test] - fn test_measure_all_after_width_change(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all(); - - struct TestView(ListState); - impl Render for TestView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - list(self.0.clone(), |_, _, _| { - div().h(px(50.)).w_full().into_any() - }) - .w_full() - .h_full() - } - } - - let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone()))); - - // First draw at width 100: all 10 items measured (total 500px). - // Viewport is 200px, so max scroll offset should be 300px. - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.clone().into_any_element() - }); - assert_eq!(state.max_offset_for_scrollbar().y, px(300.)); - - // Second draw at a different width: items get invalidated. - // Without the fix, max_offset would drop because unmeasured items - // contribute 0 height. - cx.draw(point(px(0.), px(0.)), size(px(200.), px(200.)), |_, _| { - view.into_any_element() - }); - assert_eq!(state.max_offset_for_scrollbar().y, px(300.)); - } - - #[gpui::test] - fn test_remeasure(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - // Create a list with 10 items, each 100px tall. We'll keep a reference - // to the item height so we can later change the height and assert how - // `ListState` handles it. - let item_height = Rc::new(Cell::new(100usize)); - let state = ListState::new(10, crate::ListAlignment::Top, px(10.)); - - struct TestView { - state: ListState, - item_height: Rc>, - } - - impl Render for TestView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - let height = self.item_height.get(); - list(self.state.clone(), move |_, _, _| { - div().h(px(height as f32)).w_full().into_any() - }) - .w_full() - .h_full() - } - } - - let state_clone = state.clone(); - let item_height_clone = item_height.clone(); - let view = cx.update(|_, cx| { - cx.new(|_| TestView { - state: state_clone, - item_height: item_height_clone, - }) - }); - - // Simulate scrolling 40px inside the element with index 2. Since the - // original item height is 100px, this equates to 40% inside the item. - state.scroll_to(gpui::ListOffset { - item_ix: 2, - offset_in_item: px(40.), - }); - - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.clone().into_any_element() - }); - - let offset = state.logical_scroll_top(); - assert_eq!(offset.item_ix, 2); - assert_eq!(offset.offset_in_item, px(40.)); - - // Update the `item_height` to be 50px instead of 100px so we can assert - // that the scroll position is proportionally preserved, that is, - // instead of 40px from the top of item 2, it should be 20px, since the - // item's height has been halved. - item_height.set(50); - state.remeasure(); - - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.into_any_element() - }); - - let offset = state.logical_scroll_top(); - assert_eq!(offset.item_ix, 2); - assert_eq!(offset.offset_in_item, px(20.)); - } - - #[gpui::test] - fn test_remeasure_item_preserves_scroll_offset(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - let item_height = Rc::new(Cell::new(100usize)); - let state = ListState::new(20, crate::ListAlignment::Top, px(10.)); - - struct TestView { - state: ListState, - item_height: Rc>, - } - - impl Render for TestView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - let height = self.item_height.get(); - list(self.state.clone(), move |index, _, _| { - let height = if index == 5 { height } else { 100 }; - div().h(px(height as f32)).w_full().into_any() - }) - .w_full() - .h_full() - } - } - - let state_clone = state.clone(); - let item_height_clone = item_height.clone(); - let view = cx.update(|_, cx| { - cx.new(|_| TestView { - state: state_clone, - item_height: item_height_clone, - }) - }); - - state.scroll_to(gpui::ListOffset { - item_ix: 5, - offset_in_item: px(40.), - }); - - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.clone().into_any_element() - }); - - item_height.set(200); - state.remeasure_items(5..6); - - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.into_any_element() - }); - - let offset = state.logical_scroll_top(); - assert_eq!(offset.item_ix, 5); - assert_eq!(offset.offset_in_item, px(40.)); - } - - #[gpui::test] - fn test_remeasure_then_scroll_does_not_revert_scroll_position(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - let state = ListState::new(20, crate::ListAlignment::Top, px(10.)); - - struct TestView(ListState); - impl Render for TestView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - list(self.0.clone(), |_, _, _| { - div().h(px(100.)).w_full().into_any() - }) - .w_full() - .h_full() - } - } - - let view = { - let state = state.clone(); - cx.update(|_, cx| cx.new(|_| TestView(state))) - }; - - state.scroll_to(gpui::ListOffset { - item_ix: 5, - offset_in_item: px(40.), - }); - - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.clone().into_any_element() - }); - - state.remeasure_items(5..6); - - cx.simulate_event(ScrollWheelEvent { - position: point(px(50.), px(100.)), - delta: ScrollDelta::Pixels(point(px(0.), px(-30.))), - ..Default::default() - }); - - let offset = state.logical_scroll_top(); - assert_eq!(offset.item_ix, 5); - assert_eq!(offset.offset_in_item, px(70.)); - - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.into_any_element() - }); - - let offset = state.logical_scroll_top(); - assert_eq!(offset.item_ix, 5); - assert_eq!( - offset.offset_in_item, - px(70.), - "scrolling after a remeasure should not be reverted by the stale pending scroll" - ); - } - - #[gpui::test] - fn test_scroll_after_remeasure_clamps_to_shrunk_item_height(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - let item_height = Rc::new(Cell::new(100usize)); - let state = ListState::new(20, crate::ListAlignment::Top, px(10.)); - - struct TestView { - state: ListState, - item_height: Rc>, - } - - impl Render for TestView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - let height = self.item_height.get(); - list(self.state.clone(), move |index, _, _| { - let height = if index == 5 { height } else { 100 }; - div().h(px(height as f32)).w_full().into_any() - }) - .w_full() - .h_full() - } - } - - let view = { - let state = state.clone(); - let item_height = item_height.clone(); - cx.update(|_, cx| cx.new(|_| TestView { state, item_height })) - }; - - state.scroll_to(gpui::ListOffset { - item_ix: 5, - offset_in_item: px(40.), - }); - - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.clone().into_any_element() - }); - - // Item 5 shrinks from 100px to 50px and is remeasured... - item_height.set(50); - state.remeasure_items(5..6); - - // ...and then the user scrolls down by 30px before the next frame, - // landing at offset 70. - cx.simulate_event(ScrollWheelEvent { - position: point(px(50.), px(100.)), - delta: ScrollDelta::Pixels(point(px(0.), px(-30.))), - ..Default::default() - }); - - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.into_any_element() - }); - - // The rebased pending scroll clamps the user's offset to the item's - // new height instead of leaving it pointing past the end of the item. - let offset = state.logical_scroll_top(); - assert_eq!(offset.item_ix, 5); - assert_eq!(offset.offset_in_item, px(50.)); - } - - #[gpui::test] - fn test_follow_tail_stays_at_bottom_as_items_grow(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - // 10 items, each 50px tall → 500px total content, 200px viewport. - // With follow-tail on, the list should always show the bottom. - let item_height = Rc::new(Cell::new(50usize)); - let state = ListState::new(10, crate::ListAlignment::Top, px(0.)); - - struct TestView { - state: ListState, - item_height: Rc>, - } - impl Render for TestView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - let height = self.item_height.get(); - list(self.state.clone(), move |_, _, _| { - div().h(px(height as f32)).w_full().into_any() - }) - .w_full() - .h_full() - } - } - - let state_clone = state.clone(); - let item_height_clone = item_height.clone(); - let view = cx.update(|_, cx| { - cx.new(|_| TestView { - state: state_clone, - item_height: item_height_clone, - }) - }); - - state.set_follow_mode(FollowMode::Tail); - - // First paint — items are 50px, total 500px, viewport 200px. - // Follow-tail should anchor to the end. - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.clone().into_any_element() - }); - - // The scroll should be at the bottom: the last visible items fill the - // 200px viewport from the end of 500px of content (offset 300px). - let offset = state.logical_scroll_top(); - assert_eq!(offset.item_ix, 6); - assert_eq!(offset.offset_in_item, px(0.)); - assert!(state.is_following_tail()); - - // Simulate items growing (e.g. streaming content makes each item taller). - // 10 items × 80px = 800px total. - item_height.set(80); - state.remeasure(); - - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.into_any_element() - }); - - // After growth, follow-tail should have re-anchored to the new end. - // 800px total − 200px viewport = 600px offset → item 7 at offset 40px, - // but follow-tail anchors to item_count (10), and layout walks back to - // fill 200px, landing at item 7 (7 × 80 = 560, 800 − 560 = 240 > 200, - // so item 8: 8 × 80 = 640, 800 − 640 = 160 < 200 → keeps walking → - // item 7: offset = 800 − 200 = 600, item_ix = 600/80 = 7, remainder 40). - let offset = state.logical_scroll_top(); - assert_eq!(offset.item_ix, 7); - assert_eq!(offset.offset_in_item, px(40.)); - assert!(state.is_following_tail()); - } - - #[gpui::test] - fn test_pause_following_tail_reengages_when_still_at_bottom(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - // 10 items × 50px = 500px total, 200px viewport. - let state = ListState::new(10, crate::ListAlignment::Top, px(0.)); - - struct TestView(ListState); - impl Render for TestView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - list(self.0.clone(), |_, _, _| { - div().h(px(50.)).w_full().into_any() - }) - .w_full() - .h_full() - } - } - - let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone()))); - state.set_follow_mode(FollowMode::Tail); - - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.clone().into_any_element() - }); - assert!(state.is_following_tail()); - - // Pausing while the view is still at the bottom (e.g. a no-op zoom) - // must not strand follow-tail: the next layout has to re-engage so the - // invariant "at the bottom + new content => visible" is preserved. - state.pause_following_tail(); - assert!(!state.is_following_tail()); - - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.into_any_element() - }); - assert!( - state.is_following_tail(), - "pausing while at the bottom must re-engage follow-tail on the next layout" - ); - } - - #[gpui::test] - fn test_pause_following_tail_freezes_off_bottom(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - // 10 items, 200px viewport. Item height is adjustable to simulate a - // diagram block growing/shrinking on zoom. - let item_height = Rc::new(Cell::new(50usize)); - let state = ListState::new(10, crate::ListAlignment::Top, px(0.)); - - struct TestView { - state: ListState, - item_height: Rc>, - } - impl Render for TestView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - let height = self.item_height.get(); - list(self.state.clone(), move |_, _, _| { - div().h(px(height as f32)).w_full().into_any() - }) - .w_full() - .h_full() - } - } - - let view = cx.update(|_, cx| { - cx.new(|_| TestView { - state: state.clone(), - item_height: item_height.clone(), - }) - }); - state.set_follow_mode(FollowMode::Tail); - - // At the bottom: 500px content, 200px viewport → top at item 6. - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.clone().into_any_element() - }); - assert_eq!(state.logical_scroll_top().item_ix, 6); - assert!(state.is_following_tail()); - - // Pause, then grow items (a zoom-in that pushes content below the fold). - // The frozen top must stay put rather than snapping to the new end, and - // following stays paused since we're no longer at the bottom. - state.pause_following_tail(); - item_height.set(80); - state.remeasure(); - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.clone().into_any_element() - }); - let offset = state.logical_scroll_top(); - assert_eq!(offset.item_ix, 6); - assert_eq!(offset.offset_in_item, px(0.)); - assert!( - !state.is_following_tail(), - "a paused list must not re-engage while the frozen top is off the bottom" - ); - - // Shrink back (zoom-out) so the frozen top once again reaches the - // bottom: follow-tail must re-engage on its own. - item_height.set(50); - state.remeasure(); - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.into_any_element() - }); - assert!( - state.is_following_tail(), - "returning to the bottom must restore follow-tail" - ); - } - - #[gpui::test] - fn test_follow_tail_disengages_on_user_scroll(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - // 10 items × 50px = 500px total, 200px viewport. - let state = ListState::new(10, crate::ListAlignment::Top, px(0.)); - - struct TestView(ListState); - impl Render for TestView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - list(self.0.clone(), |_, _, _| { - div().h(px(50.)).w_full().into_any() - }) - .w_full() - .h_full() - } - } - - state.set_follow_mode(FollowMode::Tail); - - // Paint with follow-tail — scroll anchored to the bottom. - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, cx| { - cx.new(|_| TestView(state.clone())).into_any_element() - }); - assert!(state.is_following_tail()); - - // Simulate the user scrolling up. - // This should disengage follow-tail. - cx.simulate_event(ScrollWheelEvent { - position: point(px(50.), px(100.)), - delta: ScrollDelta::Pixels(point(px(0.), px(100.))), - ..Default::default() - }); - - assert!( - !state.is_following_tail(), - "follow-tail should disengage when the user scrolls toward the start" - ); - } - - #[gpui::test] - fn test_follow_tail_disengages_on_scrollbar_reposition(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - // 10 items × 50px = 500px total, 200px viewport. - let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all(); - - struct TestView(ListState); - impl Render for TestView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - list(self.0.clone(), |_, _, _| { - div().h(px(50.)).w_full().into_any() - }) - .w_full() - .h_full() - } - } - - let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone()))); - - state.set_follow_mode(FollowMode::Tail); - - // Paint with follow-tail — scroll anchored to the bottom. - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.clone().into_any_element() - }); - assert!(state.is_following_tail()); - - // Simulate the scrollbar moving the viewport to the middle. - state.set_offset_from_scrollbar(point(px(0.), px(-150.))); - - let offset = state.logical_scroll_top(); - assert_eq!(offset.item_ix, 3); - assert_eq!(offset.offset_in_item, px(0.)); - assert!( - !state.is_following_tail(), - "follow-tail should disengage when the scrollbar manually repositions the list" - ); - - // A subsequent draw should preserve the user's manual position instead - // of snapping back to the end. - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.into_any_element() - }); - - let offset = state.logical_scroll_top(); - assert_eq!(offset.item_ix, 3); - assert_eq!(offset.offset_in_item, px(0.)); - } - - #[gpui::test] - fn test_scrollbar_drag_with_growing_content(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - let last_item_height = Rc::new(Cell::new(50usize)); - let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all(); - - struct TestView { - state: ListState, - last_item_height: Rc>, - } - impl Render for TestView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - let last_item_height = self.last_item_height.clone(); - list(self.state.clone(), move |index, _, _| { - let height = if index == 9 { - last_item_height.get() - } else { - 50 - }; - div().h(px(height as f32)).w_full().into_any() - }) - .w_full() - .h_full() - } - } - - let view = cx.update(|_, cx| { - cx.new(|_| TestView { - state: state.clone(), - last_item_height: last_item_height.clone(), - }) - }); - - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.clone().into_any_element() - }); - - state.scrollbar_drag_started(); - - state.set_offset_from_scrollbar(point(px(0.), px(-150.))); - let scrollbar_offset_before_growth = state.scroll_px_offset_for_scrollbar(); - - let offset = state.logical_scroll_top(); - assert_eq!(offset.item_ix, 3); - assert_eq!(offset.offset_in_item, px(0.)); - - last_item_height.set(550); - state.remeasure_items(9..10); - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.clone().into_any_element() - }); - - assert_eq!(state.max_offset_for_scrollbar().y, px(300.)); - assert_eq!( - state.scroll_px_offset_for_scrollbar(), - scrollbar_offset_before_growth - ); - - state.set_offset_from_scrollbar(point(px(0.), px(-150.))); - let offset = state.logical_scroll_top(); - assert_eq!(offset.item_ix, 3); - assert_eq!(offset.offset_in_item, px(0.)); - } - - #[gpui::test] - fn test_set_follow_tail_snaps_to_bottom(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - // 10 items × 50px = 500px total, 200px viewport. - let state = ListState::new(10, crate::ListAlignment::Top, px(0.)); - - struct TestView(ListState); - impl Render for TestView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - list(self.0.clone(), |_, _, _| { - div().h(px(50.)).w_full().into_any() - }) - .w_full() - .h_full() - } - } - - let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone()))); - - // Scroll to the middle of the list (item 3). - state.scroll_to(gpui::ListOffset { - item_ix: 3, - offset_in_item: px(0.), - }); - - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.clone().into_any_element() - }); - - let offset = state.logical_scroll_top(); - assert_eq!(offset.item_ix, 3); - assert_eq!(offset.offset_in_item, px(0.)); - assert!(!state.is_following_tail()); - - // Enable follow-tail — this should immediately snap the scroll anchor - // to the end, like the user just sent a prompt. - state.set_follow_mode(FollowMode::Tail); - - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.into_any_element() - }); - - // After paint, scroll should be at the bottom. - // 500px total − 200px viewport = 300px offset → item 6, offset 0. - let offset = state.logical_scroll_top(); - assert_eq!(offset.item_ix, 6); - assert_eq!(offset.offset_in_item, px(0.)); - assert!(state.is_following_tail()); - } - - #[gpui::test] - fn test_bottom_aligned_scrollbar_offset_at_end(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - const ITEMS: usize = 10; - const ITEM_SIZE: f32 = 50.0; - - let state = ListState::new( - ITEMS, - crate::ListAlignment::Bottom, - px(ITEMS as f32 * ITEM_SIZE), - ); - - struct TestView(ListState); - impl Render for TestView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - list(self.0.clone(), |_, _, _| { - div().h(px(ITEM_SIZE)).w_full().into_any() - }) - .w_full() - .h_full() - } - } - - cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| { - cx.new(|_| TestView(state.clone())).into_any_element() - }); - - // Bottom-aligned lists start pinned to the end: logical_scroll_top returns - // item_ix == item_count, meaning no explicit scroll position has been set. - assert_eq!(state.logical_scroll_top().item_ix, ITEMS); - - let max_offset = state.max_offset_for_scrollbar(); - let scroll_offset = state.scroll_px_offset_for_scrollbar(); - - assert_eq!( - -scroll_offset.y, max_offset.y, - "scrollbar offset ({}) should equal max offset ({}) when list is pinned to bottom", - -scroll_offset.y, max_offset.y, - ); - } - - /// When the user scrolls away from the bottom during follow_tail, - /// follow_tail suspends. If they scroll back to the bottom, the - /// next paint should re-engage follow_tail using fresh measurements. - #[gpui::test] - fn test_follow_tail_reengages_when_scrolled_back_to_bottom(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - // 10 items × 50px = 500px total, 200px viewport. - let state = ListState::new(10, crate::ListAlignment::Top, px(0.)); - - struct TestView(ListState); - impl Render for TestView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - list(self.0.clone(), |_, _, _| { - div().h(px(50.)).w_full().into_any() - }) - .w_full() - .h_full() - } - } - - let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone()))); - - state.set_follow_mode(FollowMode::Tail); - - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.clone().into_any_element() - }); - assert!(state.is_following_tail()); - - // Scroll up — follow_tail should suspend (not fully disengage). - cx.simulate_event(ScrollWheelEvent { - position: point(px(50.), px(100.)), - delta: ScrollDelta::Pixels(point(px(0.), px(50.))), - ..Default::default() - }); - assert!(!state.is_following_tail()); - - // Scroll back down to the bottom. - cx.simulate_event(ScrollWheelEvent { - position: point(px(50.), px(100.)), - delta: ScrollDelta::Pixels(point(px(0.), px(-10000.))), - ..Default::default() - }); - - // After a paint, follow_tail should re-engage because the - // layout confirmed we're at the true bottom. - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.clone().into_any_element() - }); - assert!( - state.is_following_tail(), - "follow_tail should re-engage after scrolling back to the bottom" - ); - } - - /// When an item is spliced to unmeasured (0px) while follow_tail - /// is suspended, the re-engagement check should still work correctly - #[gpui::test] - fn test_follow_tail_reengagement_not_fooled_by_unmeasured_items(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - // 20 items × 50px = 1000px total, 200px viewport, 1000px - // overdraw so all items get measured during the follow_tail - // paint (matching realistic production settings). - let state = ListState::new(20, crate::ListAlignment::Top, px(1000.)); - - struct TestView(ListState); - impl Render for TestView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - list(self.0.clone(), |_, _, _| { - div().h(px(50.)).w_full().into_any() - }) - .w_full() - .h_full() - } - } - - let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone()))); - - state.set_follow_mode(FollowMode::Tail); - - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.clone().into_any_element() - }); - assert!(state.is_following_tail()); - - // Scroll up a meaningful amount — suspends follow_tail. - // 20 items × 50px = 1000px. viewport 200px. scroll_max = 800px. - // Scrolling up 200px puts us at 600px, clearly not at bottom. - cx.simulate_event(ScrollWheelEvent { - position: point(px(50.), px(100.)), - delta: ScrollDelta::Pixels(point(px(0.), px(200.))), - ..Default::default() - }); - assert!(!state.is_following_tail()); - - // Invalidate the last item (simulates EntryUpdated calling - // remeasure_items). This makes items.summary().height - // temporarily wrong (0px for the invalidated item). - state.remeasure_items(19..20); - - // Paint — layout re-measures the invalidated item with its true - // height. The re-engagement check uses these fresh measurements. - // Since we scrolled 200px up from the 800px max, we're at - // ~600px — NOT at the bottom, so follow_tail should NOT - // re-engage. - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.clone().into_any_element() - }); - assert!( - !state.is_following_tail(), - "follow_tail should not falsely re-engage due to an unmeasured item \ - reducing items.summary().height" - ); - } - - #[gpui::test] - fn test_follow_tail_reengages_after_scrollbar_disengagement(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - - // 10 items × 50px = 500px total, 200px viewport, scroll_max = 300px. - let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all(); - - struct TestView(ListState); - impl Render for TestView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - list(self.0.clone(), |_, _, _| { - div().h(px(50.)).w_full().into_any() - }) - .w_full() - .h_full() - } - } - - let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone()))); - - state.set_follow_mode(FollowMode::Tail); - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.clone().into_any_element() - }); - assert!(state.is_following_tail()); - - // Drag the scrollbar up to the middle — follow_tail should suspend. - state.set_offset_from_scrollbar(point(px(0.), px(-150.))); - assert!(!state.is_following_tail()); - - // Drag the scrollbar back to the bottom — follow_tail should re-engage - // on the next paint. - state.set_offset_from_scrollbar(point(px(0.), px(-300.))); - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.into_any_element() - }); - assert!( - state.is_following_tail(), - "follow_tail should re-engage after scrolling back to the bottom via the scrollbar" - ); - } - - #[gpui::test] - fn test_follow_tail_reengages_after_scrollbar_drag_to_bottom_while_growing( - cx: &mut TestAppContext, - ) { - let cx = cx.add_empty_window(); - - let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all(); - - struct TestView(ListState); - impl Render for TestView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - list(self.0.clone(), |_, _, _| { - div().h(px(50.)).w_full().into_any() - }) - .w_full() - .h_full() - } - } - - let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone()))); - - state.set_follow_mode(FollowMode::Tail); - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.clone().into_any_element() - }); - assert!(state.is_following_tail()); - - state.scrollbar_drag_started(); - - state.splice(10..10, 10); - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.clone().into_any_element() - }); - - state.set_offset_from_scrollbar(point(px(0.), px(-300.))); - state.scrollbar_drag_ended(); - - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.into_any_element() - }); - - assert!( - state.is_following_tail(), - "follow_tail should re-engage when the user drags the scrollbar to \ - the bottom of its track, even when content has grown during the drag \ - (so frozen_bottom < live_bottom)" - ); - } -} diff --git a/crates/gpui_pre/src/elements/mod.rs b/crates/gpui_pre/src/elements/mod.rs deleted file mode 100644 index 8a2a1b7..0000000 --- a/crates/gpui_pre/src/elements/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -mod anchored; -mod animation; -mod canvas; -mod container_query; -mod deferred; -mod div; -mod image_cache; -mod img; -mod list; -mod surface; -mod svg; -mod text; -mod uniform_list; - -pub use anchored::*; -pub use animation::*; -pub use canvas::*; -pub use container_query::*; -pub use deferred::*; -pub use div::*; -pub use image_cache::*; -pub use img::*; -pub use list::*; -pub use surface::*; -pub use svg::*; -pub use text::*; -pub use uniform_list::*; diff --git a/crates/gpui_pre/src/elements/surface.rs b/crates/gpui_pre/src/elements/surface.rs deleted file mode 100644 index ac1c247..0000000 --- a/crates/gpui_pre/src/elements/surface.rs +++ /dev/null @@ -1,121 +0,0 @@ -use crate::{ - App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId, - ObjectFit, Pixels, Style, StyleRefinement, Styled, Window, -}; -#[cfg(target_os = "macos")] -use core_video::pixel_buffer::CVPixelBuffer; -use refineable::Refineable; - -/// A source of a surface's content. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum SurfaceSource { - /// A macOS image buffer from CoreVideo - #[cfg(target_os = "macos")] - Surface(CVPixelBuffer), -} - -#[cfg(target_os = "macos")] -impl From for SurfaceSource { - fn from(value: CVPixelBuffer) -> Self { - SurfaceSource::Surface(value) - } -} - -/// A surface element. -pub struct Surface { - source: SurfaceSource, - object_fit: ObjectFit, - style: StyleRefinement, -} - -/// Create a new surface element. -#[cfg(target_os = "macos")] -pub fn surface(source: impl Into) -> Surface { - Surface { - source: source.into(), - object_fit: ObjectFit::Contain, - style: Default::default(), - } -} - -impl Surface { - /// Set the object fit for the image. - pub fn object_fit(mut self, object_fit: ObjectFit) -> Self { - self.object_fit = object_fit; - self - } -} - -impl Element for Surface { - type RequestLayoutState = (); - type PrepaintState = (); - - fn id(&self) -> Option { - None - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _global_id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - let mut style = Style::default(); - style.refine(&self.style); - let layout_id = window.request_layout(style, [], cx); - (layout_id, ()) - } - - fn prepaint( - &mut self, - _global_id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - _window: &mut Window, - _cx: &mut App, - ) -> Self::PrepaintState { - } - - fn paint( - &mut self, - _global_id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - #[cfg_attr(not(target_os = "macos"), allow(unused_variables))] bounds: Bounds, - _: &mut Self::RequestLayoutState, - _: &mut Self::PrepaintState, - #[cfg_attr(not(target_os = "macos"), allow(unused_variables))] window: &mut Window, - _: &mut App, - ) { - match &self.source { - #[cfg(target_os = "macos")] - SurfaceSource::Surface(surface) => { - let size = crate::size(surface.get_width().into(), surface.get_height().into()); - let new_bounds = self.object_fit.get_bounds(bounds, size); - // TODO: Add support for corner_radii - window.paint_surface(new_bounds, surface.clone()); - } - #[allow(unreachable_patterns)] - _ => {} - } - } -} - -impl IntoElement for Surface { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -impl Styled for Surface { - fn style(&mut self) -> &mut StyleRefinement { - &mut self.style - } -} diff --git a/crates/gpui_pre/src/elements/svg.rs b/crates/gpui_pre/src/elements/svg.rs deleted file mode 100644 index 176072f..0000000 --- a/crates/gpui_pre/src/elements/svg.rs +++ /dev/null @@ -1,303 +0,0 @@ -use std::{ - fs, - hash::{Hash, Hasher}, - path::Path, - sync::Arc, -}; - -use crate::{ - App, Asset, Bounds, Element, GlobalElementId, Hitbox, InspectorElementId, InteractiveElement, - Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString, Size, - StyleRefinement, Styled, TransformationMatrix, Window, point, px, radians, size, -}; -use gpui_util::ResultExt; - -/// An SVG element. -pub struct Svg { - interactivity: Interactivity, - transformation: Option, - path: Option, - external_path: Option, - data: Option>, - data_path: Option, -} - -/// Create a new SVG element. -#[track_caller] -pub fn svg() -> Svg { - Svg { - interactivity: Interactivity::new(), - transformation: None, - path: None, - external_path: None, - data: None, - data_path: None, - } -} - -impl Svg { - /// Set the path to the SVG file for this element. - pub fn path(mut self, path: impl Into) -> Self { - self.path = Some(path.into()); - self - } - - /// Set the path to the SVG file for this element. - pub fn external_path(mut self, path: impl Into) -> Self { - self.external_path = Some(path.into()); - self - } - - /// Set the raw SVG data for this element. - /// The SVG will be rendered directly from the provided bytes. - pub fn data(mut self, data: &[u8]) -> Self { - // Generate a unique deterministic path based on the data hash for caching - let mut hasher = std::collections::hash_map::DefaultHasher::new(); - data.hash(&mut hasher); - let hash = hasher.finish(); - let path = SharedString::from(format!("__binary_svg__{}", hash)); - self.data = Some(Arc::from(data)); - self.data_path = Some(path); - self - } - - /// Transform the SVG element with the given transformation. - /// Note that this won't effect the hitbox or layout of the element, only the rendering. - pub fn with_transformation(mut self, transformation: Transformation) -> Self { - self.transformation = Some(transformation); - self - } -} - -impl Element for Svg { - type RequestLayoutState = (); - type PrepaintState = Option; - - fn id(&self) -> Option { - self.interactivity.element_id.clone() - } - - fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { - self.interactivity.source_location() - } - - fn request_layout( - &mut self, - global_id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - let layout_id = self.interactivity.request_layout( - global_id, - inspector_id, - window, - cx, - |style, window, cx| window.request_layout(style, None, cx), - ); - (layout_id, ()) - } - - fn prepaint( - &mut self, - global_id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Option { - self.interactivity.prepaint( - global_id, - inspector_id, - bounds, - bounds.size, - window, - cx, - |_, _, hitbox, _, _| hitbox, - ) - } - - fn paint( - &mut self, - global_id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - hitbox: &mut Option, - window: &mut Window, - cx: &mut App, - ) where - Self: Sized, - { - self.interactivity.paint( - global_id, - inspector_id, - bounds, - hitbox.as_ref(), - window, - cx, - |style, window, cx| { - let transformation = self - .transformation - .as_ref() - .map(|transformation| { - transformation.into_matrix(bounds.center(), window.scale_factor()) - }) - .unwrap_or_default(); - - if let Some((data, path)) = self.data.as_ref().zip(self.data_path.as_ref()) { - if let Some(color) = style.text.color { - window - .paint_svg( - bounds, - path.clone(), - Some(&**data), - transformation, - color, - cx, - ) - .log_err(); - } - } else if let Some((path, color)) = - self.external_path.as_ref().zip(style.text.color) - { - let Some(bytes) = window - .use_asset::(path, cx) - .and_then(|asset| asset.log_err()) - else { - return; - }; - - window - .paint_svg( - bounds, - path.clone(), - Some(&bytes), - transformation, - color, - cx, - ) - .log_err(); - } else if let Some((path, color)) = self.path.as_ref().zip(style.text.color) { - window - .paint_svg(bounds, path.clone(), None, transformation, color, cx) - .log_err(); - } - }, - ) - } -} - -impl IntoElement for Svg { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -impl Styled for Svg { - fn style(&mut self) -> &mut StyleRefinement { - &mut self.interactivity.base_style - } -} - -impl InteractiveElement for Svg { - fn interactivity(&mut self) -> &mut Interactivity { - &mut self.interactivity - } -} - -/// A transformation to apply to an SVG element. -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct Transformation { - scale: Size, - translate: Point, - rotate: Radians, -} - -impl Default for Transformation { - fn default() -> Self { - Self { - scale: size(1.0, 1.0), - translate: point(px(0.0), px(0.0)), - rotate: radians(0.0), - } - } -} - -impl Transformation { - /// Create a new Transformation with the specified scale along each axis. - pub fn scale(scale: Size) -> Self { - Self { - scale, - translate: point(px(0.0), px(0.0)), - rotate: radians(0.0), - } - } - - /// Create a new Transformation with the specified translation. - pub fn translate(translate: Point) -> Self { - Self { - scale: size(1.0, 1.0), - translate, - rotate: radians(0.0), - } - } - - /// Create a new Transformation with the specified rotation in radians. - pub fn rotate(rotate: impl Into) -> Self { - let rotate = rotate.into(); - Self { - scale: size(1.0, 1.0), - translate: point(px(0.0), px(0.0)), - rotate, - } - } - - /// Update the scaling factor of this transformation. - pub fn with_scaling(mut self, scale: Size) -> Self { - self.scale = scale; - self - } - - /// Update the translation value of this transformation. - pub fn with_translation(mut self, translate: Point) -> Self { - self.translate = translate; - self - } - - /// Update the rotation angle of this transformation. - pub fn with_rotation(mut self, rotate: impl Into) -> Self { - self.rotate = rotate.into(); - self - } - - fn into_matrix(self, center: Point, scale_factor: f32) -> TransformationMatrix { - //Note: if you read this as a sequence of matrix multiplications, start from the bottom - TransformationMatrix::unit() - .translate(center.scale(scale_factor) + self.translate.scale(scale_factor)) - .rotate(self.rotate) - .scale(self.scale) - .translate(center.scale(-scale_factor)) - } -} - -enum SvgAsset {} - -impl Asset for SvgAsset { - type Source = SharedString; - type Output = Result, Arc>; - - fn load( - source: Self::Source, - _cx: &mut App, - ) -> impl Future + Send + 'static { - async move { - let bytes = fs::read(Path::new(source.as_ref())).map_err(|e| Arc::new(e))?; - let bytes = Arc::from(bytes); - Ok(bytes) - } - } -} diff --git a/crates/gpui_pre/src/elements/text.rs b/crates/gpui_pre/src/elements/text.rs deleted file mode 100644 index 3470ac9..0000000 --- a/crates/gpui_pre/src/elements/text.rs +++ /dev/null @@ -1,1313 +0,0 @@ -use crate::{ - ActiveTooltip, AnyView, App, Bounds, DispatchPhase, Element, ElementId, GlobalElementId, - HighlightStyle, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, LayoutId, - MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextOverflow, - TextRun, TextStyle, TooltipId, TruncateFrom, WhiteSpace, Window, WrappedLine, - WrappedLineLayout, register_tooltip_mouse_handlers, set_tooltip_on_window, -}; -use anyhow::Context as _; -use gpui_util::ResultExt; -use itertools::Itertools; -use smallvec::SmallVec; -use std::{ - borrow::Cow, - cell::{Cell, RefCell}, - mem, - ops::{Deref, DerefMut, Range}, - rc::Rc, - sync::Arc, -}; - -/// An [`Element`] that renders text. -/// -/// In general, [`Text`] objects should be created via the [`text`] macro: -/// ```rust -/// # use gpui::*; -/// # fn render() -> impl IntoElement { -/// div().child(text!("hello")) -/// # } -/// ``` -/// ## IDs and Accessibility -/// -/// [`Text`] elements have an ID. This ID is primarily used to produce nodes in -/// the accessibility tree, which allows the text to be visible to screen -/// readers and other assistive technologies. -/// -/// This ID is stable across frames. If the same text, with the same ID, is -/// present in two consecutive frames, no updates are reported to the screen -/// reader. If the text changes, but the ID stays the same, then the screen -/// reader will be notified that a text node's content has changed. **However**, -/// if the ID changes, then the screen reader will be notified that a node has -/// been removed, and a new node has been added. -/// -/// When using the [`text`] macro, each invocation of the macro will get a -/// unique ID, derived from its position in the source code (filename, line, and -/// column). For example: -/// ```rust -/// # use gpui::*; -/// let x = text!("hello"); -/// let y = text!("hello"); -/// // not equal, because different `text!` invocations produced them -/// assert_ne!(x.id(), y.id()); -/// -/// fn make_text(s: &str) -> Text { text!(s) } -/// let x = make_text("hello"); -/// let y = make_text("hello"); -/// // equal, because the same `text!` invocation produced them -/// assert_eq!(x.id(), y.id()); -/// ``` -/// When the contents of an invocation of [`text`] do not change, this -/// distinction is less relevant (with the caveat that you still need to take -/// care to ensure that duplicate IDs do not appear). -/// -/// However, when a [`text`] invocation's argument *does* change, you should -/// consider whether this change should be reported as a node "updating its -/// contents", or an old node being destroyed and a new node being created. -#[derive(Debug, Clone)] -pub struct Text { - id: Option, - text: SharedString, -} - -impl Text { - /// Create a new [`Text`] element with a specific ID. - /// - /// If you want a unique ID to be assigned automatically, use the [`text`] - /// macro. The docs for [`Text`] have more detail about choosing IDs. - #[inline] - pub const fn new(id: ElementId, text: SharedString) -> Self { - Self { id: Some(id), text } - } - - /// Create a new [`Text`] element that is inaccessible to screen readers. - /// - /// In order for text to be accessible to screen readers, it must have an ID - /// provided. If you want text to be accessible, either use [`text`] to have - /// an ID automatically assigned, or use [`Text::new`] to manually assign an - /// ID. - /// - /// This function is intended for use inside custom UI components, where - /// accessible properties may be set on parent containers. - #[inline] - pub const fn new_inaccessible(text: SharedString) -> Self { - Self { id: None, text } - } - - /// The ID of this [`Text`] element. - #[inline] - pub const fn id(&self) -> Option<&ElementId> { - self.id.as_ref() - } - - /// Produce a new [`Text`] with the given `id`. - pub fn with_id(mut self, id: impl Into) -> Self { - self.id = Some(id.into()); - self - } - - /// The text that this [`Text`] element will display. - #[inline] - pub const fn text(&self) -> &SharedString { - &self.text - } -} - -impl Deref for Text { - type Target = SharedString; - fn deref(&self) -> &Self::Target { - &self.text - } -} - -impl DerefMut for Text { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.text - } -} - -/// Trivial hash function for the location information produced by the [`text`] -/// macro. Not covered by semver guarantees. Performance is not particularly -/// significant because it's only used on small strings in const contexts. -#[doc(hidden)] -pub const fn __hash_text_macro_location_unstable_do_not_use(s: &'static str) -> u64 { - const BASIS: u64 = 0xcbf29ce484222325; - const PRIME: u64 = 0x100000001b3; - - let bytes = s.as_bytes(); - let mut hash = BASIS; - let mut i = 0; - while i < bytes.len() { - hash ^= bytes[i] as u64; - hash = hash.wrapping_mul(PRIME); - i += 1; - } - hash -} - -/// Create a new [`Text`] element. -/// -/// ```rust -/// # use gpui::*; -/// let a = text!("hello"); -/// let b = text!(id = "farewell-message", "hello"); -/// -/// ``` -/// -/// Text created with this macro is *accessible*. The macro generates an ID -/// based on the source location. See the docs for [`Text`] for a more in-depth -/// explanation of the significance of the ID of a [`Text`] element. -#[macro_export] -macro_rules! text { - (id = $id:expr, $text:expr) => {{ $crate::Text::new($id.into(), $text.into()) }}; - ($text:expr) => {{ - const ID: &'static str = concat!(file!(), "/", line!(), ":", column!()); - const HASH: u64 = $crate::__hash_text_macro_location_unstable_do_not_use(ID); - $crate::Text::new($crate::ElementId::Integer(HASH), $text.into()) - }}; -} - -impl IntoElement for Text { - type Element = Self; - #[inline] - fn into_element(self) -> Self::Element { - self - } -} - -impl Element for Text { - type RequestLayoutState = TextLayout; - type PrepaintState = (); - - fn id(&self) -> Option { - self.id.clone() - } - - fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { - None - } - - fn a11y_role(&self) -> Option { - if self.id.is_some() { - Some(accesskit::Role::Label) - } else { - None - } - } - - fn write_a11y_info(&self, node: &mut accesskit::Node) { - node.set_value(self.text.to_string()); - } - - fn request_layout( - &mut self, - id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - ::request_layout(&mut self.text, id, inspector_id, window, cx) - } - - fn prepaint( - &mut self, - id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState { - ::prepaint( - &mut self.text, - id, - inspector_id, - bounds, - request_layout, - window, - cx, - ) - } - - fn paint( - &mut self, - id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - request_layout: &mut Self::RequestLayoutState, - prepaint: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - ::paint( - &mut self.text, - id, - inspector_id, - bounds, - request_layout, - prepaint, - window, - cx, - ); - } -} - -impl Element for &'static str { - type RequestLayoutState = TextLayout; - type PrepaintState = (); - - fn id(&self) -> Option { - None - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - let mut state = TextLayout::default(); - let layout_id = state.layout(SharedString::from(*self), None, window, cx); - (layout_id, state) - } - - fn prepaint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - text_layout: &mut Self::RequestLayoutState, - _window: &mut Window, - _cx: &mut App, - ) { - text_layout.prepaint(bounds, self) - } - - fn paint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _bounds: Bounds, - text_layout: &mut TextLayout, - _: &mut (), - window: &mut Window, - cx: &mut App, - ) { - text_layout.paint(self, window, cx) - } -} - -impl IntoElement for &'static str { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -impl IntoElement for String { - type Element = SharedString; - - fn into_element(self) -> Self::Element { - self.into() - } -} - -impl IntoElement for Cow<'static, str> { - type Element = SharedString; - - fn into_element(self) -> Self::Element { - self.into() - } -} - -impl Element for SharedString { - type RequestLayoutState = TextLayout; - type PrepaintState = (); - - fn id(&self) -> Option { - None - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - let mut state = TextLayout::default(); - let layout_id = state.layout(self.clone(), None, window, cx); - (layout_id, state) - } - - fn prepaint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - text_layout: &mut Self::RequestLayoutState, - _window: &mut Window, - _cx: &mut App, - ) { - text_layout.prepaint(bounds, self.as_ref()) - } - - fn paint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _bounds: Bounds, - text_layout: &mut Self::RequestLayoutState, - _: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - text_layout.paint(self.as_ref(), window, cx) - } -} - -impl IntoElement for SharedString { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -/// Renders text with runs of different styles. -/// -/// Callers are responsible for setting the correct style for each run. -/// For text with a uniform style, you can usually avoid calling this constructor -/// and just pass text directly. -pub struct StyledText { - text: SharedString, - runs: Option>, - delayed_highlights: Option, HighlightStyle)>>, - delayed_font_family_overrides: Option, SharedString)>>, - layout: TextLayout, -} - -impl StyledText { - /// Construct a new styled text element from the given string. - pub fn new(text: impl Into) -> Self { - StyledText { - text: text.into(), - runs: None, - delayed_highlights: None, - delayed_font_family_overrides: None, - layout: TextLayout::default(), - } - } - - /// Get the layout for this element. This can be used to map indices to pixels and vice versa. - pub fn layout(&self) -> &TextLayout { - &self.layout - } - - /// Set the styling attributes for the given text, as well as - /// as any ranges of text that have had their style customized. - pub fn with_default_highlights( - mut self, - default_style: &TextStyle, - highlights: impl IntoIterator, HighlightStyle)>, - ) -> Self { - debug_assert!( - self.delayed_highlights.is_none(), - "Can't use `with_default_highlights` and `with_highlights`" - ); - let runs = Self::compute_runs(&self.text, default_style, highlights); - self.with_runs(runs) - } - - /// Set the styling attributes for the given text, as well as - /// as any ranges of text that have had their style customized. - pub fn with_highlights( - mut self, - highlights: impl IntoIterator, HighlightStyle)>, - ) -> Self { - debug_assert!( - self.runs.is_none(), - "Can't use `with_highlights` and `with_default_highlights`" - ); - self.delayed_highlights = Some( - highlights - .into_iter() - .inspect(|(run, _)| { - debug_assert!(self.text.is_char_boundary(run.start)); - debug_assert!(self.text.is_char_boundary(run.end)); - }) - .collect::>(), - ); - self - } - - fn compute_runs( - text: &str, - default_style: &TextStyle, - highlights: impl IntoIterator, HighlightStyle)>, - ) -> Vec { - let mut runs = Vec::new(); - let mut ix = 0; - for (range, highlight) in highlights { - if ix < range.start { - debug_assert!(text.is_char_boundary(range.start)); - runs.push(default_style.clone().to_run(range.start - ix)); - } - debug_assert!(text.is_char_boundary(range.end)); - runs.push( - default_style - .clone() - .highlight(highlight) - .to_run(range.len()), - ); - ix = range.end; - } - if ix < text.len() { - runs.push(default_style.to_run(text.len() - ix)); - } - runs - } - - /// Override the font family for specific byte ranges of the text. - /// - /// This is resolved lazily at layout time, so the overrides are applied - /// on top of the inherited text style from the parent element. - /// Can be combined with [`with_highlights`](Self::with_highlights). - /// - /// The overrides must be sorted by range start and non-overlapping. - /// Each override range must fall on character boundaries. - pub fn with_font_family_overrides( - mut self, - overrides: impl IntoIterator, SharedString)>, - ) -> Self { - self.delayed_font_family_overrides = Some( - overrides - .into_iter() - .inspect(|(range, _)| { - debug_assert!(self.text.is_char_boundary(range.start)); - debug_assert!(self.text.is_char_boundary(range.end)); - }) - .collect(), - ); - self - } - - fn apply_font_family_overrides( - runs: &mut [TextRun], - overrides: &[(Range, SharedString)], - ) { - let mut byte_offset = 0; - let mut override_idx = 0; - for run in runs.iter_mut() { - let run_end = byte_offset + run.len; - while override_idx < overrides.len() && overrides[override_idx].0.end <= byte_offset { - override_idx += 1; - } - if override_idx < overrides.len() { - let (ref range, ref family) = overrides[override_idx]; - if byte_offset >= range.start && run_end <= range.end { - run.font.family = family.clone(); - } - } - byte_offset = run_end; - } - } - - /// Set the text runs for this piece of text. - pub fn with_runs(mut self, runs: Vec) -> Self { - let mut text = &*self.text; - for run in &runs { - text = text.get(run.len..).unwrap_or_else(|| { - #[cfg(debug_assertions)] - panic!("invalid text run. Text: '{text}', run: {run:?}"); - #[cfg(not(debug_assertions))] - panic!("invalid text run"); - }); - } - assert!(text.is_empty(), "invalid text run"); - self.runs = Some(runs); - self - } -} - -impl Element for StyledText { - type RequestLayoutState = (); - type PrepaintState = (); - - fn id(&self) -> Option { - None - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - let font_family_overrides = self.delayed_font_family_overrides.take(); - let mut runs = self.runs.take().or_else(|| { - self.delayed_highlights.take().map(|delayed_highlights| { - Self::compute_runs(&self.text, &window.text_style(), delayed_highlights) - }) - }); - - if let Some(ref overrides) = font_family_overrides { - let runs = - runs.get_or_insert_with(|| vec![window.text_style().to_run(self.text.len())]); - Self::apply_font_family_overrides(runs, overrides); - } - - let layout_id = self.layout.layout(self.text.clone(), runs, window, cx); - (layout_id, ()) - } - - fn prepaint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - _: &mut Self::RequestLayoutState, - _window: &mut Window, - _cx: &mut App, - ) { - self.layout.prepaint(bounds, &self.text) - } - - fn paint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _bounds: Bounds, - _: &mut Self::RequestLayoutState, - _: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - self.layout.paint(&self.text, window, cx) - } -} - -impl IntoElement for StyledText { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -/// The Layout for TextElement. This can be used to map indices to pixels and vice versa. -#[derive(Default, Clone)] -pub struct TextLayout(Rc>>); - -struct TextLayoutInner { - len: usize, - lines: SmallVec<[WrappedLine; 1]>, - line_height: Pixels, - wrap_width: Option, - truncate_width: Option, - size: Option>, - bounds: Option>, -} - -impl TextLayout { - fn layout( - &self, - text: SharedString, - runs: Option>, - window: &mut Window, - _: &mut App, - ) -> LayoutId { - let text_style = window.text_style(); - let font_size = text_style.font_size.to_pixels(window.rem_size()); - let line_height = window.pixel_snap( - text_style - .line_height - .to_pixels(font_size.into(), window.rem_size()), - ); - - let runs = if let Some(runs) = runs { - runs - } else { - vec![text_style.to_run(text.len())] - }; - window.request_measured_layout(Default::default(), { - let element_state = self.clone(); - - move |known_dimensions, available_space, window, cx| { - let wrap_width = if text_style.white_space == WhiteSpace::Normal { - known_dimensions.width.or(match available_space.width { - crate::AvailableSpace::Definite(x) => Some(x), - _ => None, - }) - } else { - None - }; - - let (truncate_width, truncation_affix, truncate_from) = - if let Some(text_overflow) = text_style.text_overflow.clone() { - let width = known_dimensions.width.or(match available_space.width { - crate::AvailableSpace::Definite(x) => match text_style.line_clamp { - Some(max_lines) => Some(x * max_lines), - None => Some(x), - }, - _ => None, - }); - - match text_overflow { - TextOverflow::Truncate(s) => (width, s, TruncateFrom::End), - TextOverflow::TruncateStart(s) => (width, s, TruncateFrom::Start), - TextOverflow::TruncateMiddle(s) => (width, s, TruncateFrom::Middle), - } - } else { - (None, "".into(), TruncateFrom::End) - }; - - // Only use cached layout if: - // 1. We have a cached size - // 2. wrap_width matches (or both are None) - // 3. truncate_width is None (if truncate_width is Some, we need to re-layout - // because the previous layout may have been computed without truncation) - // 4. the cached layout was not truncated (a truncated layout answers an - // unconstrained probe with the truncated size, which poisons intrinsic - // sizing with whatever width some earlier measure pass happened to use) - if let Some(text_layout) = element_state.0.borrow().as_ref() - && let Some(size) = text_layout.size - && (wrap_width.is_none() || wrap_width == text_layout.wrap_width) - && truncate_width.is_none() - && text_layout.truncate_width.is_none() - { - return size; - } - - let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size); - let (text, runs) = if let Some(truncate_width) = truncate_width { - if let Some(max_lines) = text_style.line_clamp - && let Some(wrap_width) = wrap_width - { - line_wrapper.truncate_wrapped_line( - text.clone(), - wrap_width, - max_lines, - &truncation_affix, - &runs, - truncate_from, - ) - } else if let Some(unclipped) = window - .text_system() - .shape_text(text.clone(), font_size, &runs, None, None) - .log_err() - && unclipped - .iter() - .all(|line| line.size(line_height).width <= truncate_width) - { - // The truncation decision below sums per-character advances, - // which overestimates the shaped width (no kerning), truncating - // text that fits exactly in its measured width. Skip truncation - // whenever the honestly-shaped text fits; the shaping result - // comes from the line layout cache when the same text was - // already measured untruncated this frame. - (text.clone(), Cow::Borrowed(&*runs)) - } else { - line_wrapper.truncate_line( - text.clone(), - truncate_width, - &truncation_affix, - &runs, - truncate_from, - ) - } - } else { - (text.clone(), Cow::Borrowed(&*runs)) - }; - let len = text.len(); - - let Some(lines) = window - .text_system() - .shape_text( - text, - font_size, - &runs, - wrap_width, // Wrap if we know the width. - text_style.line_clamp, // Limit the number of lines if line_clamp is set. - ) - .log_err() - else { - element_state.0.borrow_mut().replace(TextLayoutInner { - lines: Default::default(), - len: 0, - line_height, - wrap_width, - truncate_width, - size: Some(Size::default()), - bounds: None, - }); - return Size::default(); - }; - - let mut size: Size = Size::default(); - for line in &lines { - let line_size = line.size(line_height); - size.height += line_size.height; - size.width = size.width.max(line_size.width).ceil(); - } - - element_state.0.borrow_mut().replace(TextLayoutInner { - lines, - len, - line_height, - wrap_width, - truncate_width, - size: Some(size), - bounds: None, - }); - - size - } - }) - } - - fn prepaint(&self, bounds: Bounds, text: &str) { - let mut element_state = self.0.borrow_mut(); - let element_state = element_state - .as_mut() - .with_context(|| format!("measurement has not been performed on {text}")) - .unwrap(); - element_state.bounds = Some(bounds); - } - - fn paint(&self, text: &str, window: &mut Window, cx: &mut App) { - let element_state = self.0.borrow(); - let element_state = element_state - .as_ref() - .with_context(|| format!("measurement has not been performed on {text}")) - .unwrap(); - let bounds = element_state - .bounds - .with_context(|| format!("prepaint has not been performed on {text}")) - .unwrap(); - - let line_height = element_state.line_height; - let mut line_origin = bounds.origin; - let text_style = window.text_style(); - for line in &element_state.lines { - line.paint_background( - line_origin, - line_height, - text_style.text_align, - Some(bounds), - window, - cx, - ) - .log_err(); - line.paint( - line_origin, - line_height, - text_style.text_align, - Some(bounds), - window, - cx, - ) - .log_err(); - line_origin.y += line.size(line_height).height; - } - } - - /// Get the byte index into the input of the pixel position. - pub fn index_for_position(&self, mut position: Point) -> Result { - let element_state = self.0.borrow(); - let element_state = element_state - .as_ref() - .expect("measurement has not been performed"); - let bounds = element_state - .bounds - .expect("prepaint has not been performed"); - - if position.y < bounds.top() { - return Err(0); - } - - let line_height = element_state.line_height; - let mut line_origin = bounds.origin; - let mut line_start_ix = 0; - for line in &element_state.lines { - let line_bottom = line_origin.y + line.size(line_height).height; - if position.y > line_bottom { - line_origin.y = line_bottom; - line_start_ix += line.len() + 1; - } else { - let position_within_line = position - line_origin; - match line.index_for_position(position_within_line, line_height) { - Ok(index_within_line) => return Ok(line_start_ix + index_within_line), - Err(index_within_line) => return Err(line_start_ix + index_within_line), - } - } - } - - Err(line_start_ix.saturating_sub(1)) - } - - /// Get the pixel position for the given byte index. - pub fn position_for_index(&self, index: usize) -> Option> { - let element_state = self.0.borrow(); - let element_state = element_state - .as_ref() - .expect("measurement has not been performed"); - let bounds = element_state - .bounds - .expect("prepaint has not been performed"); - let line_height = element_state.line_height; - - let mut line_origin = bounds.origin; - let mut line_start_ix = 0; - - for line in &element_state.lines { - let line_end_ix = line_start_ix + line.len(); - if index < line_start_ix { - break; - } else if index > line_end_ix { - line_origin.y += line.size(line_height).height; - line_start_ix = line_end_ix + 1; - continue; - } else { - let ix_within_line = index - line_start_ix; - return Some(line_origin + line.position_for_index(ix_within_line, line_height)?); - } - } - - None - } - - /// Retrieve the layout for the line containing the given byte index. - pub fn line_layout_for_index(&self, index: usize) -> Option> { - let element_state = self.0.borrow(); - let element_state = element_state - .as_ref() - .expect("measurement has not been performed"); - let mut line_start_ix = 0; - - for line in &element_state.lines { - let line_end_ix = line_start_ix + line.len(); - if index < line_start_ix { - break; - } else if index > line_end_ix { - line_start_ix = line_end_ix + 1; - continue; - } else { - return Some(line.layout.clone()); - } - } - - None - } - - /// Retrieve all line layouts in source order. - pub fn line_layouts(&self) -> SmallVec<[Arc; 1]> { - self.0 - .borrow() - .as_ref() - .expect("measurement has not been performed") - .lines - .iter() - .map(|line| line.layout.clone()) - .collect() - } - - /// The bounds of this layout. - pub fn bounds(&self) -> Bounds { - self.0.borrow().as_ref().unwrap().bounds.unwrap() - } - - /// The line height for this layout. - pub fn line_height(&self) -> Pixels { - self.0.borrow().as_ref().unwrap().line_height - } - - /// The UTF-8 length of the underlying text. - pub fn len(&self) -> usize { - self.0.borrow().as_ref().unwrap().len - } - - /// The text for this layout. - pub fn text(&self) -> String { - self.0 - .borrow() - .as_ref() - .unwrap() - .lines - .iter() - .map(|s| &s.text) - .join("\n") - } - - /// The text for this layout (with soft-wraps as newlines) - pub fn wrapped_text(&self) -> String { - let mut accumulator = String::new(); - - for wrapped in self.0.borrow().as_ref().unwrap().lines.iter() { - let mut seen = 0; - for boundary in wrapped.layout.wrap_boundaries.iter() { - let index = wrapped.layout.unwrapped_layout.runs[boundary.run_ix].glyphs - [boundary.glyph_ix] - .index; - - accumulator.push_str(&wrapped.text[seen..index]); - accumulator.push('\n'); - seen = index; - } - accumulator.push_str(&wrapped.text[seen..]); - accumulator.push('\n'); - } - // Remove trailing newline - accumulator.pop(); - accumulator - } -} - -/// A text element that can be interacted with. -pub struct InteractiveText { - element_id: ElementId, - text: StyledText, - click_listener: - Option], InteractiveTextClickEvent, &mut Window, &mut App)>>, - hover_listener: Option, MouseMoveEvent, &mut Window, &mut App)>>, - tooltip_builder: Option Option>>, - tooltip_id: Option, - clickable_ranges: Vec>, -} - -struct InteractiveTextClickEvent { - mouse_down_index: usize, - mouse_up_index: usize, -} - -#[doc(hidden)] -#[derive(Default)] -pub struct InteractiveTextState { - mouse_down_index: Rc>>, - hovered_index: Rc>>, - active_tooltip: Rc>>, -} - -/// InteractiveTest is a wrapper around StyledText that adds mouse interactions. -impl InteractiveText { - /// Creates a new InteractiveText from the given text. - pub fn new(id: impl Into, text: StyledText) -> Self { - Self { - element_id: id.into(), - text, - click_listener: None, - hover_listener: None, - tooltip_builder: None, - tooltip_id: None, - clickable_ranges: Vec::new(), - } - } - - /// on_click is called when the user clicks on one of the given ranges, passing the index of - /// the clicked range. - pub fn on_click( - mut self, - ranges: Vec>, - listener: impl Fn(usize, &mut Window, &mut App) + 'static, - ) -> Self { - self.click_listener = Some(Box::new(move |ranges, event, window, cx| { - for (range_ix, range) in ranges.iter().enumerate() { - if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index) - { - listener(range_ix, window, cx); - } - } - })); - self.clickable_ranges = ranges; - self - } - - /// on_hover is called when the mouse moves over a character within the text, passing the - /// index of the hovered character, or None if the mouse leaves the text. - pub fn on_hover( - mut self, - listener: impl Fn(Option, MouseMoveEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.hover_listener = Some(Box::new(listener)); - self - } - - /// tooltip lets you specify a tooltip for a given character index in the string. - pub fn tooltip( - mut self, - builder: impl Fn(usize, &mut Window, &mut App) -> Option + 'static, - ) -> Self { - self.tooltip_builder = Some(Rc::new(builder)); - self - } -} - -impl Element for InteractiveText { - type RequestLayoutState = (); - type PrepaintState = Hitbox; - - fn id(&self) -> Option { - Some(self.element_id.clone()) - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn a11y_role(&self) -> Option { - Some(accesskit::Role::Label) - } - - fn write_a11y_info(&self, node: &mut accesskit::Node) { - node.set_value(self.text.text.to_string()); - } - - fn request_layout( - &mut self, - _id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - self.text.request_layout(None, inspector_id, window, cx) - } - - fn prepaint( - &mut self, - global_id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - state: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Hitbox { - window.with_optional_element_state::( - global_id, - |interactive_state, window| { - let mut interactive_state = interactive_state - .map(|interactive_state| interactive_state.unwrap_or_default()); - - if let Some(interactive_state) = interactive_state.as_mut() { - if self.tooltip_builder.is_some() { - self.tooltip_id = - set_tooltip_on_window(&interactive_state.active_tooltip, window); - } else { - // If there is no longer a tooltip builder, remove the active tooltip. - interactive_state.active_tooltip.take(); - } - } - - self.text - .prepaint(None, inspector_id, bounds, state, window, cx); - let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); - (hitbox, interactive_state) - }, - ) - } - - fn paint( - &mut self, - global_id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - _: &mut Self::RequestLayoutState, - hitbox: &mut Hitbox, - window: &mut Window, - cx: &mut App, - ) { - let current_view = window.current_view(); - let text_layout = self.text.layout().clone(); - window.with_element_state::( - global_id.unwrap(), - |interactive_state, window| { - let mut interactive_state = interactive_state.unwrap_or_default(); - if let Some(click_listener) = self.click_listener.take() { - let mouse_position = window.mouse_position(); - if let Ok(ix) = text_layout.index_for_position(mouse_position) - && self - .clickable_ranges - .iter() - .any(|range| range.contains(&ix)) - { - window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox) - } - - let text_layout = text_layout.clone(); - let mouse_down = interactive_state.mouse_down_index.clone(); - if let Some(mouse_down_index) = mouse_down.get() { - let hitbox = hitbox.clone(); - let clickable_ranges = mem::take(&mut self.clickable_ranges); - window.on_mouse_event( - move |event: &MouseUpEvent, phase, window: &mut Window, cx| { - if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { - if let Ok(mouse_up_index) = - text_layout.index_for_position(event.position) - { - click_listener( - &clickable_ranges, - InteractiveTextClickEvent { - mouse_down_index, - mouse_up_index, - }, - window, - cx, - ) - } - - mouse_down.take(); - window.refresh(); - } - }, - ); - } else { - let hitbox = hitbox.clone(); - window.on_mouse_event(move |event: &MouseDownEvent, phase, window, _| { - if phase == DispatchPhase::Bubble - && hitbox.is_hovered(window) - && let Ok(mouse_down_index) = - text_layout.index_for_position(event.position) - { - mouse_down.set(Some(mouse_down_index)); - window.refresh(); - } - }); - } - } - - window.on_mouse_event({ - let mut hover_listener = self.hover_listener.take(); - let hitbox = hitbox.clone(); - let text_layout = text_layout.clone(); - let hovered_index = interactive_state.hovered_index.clone(); - move |event: &MouseMoveEvent, phase, window, cx| { - if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { - let current = hovered_index.get(); - let updated = text_layout.index_for_position(event.position).ok(); - if current != updated { - hovered_index.set(updated); - if let Some(hover_listener) = hover_listener.as_ref() { - hover_listener(updated, event.clone(), window, cx); - } - cx.notify(current_view); - } - } - } - }); - - if let Some(tooltip_builder) = self.tooltip_builder.clone() { - let active_tooltip = interactive_state.active_tooltip.clone(); - let build_tooltip = Rc::new({ - let tooltip_is_hoverable = false; - let text_layout = text_layout.clone(); - move |window: &mut Window, cx: &mut App| { - text_layout - .index_for_position(window.mouse_position()) - .ok() - .and_then(|position| tooltip_builder(position, window, cx)) - .map(|view| (view, tooltip_is_hoverable)) - } - }); - - // Use bounds instead of testing hitbox since this is called during prepaint. - let check_is_hovered_during_prepaint = Rc::new({ - let source_bounds = hitbox.bounds; - let text_layout = text_layout.clone(); - let pending_mouse_down = interactive_state.mouse_down_index.clone(); - move |window: &Window| { - text_layout - .index_for_position(window.mouse_position()) - .is_ok() - && source_bounds.contains(&window.mouse_position()) - && pending_mouse_down.get().is_none() - } - }); - - let check_is_hovered = Rc::new({ - let hitbox = hitbox.clone(); - let text_layout = text_layout.clone(); - let pending_mouse_down = interactive_state.mouse_down_index.clone(); - move |window: &Window| { - text_layout - .index_for_position(window.mouse_position()) - .is_ok() - && hitbox.is_hovered(window) - && pending_mouse_down.get().is_none() - } - }); - - register_tooltip_mouse_handlers( - &active_tooltip, - self.tooltip_id, - build_tooltip, - check_is_hovered, - check_is_hovered_during_prepaint, - None, - window, - ); - } - - self.text - .paint(None, inspector_id, bounds, &mut (), &mut (), window, cx); - - ((), interactive_state) - }, - ); - } -} - -impl IntoElement for InteractiveText { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_into_element_for() { - use crate::{ParentElement as _, SharedString, div}; - use std::borrow::Cow; - - let _ = div().child("static str"); - let _ = div().child("String".to_string()); - let _ = div().child(Cow::Borrowed("Cow")); - let _ = div().child(SharedString::from("SharedString")); - } - - #[test] - fn text_macro_id() { - // one call to `text!` = one id - fn make_text_stable_id(happy: bool) -> Text { - text!(if happy { "happy" } else { "sad" }) - } - - // two calls to `text!` = two ids - fn make_text_unstable_id(happy: bool) -> Text { - if happy { text!("happy") } else { text!("sad") } - } - - assert_eq!(make_text_stable_id(false).id, make_text_stable_id(true).id); - assert_ne!( - make_text_unstable_id(false).id, - make_text_unstable_id(true).id - ); - } -} diff --git a/crates/gpui_pre/src/elements/uniform_list.rs b/crates/gpui_pre/src/elements/uniform_list.rs deleted file mode 100644 index 308fbfe..0000000 --- a/crates/gpui_pre/src/elements/uniform_list.rs +++ /dev/null @@ -1,868 +0,0 @@ -//! A scrollable list of elements with uniform height, optimized for large lists. -//! Rather than use the full taffy layout system, uniform_list simply measures -//! the first element and then lays out all remaining elements in a line based on that -//! measurement. This is much faster than the full layout system, but only works for -//! elements with uniform height. - -use crate::{ - AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, Entity, - GlobalElementId, Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, - IsZero, LayoutId, ListSizingBehavior, Overflow, Pixels, Point, ScrollHandle, Size, - StyleRefinement, Styled, Window, point, px, size, -}; -use smallvec::SmallVec; -use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; - -use super::ListHorizontalSizingBehavior; - -/// uniform_list provides lazy rendering for a set of items that are of uniform height. -/// When rendered into a container with overflow-y: hidden and a fixed (or max) height, -/// uniform_list will only render the visible subset of items. -#[track_caller] -pub fn uniform_list( - id: impl Into, - item_count: usize, - f: impl 'static + Fn(Range, &mut Window, &mut App) -> Vec, -) -> UniformList -where - R: IntoElement, -{ - let id = id.into(); - let mut base_style = StyleRefinement::default(); - base_style.overflow.y = Some(Overflow::Scroll); - - let render_range = move |range: Range, window: &mut Window, cx: &mut App| { - f(range, window, cx) - .into_iter() - .map(|component| component.into_any_element()) - .collect() - }; - - UniformList { - item_count, - item_to_measure_index: 0, - render_items: Box::new(render_range), - decorations: Vec::new(), - interactivity: Interactivity { - element_id: Some(id), - base_style: Box::new(base_style), - ..Interactivity::new() - }, - scroll_handle: None, - sizing_behavior: ListSizingBehavior::default(), - horizontal_sizing_behavior: ListHorizontalSizingBehavior::default(), - } -} - -/// A list element for efficiently laying out and displaying a list of uniform-height elements. -pub struct UniformList { - item_count: usize, - item_to_measure_index: usize, - render_items: Box< - dyn for<'a> Fn(Range, &'a mut Window, &'a mut App) -> SmallVec<[AnyElement; 64]>, - >, - decorations: Vec>, - interactivity: Interactivity, - scroll_handle: Option, - sizing_behavior: ListSizingBehavior, - horizontal_sizing_behavior: ListHorizontalSizingBehavior, -} - -/// Frame state used by the [UniformList]. -pub struct UniformListFrameState { - items: SmallVec<[AnyElement; 32]>, - decorations: SmallVec<[AnyElement; 2]>, -} - -/// A handle for controlling the scroll position of a uniform list. -/// This should be stored in your view and passed to the uniform_list on each frame. -#[derive(Clone, Debug, Default)] -pub struct UniformListScrollHandle(pub Rc>); - -/// Where to place the element scrolled to. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ScrollStrategy { - /// Place the element at the top of the list's viewport. - Top, - /// Attempt to place the element in the middle of the list's viewport. - /// May not be possible if there's not enough list items above the item scrolled to: - /// in this case, the element will be placed at the closest possible position. - Center, - /// Attempt to place the element at the bottom of the list's viewport. - /// May not be possible if there's not enough list items above the item scrolled to: - /// in this case, the element will be placed at the closest possible position. - Bottom, - /// If the element is not visible attempt to place it at: - /// - The top of the list's viewport if the target element is above currently visible elements. - /// - The bottom of the list's viewport if the target element is above currently visible elements. - Nearest, -} - -#[derive(Clone, Copy, Debug)] -#[allow(missing_docs)] -pub struct DeferredScrollToItem { - /// The item index to scroll to - pub item_index: usize, - /// The scroll strategy to use - pub strategy: ScrollStrategy, - /// The offset in number of items - pub offset: usize, - pub scroll_strict: bool, -} - -#[derive(Clone, Debug, Default)] -#[allow(missing_docs)] -pub struct UniformListScrollState { - pub base_handle: ScrollHandle, - pub deferred_scroll_to_item: Option, - /// Size of the item, captured during last layout. - pub last_item_size: Option, - /// Whether the list was vertically flipped during last layout. - pub y_flipped: bool, -} - -#[derive(Copy, Clone, Debug, Default)] -/// The size of the item and its contents. -pub struct ItemSize { - /// The size of the item. - pub item: Size, - /// The size of the item's contents, which may be larger than the item itself, - /// if the item was bounded by a parent element. - pub contents: Size, -} - -impl UniformListScrollHandle { - /// Create a new scroll handle to bind to a uniform list. - pub fn new() -> Self { - Self(Rc::new(RefCell::new(UniformListScrollState { - base_handle: ScrollHandle::new(), - deferred_scroll_to_item: None, - last_item_size: None, - y_flipped: false, - }))) - } - - /// Scroll the list so that the given item index is visible. - /// - /// This uses non-strict scrolling: if the item is already fully visible, no scrolling occurs. - /// If the item is out of view, it scrolls the minimum amount to bring it into view according - /// to the strategy. - pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) { - self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem { - item_index: ix, - strategy, - offset: 0, - scroll_strict: false, - }); - } - - /// Scroll the list so that the given item index is at scroll strategy position. - /// - /// This uses strict scrolling: the item will always be scrolled to match the strategy position, - /// even if it's already visible. Use this when you need precise positioning. - pub fn scroll_to_item_strict(&self, ix: usize, strategy: ScrollStrategy) { - self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem { - item_index: ix, - strategy, - offset: 0, - scroll_strict: true, - }); - } - - /// Scroll the list to the given item index with an offset in number of items. - /// - /// This uses non-strict scrolling: if the item is already visible within the offset region, - /// no scrolling occurs. - /// - /// The offset parameter shrinks the effective viewport by the specified number of items - /// from the corresponding edge, then applies the scroll strategy within that reduced viewport: - /// - `ScrollStrategy::Top`: Shrinks from top, positions item at the new top - /// - `ScrollStrategy::Center`: Shrinks from top, centers item in the reduced viewport - /// - `ScrollStrategy::Bottom`: Shrinks from bottom, positions item at the new bottom - pub fn scroll_to_item_with_offset(&self, ix: usize, strategy: ScrollStrategy, offset: usize) { - self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem { - item_index: ix, - strategy, - offset, - scroll_strict: false, - }); - } - - /// Scroll the list so that the given item index is at the exact scroll strategy position with an offset. - /// - /// This uses strict scrolling: the item will always be scrolled to match the strategy position, - /// even if it's already visible. - /// - /// The offset parameter shrinks the effective viewport by the specified number of items - /// from the corresponding edge, then applies the scroll strategy within that reduced viewport: - /// - `ScrollStrategy::Top`: Shrinks from top, positions item at the new top - /// - `ScrollStrategy::Center`: Shrinks from top, centers item in the reduced viewport - /// - `ScrollStrategy::Bottom`: Shrinks from bottom, positions item at the new bottom - pub fn scroll_to_item_strict_with_offset( - &self, - ix: usize, - strategy: ScrollStrategy, - offset: usize, - ) { - self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem { - item_index: ix, - strategy, - offset, - scroll_strict: true, - }); - } - - /// Check if the list is flipped vertically. - pub fn y_flipped(&self) -> bool { - self.0.borrow().y_flipped - } - - /// Get the index of the topmost visible child. - #[cfg(any(test, feature = "test-support"))] - pub fn logical_scroll_top_index(&self) -> usize { - let this = self.0.borrow(); - this.deferred_scroll_to_item - .as_ref() - .map(|deferred| deferred.item_index) - .unwrap_or_else(|| this.base_handle.logical_scroll_top().0) - } - - /// Checks if the list can be scrolled vertically. - pub fn is_scrollable(&self) -> bool { - if let Some(size) = self.0.borrow().last_item_size { - size.contents.height > size.item.height - } else { - false - } - } - - /// Whether the list is scrolled to the end, or `None` if the list is - /// not scrollable. - pub fn is_scrolled_to_end(&self) -> Option { - let state = self.0.borrow(); - let max_offset = state.base_handle.max_offset(); - if max_offset.y <= px(0.) { - return None; - } - let offset = state.base_handle.offset(); - Some(-offset.y >= max_offset.y) - } - - /// Scroll to the bottom of the list. - pub fn scroll_to_bottom(&self) { - self.scroll_to_item(usize::MAX, ScrollStrategy::Bottom); - } -} - -impl Styled for UniformList { - fn style(&mut self) -> &mut StyleRefinement { - &mut self.interactivity.base_style - } -} - -impl Element for UniformList { - type RequestLayoutState = UniformListFrameState; - type PrepaintState = Option; - - fn id(&self) -> Option { - self.interactivity.element_id.clone() - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - global_id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - let max_items = self.item_count; - let item_size = self.measure_item(None, window, cx); - let layout_id = self.interactivity.request_layout( - global_id, - inspector_id, - window, - cx, - |style, window, cx| match self.sizing_behavior { - ListSizingBehavior::Infer => { - window.with_text_style(style.text_style().cloned(), |window| { - window.request_measured_layout( - style, - move |known_dimensions, available_space, _window, _cx| { - let desired_height = item_size.height * max_items; - let width = known_dimensions.width.unwrap_or(match available_space - .width - { - AvailableSpace::Definite(x) => x, - AvailableSpace::MinContent | AvailableSpace::MaxContent => { - item_size.width - } - }); - let height = match available_space.height { - AvailableSpace::Definite(height) => desired_height.min(height), - AvailableSpace::MinContent | AvailableSpace::MaxContent => { - desired_height - } - }; - size(width, height) - }, - ) - }) - } - ListSizingBehavior::Auto => window - .with_text_style(style.text_style().cloned(), |window| { - window.request_layout(style, None, cx) - }), - }, - ); - - ( - layout_id, - UniformListFrameState { - items: SmallVec::new(), - decorations: SmallVec::new(), - }, - ) - } - - fn prepaint( - &mut self, - global_id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - frame_state: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Option { - let style = self - .interactivity - .compute_style(global_id, None, window, cx); - let border = style.border_widths.to_pixels(window.rem_size()); - let padding = style - .padding - .to_pixels(bounds.size.into(), window.rem_size()); - - let padded_bounds = Bounds::from_corners( - bounds.origin + point(border.left + padding.left, border.top + padding.top), - bounds.bottom_right() - - point(border.right + padding.right, border.bottom + padding.bottom), - ); - - let can_scroll_horizontally = matches!( - self.horizontal_sizing_behavior, - ListHorizontalSizingBehavior::Unconstrained - ); - - let longest_item_size = self.measure_item(None, window, cx); - let content_width = if can_scroll_horizontally { - padded_bounds.size.width.max(longest_item_size.width) - } else { - padded_bounds.size.width - }; - let content_size = Size { - width: content_width, - height: longest_item_size.height * self.item_count, - }; - - let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap(); - let item_height = longest_item_size.height; - let shared_scroll_to_item = self.scroll_handle.as_mut().and_then(|handle| { - let mut handle = handle.0.borrow_mut(); - handle.last_item_size = Some(ItemSize { - item: padded_bounds.size, - contents: content_size, - }); - handle.deferred_scroll_to_item.take() - }); - - self.interactivity.prepaint( - global_id, - inspector_id, - bounds, - content_size, - window, - cx, - |_style, mut scroll_offset, hitbox, window, cx| { - let y_flipped = if let Some(scroll_handle) = &self.scroll_handle { - let scroll_state = scroll_handle.0.borrow(); - scroll_state.y_flipped - } else { - false - }; - - if self.item_count > 0 { - let content_height = item_height * self.item_count; - - let is_scrolled_vertically = !scroll_offset.y.is_zero(); - let max_scroll_offset = padded_bounds.size.height - content_height; - - if is_scrolled_vertically && scroll_offset.y < max_scroll_offset { - shared_scroll_offset.borrow_mut().y = max_scroll_offset; - scroll_offset.y = max_scroll_offset; - } - - let content_width = content_size.width + padding.left + padding.right; - let is_scrolled_horizontally = - can_scroll_horizontally && !scroll_offset.x.is_zero(); - if is_scrolled_horizontally && content_width <= padded_bounds.size.width { - shared_scroll_offset.borrow_mut().x = Pixels::ZERO; - scroll_offset.x = Pixels::ZERO; - } - - if let Some(DeferredScrollToItem { - mut item_index, - mut strategy, - offset, - scroll_strict, - }) = shared_scroll_to_item - { - if y_flipped { - item_index = self.item_count.saturating_sub(item_index + 1); - } - let list_height = padded_bounds.size.height; - let mut updated_scroll_offset = shared_scroll_offset.borrow_mut(); - let item_top = item_height * item_index; - let item_bottom = item_top + item_height; - let scroll_top = -updated_scroll_offset.y; - let offset_pixels = item_height * offset; - - // is the selected item above/below currently visible items - let is_above = item_top < scroll_top + offset_pixels; - let is_below = item_bottom > scroll_top + list_height; - - if scroll_strict || is_above || is_below { - if strategy == ScrollStrategy::Nearest { - if is_above { - strategy = ScrollStrategy::Top; - } else if is_below { - strategy = ScrollStrategy::Bottom; - } - } - - let max_scroll_offset = - (content_height - list_height).max(Pixels::ZERO); - match strategy { - ScrollStrategy::Top => { - updated_scroll_offset.y = -(item_top - offset_pixels) - .clamp(Pixels::ZERO, max_scroll_offset); - } - ScrollStrategy::Center => { - let item_center = item_top + item_height / 2.0; - - let viewport_height = list_height - offset_pixels; - let viewport_center = offset_pixels + viewport_height / 2.0; - let target_scroll_top = item_center - viewport_center; - updated_scroll_offset.y = - -target_scroll_top.clamp(Pixels::ZERO, max_scroll_offset); - } - ScrollStrategy::Bottom => { - updated_scroll_offset.y = -(item_bottom - list_height) - .clamp(Pixels::ZERO, max_scroll_offset); - } - ScrollStrategy::Nearest => { - // Nearest, but the item is visible -> no scroll is required - } - } - } - scroll_offset = *updated_scroll_offset - } - - let first_visible_element_ix = - (-(scroll_offset.y + padding.top) / item_height).floor() as usize; - let last_visible_element_ix = ((-scroll_offset.y + padded_bounds.size.height) - / item_height) - .ceil() as usize; - - let visible_range = first_visible_element_ix - ..cmp::min(last_visible_element_ix, self.item_count); - - let items = if y_flipped { - let flipped_range = self.item_count.saturating_sub(visible_range.end) - ..self.item_count.saturating_sub(visible_range.start); - let mut items = (self.render_items)(flipped_range, window, cx); - items.reverse(); - items - } else { - (self.render_items)(visible_range.clone(), window, cx) - }; - - let content_mask = ContentMask { - bounds, - ..Default::default() - }; - window.with_content_mask(Some(content_mask), |window| { - for (mut item, ix) in items.into_iter().zip(visible_range.clone()) { - let item_origin = padded_bounds.origin - + scroll_offset - + point(Pixels::ZERO, item_height * ix); - - let available_width = if can_scroll_horizontally { - padded_bounds.size.width + scroll_offset.x.abs() - } else { - padded_bounds.size.width - }; - let available_space = size( - AvailableSpace::Definite(available_width), - AvailableSpace::Definite(item_height), - ); - item.layout_as_root(available_space, window, cx); - item.prepaint_at(item_origin, window, cx); - frame_state.items.push(item); - } - - let bounds = - Bounds::new(padded_bounds.origin + scroll_offset, padded_bounds.size); - for decoration in &self.decorations { - let mut decoration = decoration.as_ref().compute( - visible_range.clone(), - bounds, - scroll_offset, - item_height, - self.item_count, - window, - cx, - ); - let available_space = size( - AvailableSpace::Definite(bounds.size.width), - AvailableSpace::Definite(bounds.size.height), - ); - decoration.layout_as_root(available_space, window, cx); - decoration.prepaint_at(bounds.origin, window, cx); - frame_state.decorations.push(decoration); - } - }); - } - - hitbox - }, - ) - } - - fn paint( - &mut self, - global_id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - request_layout: &mut Self::RequestLayoutState, - hitbox: &mut Option, - window: &mut Window, - cx: &mut App, - ) { - self.interactivity.paint( - global_id, - inspector_id, - bounds, - hitbox.as_ref(), - window, - cx, - |_, window, cx| { - for item in &mut request_layout.items { - item.paint(window, cx); - } - for decoration in &mut request_layout.decorations { - decoration.paint(window, cx); - } - }, - ) - } -} - -impl IntoElement for UniformList { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -/// A decoration for a [`UniformList`]. This can be used for various things, -/// such as rendering indent guides, or other visual effects. -pub trait UniformListDecoration { - /// Compute the decoration element, given the visible range of list items, - /// the bounds of the list, and the height of each item. - fn compute( - &self, - visible_range: Range, - bounds: Bounds, - scroll_offset: Point, - item_height: Pixels, - item_count: usize, - window: &mut Window, - cx: &mut App, - ) -> AnyElement; -} - -impl UniformListDecoration for Entity { - fn compute( - &self, - visible_range: Range, - bounds: Bounds, - scroll_offset: Point, - item_height: Pixels, - item_count: usize, - window: &mut Window, - cx: &mut App, - ) -> AnyElement { - self.update(cx, |inner, cx| { - inner.compute( - visible_range, - bounds, - scroll_offset, - item_height, - item_count, - window, - cx, - ) - }) - } -} - -impl UniformList { - /// Selects a specific list item for measurement. - pub fn with_width_from_item(mut self, item_index: Option) -> Self { - self.item_to_measure_index = item_index.unwrap_or(0); - self - } - - /// Sets the sizing behavior, similar to the `List` element. - pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self { - self.sizing_behavior = behavior; - self - } - - /// Sets the horizontal sizing behavior, controlling the way list items laid out horizontally. - /// With [`ListHorizontalSizingBehavior::Unconstrained`] behavior, every item and the list itself will - /// have the size of the widest item and lay out pushing the `end_slot` to the right end. - pub fn with_horizontal_sizing_behavior( - mut self, - behavior: ListHorizontalSizingBehavior, - ) -> Self { - self.horizontal_sizing_behavior = behavior; - match behavior { - ListHorizontalSizingBehavior::FitList => { - self.interactivity.base_style.overflow.x = None; - } - ListHorizontalSizingBehavior::Unconstrained => { - self.interactivity.base_style.overflow.x = Some(Overflow::Scroll); - } - } - self - } - - /// Adds a decoration element to the list. - pub fn with_decoration(mut self, decoration: impl UniformListDecoration + 'static) -> Self { - self.decorations.push(Box::new(decoration)); - self - } - - fn measure_item( - &self, - list_width: Option, - window: &mut Window, - cx: &mut App, - ) -> Size { - if self.item_count == 0 { - return Size::default(); - } - - let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1); - let mut items = (self.render_items)(item_ix..item_ix + 1, window, cx); - let Some(mut item_to_measure) = items.pop() else { - return Size::default(); - }; - let available_space = size( - list_width.map_or(AvailableSpace::MaxContent, |width| { - AvailableSpace::Definite(width) - }), - AvailableSpace::MinContent, - ); - item_to_measure.layout_as_root(available_space, window, cx) - } - - /// Track and render scroll state of this list with reference to the given scroll handle. - pub fn track_scroll(mut self, handle: &UniformListScrollHandle) -> Self { - self.interactivity.tracked_scroll_handle = Some(handle.0.borrow().base_handle.clone()); - self.scroll_handle = Some(handle.clone()); - self - } - - /// Sets whether the list is flipped vertically, such that item 0 appears at the bottom. - pub fn y_flipped(mut self, y_flipped: bool) -> Self { - if let Some(ref scroll_handle) = self.scroll_handle { - let mut scroll_state = scroll_handle.0.borrow_mut(); - let mut base_handle = &scroll_state.base_handle; - let offset = base_handle.offset(); - match scroll_state.last_item_size { - Some(last_size) if scroll_state.y_flipped != y_flipped => { - let new_y_offset = - -(offset.y + last_size.contents.height - last_size.item.height); - base_handle.set_offset(point(offset.x, new_y_offset)); - scroll_state.y_flipped = y_flipped; - } - // Handle case where list is initially flipped. - None if y_flipped => { - base_handle.set_offset(point(offset.x, Pixels::MIN)); - scroll_state.y_flipped = y_flipped; - } - _ => {} - } - } - self - } -} - -impl InteractiveElement for UniformList { - fn interactivity(&mut self) -> &mut crate::Interactivity { - &mut self.interactivity - } -} - -#[cfg(test)] -mod test { - use crate::TestAppContext; - - #[gpui::test] - fn test_scroll_strategy_nearest(cx: &mut TestAppContext) { - use crate::{ - Context, FocusHandle, ScrollStrategy, UniformListScrollHandle, Window, div, prelude::*, - px, uniform_list, - }; - use std::ops::Range; - - actions!(example, [SelectNext, SelectPrev]); - - struct TestView { - index: usize, - length: usize, - scroll_handle: UniformListScrollHandle, - focus_handle: FocusHandle, - visible_range: Range, - } - - impl TestView { - pub fn select_next( - &mut self, - _: &SelectNext, - window: &mut Window, - _: &mut Context, - ) { - if self.index + 1 == self.length { - self.index = 0 - } else { - self.index += 1; - } - self.scroll_handle - .scroll_to_item(self.index, ScrollStrategy::Nearest); - window.refresh(); - } - - pub fn select_previous( - &mut self, - _: &SelectPrev, - window: &mut Window, - _: &mut Context, - ) { - if self.index == 0 { - self.index = self.length - 1 - } else { - self.index -= 1; - } - self.scroll_handle - .scroll_to_item(self.index, ScrollStrategy::Nearest); - window.refresh(); - } - } - - impl Render for TestView { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .id("list-example") - .track_focus(&self.focus_handle) - .on_action(cx.listener(Self::select_next)) - .on_action(cx.listener(Self::select_previous)) - .size_full() - .child( - uniform_list( - "entries", - self.length, - cx.processor(|this, range: Range, _window, _cx| { - this.visible_range = range.clone(); - range - .map(|ix| div().id(ix).h(px(20.0)).child(format!("Item {ix}"))) - .collect() - }), - ) - .track_scroll(&self.scroll_handle) - .h(px(200.0)), - ) - } - } - - let (view, cx) = cx.add_window_view(|window, cx| { - let focus_handle = cx.focus_handle(); - window.focus(&focus_handle, cx); - TestView { - scroll_handle: UniformListScrollHandle::new(), - index: 0, - focus_handle, - length: 47, - visible_range: 0..0, - } - }); - - // 10 out of 47 items are visible - - // First 9 times selecting next item does not scroll - for ix in 1..10 { - cx.dispatch_action(SelectNext); - view.read_with(cx, |view, _| { - assert_eq!(view.index, ix); - assert_eq!(view.visible_range, 0..10); - }) - } - - // Now each time the list scrolls down by 1 - for ix in 10..47 { - cx.dispatch_action(SelectNext); - view.read_with(cx, |view, _| { - assert_eq!(view.index, ix); - assert_eq!(view.visible_range, ix - 9..ix + 1); - }) - } - - // After the last item we move back to the start - cx.dispatch_action(SelectNext); - view.read_with(cx, |view, _| { - assert_eq!(view.index, 0); - assert_eq!(view.visible_range, 0..10); - }); - - // Return to the last element - cx.dispatch_action(SelectPrev); - view.read_with(cx, |view, _| { - assert_eq!(view.index, 46); - assert_eq!(view.visible_range, 37..47); - }); - - // First 9 times selecting previous does not scroll - for ix in (37..46).rev() { - cx.dispatch_action(SelectPrev); - view.read_with(cx, |view, _| { - assert_eq!(view.index, ix); - assert_eq!(view.visible_range, 37..47); - }) - } - - // Now each time the list scrolls up by 1 - for ix in (0..37).rev() { - cx.dispatch_action(SelectPrev); - view.read_with(cx, |view, _| { - assert_eq!(view.index, ix); - assert_eq!(view.visible_range, ix..ix + 10); - }) - } - } -} diff --git a/crates/gpui_pre/src/executor.rs b/crates/gpui_pre/src/executor.rs deleted file mode 100644 index 444bb07..0000000 --- a/crates/gpui_pre/src/executor.rs +++ /dev/null @@ -1,588 +0,0 @@ -use crate::{App, PlatformDispatcher, PlatformScheduler}; -#[cfg(not(target_family = "wasm"))] -use futures::channel::mpsc; -use futures::prelude::*; -use gpui_util::{TryFutureExt, TryFutureExtBacktrace}; -use scheduler::Instant; -use scheduler::Scheduler; -use std::{future::Future, marker::PhantomData, rc::Rc, sync::Arc, time::Duration}; -#[cfg(not(target_family = "wasm"))] -use std::{mem, pin::Pin}; - -pub use scheduler::{ - DedicatedExecutor, FallibleTask, LocalExecutor as SchedulerLocalExecutor, Priority, Task, -}; - -/// A pointer to the executor that is currently running, -/// for spawning background tasks. -#[derive(Clone)] -pub struct BackgroundExecutor { - inner: scheduler::BackgroundExecutor, - dispatcher: Arc, -} - -/// A pointer to the executor that is currently running, -/// for spawning tasks on the main thread. -#[derive(Clone)] -pub struct ForegroundExecutor { - inner: scheduler::LocalExecutor, - dispatcher: Arc, - #[cfg(feature = "profiler")] - foreground_runnables: Option, - not_send: PhantomData>, -} - -/// Extension trait for `Task>` that adds `detach_and_log_err` with an `&App` context. -/// -/// This trait is automatically implemented for all `Task>` types. -pub trait TaskExt { - /// Run the task to completion in the background and log any errors that occur. - fn detach_and_log_err(self, cx: &App); - /// Like [`Self::detach_and_log_err`], but uses `{:?}` formatting on failure so `anyhow::Error` - /// values emit their full backtrace. Prefer `detach_and_log_err` unless a backtrace is wanted. - fn detach_and_log_err_with_backtrace(self, cx: &App); -} - -impl TaskExt for Task> -where - T: 'static, - E: 'static + std::fmt::Display + std::fmt::Debug, -{ - #[track_caller] - fn detach_and_log_err(self, cx: &App) { - let location = core::panic::Location::caller(); - cx.foreground_executor() - .spawn(self.log_tracked_err(*location)) - .detach(); - } - - #[track_caller] - fn detach_and_log_err_with_backtrace(self, cx: &App) { - let location = *core::panic::Location::caller(); - cx.foreground_executor() - .spawn(self.log_tracked_err_with_backtrace(location)) - .detach(); - } -} - -impl BackgroundExecutor { - /// Creates a new BackgroundExecutor from the given PlatformDispatcher. - pub fn new(dispatcher: Arc) -> Self { - #[cfg(any(test, feature = "test-support"))] - let scheduler: Arc = if let Some(test_dispatcher) = dispatcher.as_test() { - test_dispatcher.scheduler().clone() - } else { - Arc::new(PlatformScheduler::new(dispatcher.clone())) - }; - - #[cfg(not(any(test, feature = "test-support")))] - let scheduler: Arc = Arc::new(PlatformScheduler::new(dispatcher.clone())); - - Self { - inner: scheduler::BackgroundExecutor::new(scheduler), - dispatcher, - } - } - - /// Returns the underlying scheduler::BackgroundExecutor. - /// - /// This is used by Ex to pass the executor to thread/worktree code. - pub fn scheduler_executor(&self) -> scheduler::BackgroundExecutor { - self.inner.clone() - } - - /// Spawn a closure on a fresh session pinned to its own [`SchedulerLocalExecutor`]. - /// The closure runs on a new OS thread under the platform scheduler, or on - /// the test scheduler's loop in tests. - /// - /// Prefer this over [`Self::spawn`] for futures whose polls need more stack - /// than shared background threads guarantee. Dedicated threads get the - /// standard library's default 2 MiB, while `spawn` polls futures on - /// whatever threads the platform dispatcher provides — on macOS those are - /// GCD workers whose stacks are fixed at 512 KiB by the kernel (see `PTH_DEFAULT_STACKSIZE` in - /// ), - /// the tightest background-stack budget of any platform. - #[track_caller] - pub fn spawn_dedicated(&self, f: F) -> Task - where - F: FnOnce(SchedulerLocalExecutor) -> Fut + Send + 'static, - Fut: Future + 'static, - Fut::Output: Send + Sync + 'static, - { - self.inner.spawn_dedicated(f) - } - - /// Enqueues the given future to be run to completion on a background thread. - #[track_caller] - pub fn spawn(&self, future: impl Future + Send + 'static) -> Task - where - R: Send + 'static, - { - self.spawn_with_priority(Priority::default(), future.boxed()) - } - - /// Enqueues the given future to be run to completion on a background thread with the given priority. - /// - /// When `Priority::RealtimeAudio` is used, the task runs on a dedicated thread with - /// realtime scheduling priority, suitable for audio processing. - #[track_caller] - pub fn spawn_with_priority( - &self, - priority: Priority, - future: impl Future + Send + 'static, - ) -> Task - where - R: Send + 'static, - { - if priority == Priority::RealtimeAudio { - self.inner.spawn_realtime(future) - } else { - self.inner.spawn_with_priority(priority, future) - } - } - - /// Runs background tasks that may borrow from their environment and waits for all of them to complete. - /// - /// Dropping the returned future cancels its tasks and synchronously waits for their futures to - /// be destroyed before returning. - #[cfg(not(target_family = "wasm"))] - pub async fn scoped<'scope, F>(&self, scheduler: F) - where - F: FnOnce(&mut Scope<'scope>), - { - let mut scope = Scope::new(self.clone(), Priority::default()); - (scheduler)(&mut scope); - let spawned = mem::take(&mut scope.futures) - .into_iter() - .map(|f| self.spawn_with_priority(scope.priority, f)) - .collect::>(); - for task in spawned { - task.await; - } - } - - /// Runs prioritized background tasks that may borrow from their environment and waits for all - /// of them to complete. - /// - /// Dropping the returned future cancels its tasks and synchronously waits for their futures to - /// be destroyed before returning. - #[cfg(not(target_family = "wasm"))] - pub async fn scoped_priority<'scope, F>(&self, priority: Priority, scheduler: F) - where - F: FnOnce(&mut Scope<'scope>), - { - let mut scope = Scope::new(self.clone(), priority); - (scheduler)(&mut scope); - let spawned = mem::take(&mut scope.futures) - .into_iter() - .map(|f| self.spawn_with_priority(scope.priority, f)) - .collect::>(); - for task in spawned { - task.await; - } - } - - /// Get the current time. - /// - /// Calling this instead of `std::time::Instant::now` allows the use - /// of fake timers in tests. - pub fn now(&self) -> Instant { - self.inner.scheduler().clock().now() - } - - /// Returns a task that will complete after the given duration. - /// Depending on other concurrent tasks the elapsed duration may be longer - /// than requested. - #[track_caller] - pub fn timer(&self, duration: Duration) -> Task<()> { - if duration.is_zero() { - return Task::ready(()); - } - self.spawn(self.inner.scheduler().timer(duration)) - } - - /// In tests, run an arbitrary number of tasks (determined by the SEED environment variable) - #[cfg(any(test, feature = "test-support"))] - pub fn simulate_random_delay(&self) -> impl Future + use<> { - self.dispatcher.as_test().unwrap().simulate_random_delay() - } - - /// In tests, move time forward. This does not run any tasks, but does make `timer`s ready. - #[cfg(any(test, feature = "test-support"))] - pub fn advance_clock(&self, duration: Duration) { - self.dispatcher.as_test().unwrap().advance_clock(duration) - } - - /// In tests, run one task. - #[cfg(any(test, feature = "test-support"))] - pub fn tick(&self) -> bool { - self.dispatcher.as_test().unwrap().scheduler().tick() - } - - /// In tests, run tasks until the scheduler would park. - /// - /// Under the scheduler-backed test dispatcher, `tick()` will not advance the clock, so a pending - /// timer can keep `has_pending_tasks()` true even after all currently-runnable tasks have been - /// drained. To preserve the historical semantics that tests relied on (drain all work that can - /// make progress), we advance the clock to the next timer when no runnable tasks remain. - #[cfg(any(test, feature = "test-support"))] - pub fn run_until_parked(&self) { - let scheduler = self.dispatcher.as_test().unwrap().scheduler(); - scheduler.run(); - } - - /// In tests, prevents `run_until_parked` from panicking if there are outstanding tasks. - #[cfg(any(test, feature = "test-support"))] - pub fn allow_parking(&self) { - self.dispatcher - .as_test() - .unwrap() - .scheduler() - .allow_parking(); - - if std::env::var("GPUI_RUN_UNTIL_PARKED_LOG").ok().as_deref() == Some("1") { - log::warn!("[gpui::executor] allow_parking: enabled"); - } - } - - /// Sets the range of ticks to run before timing out in block_on. - #[cfg(any(test, feature = "test-support"))] - pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive) { - self.dispatcher - .as_test() - .unwrap() - .scheduler() - .set_timeout_ticks(range); - } - - /// Undoes the effect of [`Self::allow_parking`]. - #[cfg(any(test, feature = "test-support"))] - pub fn forbid_parking(&self) { - self.dispatcher - .as_test() - .unwrap() - .scheduler() - .forbid_parking(); - } - - /// In tests, returns the rng used by the dispatcher. - #[cfg(any(test, feature = "test-support"))] - pub fn rng(&self) -> scheduler::SharedRng { - self.dispatcher.as_test().unwrap().scheduler().rng() - } - - /// How many CPUs are available to the dispatcher. - pub fn num_cpus(&self) -> usize { - #[cfg(any(test, feature = "test-support"))] - if let Some(test) = self.dispatcher.as_test() { - return test.num_cpus_override().unwrap_or(4); - } - num_cpus::get() - } - - /// Override the number of CPUs reported by this executor in tests. - /// Panics if not called on a test executor. - #[cfg(any(test, feature = "test-support"))] - pub fn set_num_cpus(&self, count: usize) { - self.dispatcher - .as_test() - .expect("set_num_cpus can only be called on a test executor") - .set_num_cpus(count); - } - - /// Whether we're on the main thread. - pub fn is_main_thread(&self) -> bool { - self.dispatcher.is_main_thread() - } - - #[doc(hidden)] - pub fn dispatcher(&self) -> &Arc { - &self.dispatcher - } -} - -impl ForegroundExecutor { - /// Creates a new ForegroundExecutor from the given PlatformDispatcher. - pub fn new(dispatcher: Arc) -> Self { - #[cfg(any(test, feature = "test-support"))] - let (scheduler, session_id): (Arc, _) = - if let Some(test_dispatcher) = dispatcher.as_test() { - ( - test_dispatcher.scheduler().clone(), - test_dispatcher.session_id(), - ) - } else { - let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone())); - let inner = platform_scheduler.foreground_executor(); - return Self { - inner, - dispatcher, - #[cfg(feature = "profiler")] - foreground_runnables: Some(platform_scheduler.foreground_runnable_counter()), - not_send: PhantomData, - }; - }; - - #[cfg(not(any(test, feature = "test-support")))] - let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone())); - #[cfg(not(any(test, feature = "test-support")))] - let inner = platform_scheduler.foreground_executor(); - #[cfg(all(not(any(test, feature = "test-support")), feature = "profiler"))] - let foreground_runnables = Some(platform_scheduler.foreground_runnable_counter()); - - #[cfg(any(test, feature = "test-support"))] - let inner = { - let scheduler_for_dispatch = Arc::downgrade(&scheduler); - scheduler::LocalExecutor::new(session_id, scheduler, move |runnable| { - if let Some(scheduler) = scheduler_for_dispatch.upgrade() { - scheduler.schedule_local(session_id, runnable); - } - }) - }; - - #[cfg(all(any(test, feature = "test-support"), feature = "profiler"))] - // The deterministic test scheduler does not invoke GPUI's task profiler - // hooks, so an increment here would have no matching decrement. - let foreground_runnables = None; - - Self { - inner, - dispatcher, - #[cfg(feature = "profiler")] - foreground_runnables, - not_send: PhantomData, - } - } - - /// Enqueues the given Task to run on the main thread. - #[track_caller] - pub fn spawn(&self, future: impl Future + 'static) -> Task - where - R: 'static, - { - self.inner.spawn(future.boxed_local()) - } - - /// Enqueues the given Task to run on the main thread with the given priority. - #[track_caller] - pub fn spawn_with_priority( - &self, - _priority: Priority, - future: impl Future + 'static, - ) -> Task - where - R: 'static, - { - // Priority is ignored for foreground tasks - they run in order on the main thread - self.inner.spawn(future) - } - - /// On platforms with dedicated support, enqueues the given future to run - /// on the main thread during platform idle time. Without a `timeout`, - /// polls may be deferred indefinitely while the platform stays busy; - /// with one, a poll still waiting after that long runs as ordinary main-thread work. - /// Each poll occupies part of one idle slice, so long synchronous stretches - /// should bound themselves against [`Self::idle_time_remaining`] and yield. - /// - /// On platforms without dedicated support, schedules the given future to run - /// with a low priority, ignoring `timeout`. - #[track_caller] - pub fn spawn_when_idle( - &self, - timeout: Option, - future: impl Future + 'static, - ) -> Task - where - R: 'static, - { - let dispatcher = self.dispatcher.clone(); - #[cfg(feature = "profiler")] - let foreground_runnables = self.foreground_runnables.clone(); - self.inner - .spawn_with_dispatch(future.boxed_local(), move |runnable| { - #[cfg(feature = "profiler")] - if let Some(foreground_runnables) = &foreground_runnables { - foreground_runnables.queued(); - } - dispatcher.dispatch_on_main_thread_when_idle(runnable, timeout); - }) - } - - /// The time remaining in the current idle slice, when called from a task - /// spawned via [`Self::spawn_when_idle`] on a platform that meters idle - /// time. `None` when idle time is unmetered (or the caller is not inside - /// an idle slice); work that must bound itself should then fall back to a - /// budget of its own. - pub fn idle_time_remaining(&self) -> Option { - self.dispatcher.idle_time_remaining() - } - - /// Used by the test harness to run an async test in a synchronous fashion. - #[cfg(all(not(target_family = "wasm"), any(test, feature = "test-support")))] - #[track_caller] - pub fn block_test(&self, future: impl Future) -> R { - use std::cell::Cell; - - let scheduler = self.inner.scheduler(); - - let output = Cell::new(None); - let future = async { - output.set(Some(future.await)); - }; - let mut future = std::pin::pin!(future); - - // In async GPUI tests, we must allow foreground tasks scheduled by the test itself - // (which are associated with the test session) to make progress while we block. - // Otherwise, awaiting futures that depend on same-session foreground work can deadlock. - scheduler.block(None, future.as_mut(), None); - - output.take().expect("block_test future did not complete") - } - - /// Block the current thread until the given future resolves. - /// Consider using `block_with_timeout` instead. - #[cfg(not(target_family = "wasm"))] - pub fn block_on(&self, future: impl Future) -> R { - self.inner.block_on(future) - } - - /// Block the current thread until the given future resolves or the timeout elapses. - #[cfg(not(target_family = "wasm"))] - pub fn block_with_timeout>( - &self, - duration: Duration, - future: Fut, - ) -> Result + use> { - self.inner.block_with_timeout(duration, future) - } - - #[doc(hidden)] - pub fn dispatcher(&self) -> &Arc { - &self.dispatcher - } - - #[doc(hidden)] - pub fn scheduler_executor(&self) -> SchedulerLocalExecutor { - self.inner.clone() - } -} - -/// Scope manages a set of tasks that are enqueued and waited on together. See [`BackgroundExecutor::scoped`]. -#[cfg(not(target_family = "wasm"))] -pub struct Scope<'a> { - executor: BackgroundExecutor, - priority: Priority, - futures: Vec + Send + 'static>>>, - tx: Option>, - rx: mpsc::Receiver<()>, - lifetime: PhantomData<&'a ()>, -} - -#[cfg(not(target_family = "wasm"))] -impl<'a> Scope<'a> { - fn new(executor: BackgroundExecutor, priority: Priority) -> Self { - let (tx, rx) = mpsc::channel(1); - Self { - executor, - priority, - tx: Some(tx), - rx, - futures: Default::default(), - lifetime: PhantomData, - } - } - - /// How many CPUs are available to the dispatcher. - pub fn num_cpus(&self) -> usize { - self.executor.num_cpus() - } - - /// Spawn a future into this scope. - #[track_caller] - pub fn spawn(&mut self, f: F) - where - F: Future + Send + 'a, - { - let tx = self.tx.clone().unwrap(); - - // SAFETY: The 'a lifetime is guaranteed to outlive any of these futures because - // dropping this `Scope` blocks until all of the futures have resolved. - let f = unsafe { - mem::transmute::< - Pin + Send + 'a>>, - Pin + Send + 'static>>, - >(Box::pin(async move { - f.await; - drop(tx); - })) - }; - self.futures.push(f); - } -} - -#[cfg(not(target_family = "wasm"))] -impl Drop for Scope<'_> { - fn drop(&mut self) { - self.tx.take().unwrap(); - - // Wait until the channel is closed, which means that all of the spawned - // futures have resolved. - let future = async { - self.rx.next().await; - }; - let mut future = std::pin::pin!(future); - self.executor - .inner - .scheduler() - .block(None, future.as_mut(), None); - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::{App, TestDispatcher, TestPlatform}; - use std::cell::RefCell; - - /// Helper to create test infrastructure. - /// Returns (dispatcher, background_executor, app). - fn create_test_app() -> (TestDispatcher, BackgroundExecutor, Rc) { - let dispatcher = TestDispatcher::new(0); - let arc_dispatcher = Arc::new(dispatcher.clone()); - let background_executor = BackgroundExecutor::new(arc_dispatcher.clone()); - let foreground_executor = ForegroundExecutor::new(arc_dispatcher); - - let platform = TestPlatform::new(background_executor.clone(), foreground_executor); - let asset_source = Arc::new(()); - let http_client = http_client::FakeHttpClient::with_404_response(); - - let app = App::new_app(platform, asset_source, http_client); - (dispatcher, background_executor, app) - } - - #[test] - fn sanity_test_tasks_run() { - let (dispatcher, _background_executor, app) = create_test_app(); - let foreground_executor = app.borrow().foreground_executor.clone(); - - let task_ran = Rc::new(RefCell::new(false)); - - foreground_executor - .spawn({ - let task_ran = Rc::clone(&task_ran); - async move { - *task_ran.borrow_mut() = true; - } - }) - .detach(); - - // Run dispatcher while app is still alive - dispatcher.run_until_parked(); - - // Task should have run - assert!( - *task_ran.borrow(), - "Task should run normally when app is alive" - ); - } -} diff --git a/crates/gpui_pre/src/geometry.rs b/crates/gpui_pre/src/geometry.rs deleted file mode 100644 index 05dafbf..0000000 --- a/crates/gpui_pre/src/geometry.rs +++ /dev/null @@ -1,4008 +0,0 @@ -//! The GPUI geometry module is a collection of types and traits that -//! can be used to describe common units, concepts, and the relationships -//! between them. - -use anyhow::{Context as _, anyhow}; -use core::fmt::Debug; -use derive_more::{Add, AddAssign, Div, DivAssign, Mul, Neg, Sub, SubAssign}; -use refineable::Refineable; -use schemars::{JsonSchema, json_schema}; -use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; -use std::borrow::Cow; -use std::ops::{AddAssign, Range}; -use std::{ - cmp::{self, PartialOrd}, - fmt::{self, Display}, - hash::Hash, - ops::{Add, Div, Mul, MulAssign, Neg, Sub}, -}; -use taffy::prelude::{TaffyGridLine, TaffyGridSpan}; - -use crate::{App, DisplayId}; - -/// Axis in a 2D cartesian space. -#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] -pub enum Axis { - /// The y axis, or up and down - Vertical, - /// The x axis, or left and right - Horizontal, -} - -impl Axis { - /// Swap this axis to the opposite axis. - pub fn invert(self) -> Self { - match self { - Axis::Vertical => Axis::Horizontal, - Axis::Horizontal => Axis::Vertical, - } - } -} - -/// A trait for accessing the given unit along a certain axis. -pub trait Along { - /// The unit associated with this type - type Unit; - - /// Returns the unit along the given axis. - fn along(&self, axis: Axis) -> Self::Unit; - - /// Applies the given function to the unit along the given axis and returns a new value. - fn apply_along(&self, axis: Axis, f: impl FnOnce(Self::Unit) -> Self::Unit) -> Self; -} - -/// Describes a location in a 2D cartesian space. -/// -/// It holds two public fields, `x` and `y`, which represent the coordinates in the space. -/// The type `T` for the coordinates can be any type that implements `Default`, `Clone`, and `Debug`. -/// -/// # Examples -/// -/// ``` -/// # use gpui::Point; -/// let point = Point { x: 10, y: 20 }; -/// println!("{:?}", point); // Outputs: Point { x: 10, y: 20 } -/// ``` -#[derive( - Refineable, - Default, - Add, - AddAssign, - Sub, - SubAssign, - Copy, - Debug, - PartialEq, - Eq, - Serialize, - Deserialize, - JsonSchema, - Hash, - Neg, -)] -#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -#[repr(C)] -pub struct Point { - /// The x coordinate of the point. - pub x: T, - /// The y coordinate of the point. - pub y: T, -} - -/// Constructs a new `Point` with the given x and y coordinates. -/// -/// # Arguments -/// -/// * `x` - The x coordinate of the point. -/// * `y` - The y coordinate of the point. -/// -/// # Returns -/// -/// Returns a `Point` with the specified coordinates. -/// -/// # Examples -/// -/// ``` -/// use gpui::point; -/// let p = point(10, 20); -/// assert_eq!(p.x, 10); -/// assert_eq!(p.y, 20); -/// ``` -pub const fn point(x: T, y: T) -> Point { - Point { x, y } -} - -impl Point { - /// Creates a new `Point` with the specified `x` and `y` coordinates. - /// - /// # Arguments - /// - /// * `x` - The horizontal coordinate of the point. - /// * `y` - The vertical coordinate of the point. - /// - /// # Examples - /// - /// ``` - /// use gpui::Point; - /// let p = Point::new(10, 20); - /// assert_eq!(p.x, 10); - /// assert_eq!(p.y, 20); - /// ``` - pub const fn new(x: T, y: T) -> Self { - Self { x, y } - } - - /// Transforms the point to a `Point` by applying the given function to both coordinates. - /// - /// This method allows for converting a `Point` to a `Point` by specifying a closure - /// that defines how to convert between the two types. The closure is applied to both the `x` - /// and `y` coordinates, resulting in a new point of the desired type. - /// - /// # Arguments - /// - /// * `f` - A closure that takes a value of type `T` and returns a value of type `U`. - /// - /// # Examples - /// - /// ``` - /// # use gpui::Point; - /// let p = Point { x: 3, y: 4 }; - /// let p_float = p.map(|coord| coord as f32); - /// assert_eq!(p_float, Point { x: 3.0, y: 4.0 }); - /// ``` - #[must_use] - pub fn map(&self, f: impl Fn(T) -> U) -> Point { - Point { - x: f(self.x.clone()), - y: f(self.y.clone()), - } - } -} - -impl Along for Point { - type Unit = T; - - fn along(&self, axis: Axis) -> T { - match axis { - Axis::Horizontal => self.x.clone(), - Axis::Vertical => self.y.clone(), - } - } - - fn apply_along(&self, axis: Axis, f: impl FnOnce(T) -> T) -> Point { - match axis { - Axis::Horizontal => Point { - x: f(self.x.clone()), - y: self.y.clone(), - }, - Axis::Vertical => Point { - x: self.x.clone(), - y: f(self.y.clone()), - }, - } - } -} - -impl Point { - /// Scales the point by a given factor, which is typically derived from the resolution - /// of a target display to ensure proper sizing of UI elements. - /// - /// # Arguments - /// - /// * `factor` - The scaling factor to apply to both the x and y coordinates. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Point, Pixels, ScaledPixels}; - /// let p = Point { x: Pixels::from(10.0), y: Pixels::from(20.0) }; - /// let scaled_p = p.scale(1.5); - /// assert_eq!(scaled_p, Point { x: ScaledPixels::from(15.0), y: ScaledPixels::from(30.0) }); - /// ``` - pub fn scale(&self, factor: f32) -> Point { - Point { - x: self.x.scale(factor), - y: self.y.scale(factor), - } - } - - /// Calculates the Euclidean distance from the origin (0, 0) to this point. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Pixels, Point}; - /// let p = Point { x: Pixels::from(3.0), y: Pixels::from(4.0) }; - /// assert_eq!(p.magnitude(), 5.0); - /// ``` - pub fn magnitude(&self) -> f64 { - ((self.x.0.powi(2) + self.y.0.powi(2)) as f64).sqrt() - } -} - -impl Point -where - T: Sub + Clone + Debug + Default + PartialEq, -{ - /// Get the position of this point, relative to the given origin - pub fn relative_to(&self, origin: &Point) -> Point { - point( - self.x.clone() - origin.x.clone(), - self.y.clone() - origin.y.clone(), - ) - } -} - -impl Mul for Point -where - T: Mul + Clone + Debug + Default + PartialEq, - Rhs: Clone + Debug, -{ - type Output = Point; - - fn mul(self, rhs: Rhs) -> Self::Output { - Point { - x: self.x * rhs.clone(), - y: self.y * rhs, - } - } -} - -impl MulAssign for Point -where - T: Mul + Clone + Debug + Default + PartialEq, - S: Clone, -{ - fn mul_assign(&mut self, rhs: S) { - self.x = self.x.clone() * rhs.clone(); - self.y = self.y.clone() * rhs; - } -} - -impl Div for Point -where - T: Div + Clone + Debug + Default + PartialEq, - S: Clone, -{ - type Output = Self; - - fn div(self, rhs: S) -> Self::Output { - Self { - x: self.x / rhs.clone(), - y: self.y / rhs, - } - } -} - -impl Point -where - T: PartialOrd + Clone + Debug + Default + PartialEq, -{ - /// Returns a new point with the maximum values of each dimension from `self` and `other`. - /// - /// # Arguments - /// - /// * `other` - A reference to another `Point` to compare with `self`. - /// - /// # Examples - /// - /// ``` - /// # use gpui::Point; - /// let p1 = Point { x: 3, y: 7 }; - /// let p2 = Point { x: 5, y: 2 }; - /// let max_point = p1.max(&p2); - /// assert_eq!(max_point, Point { x: 5, y: 7 }); - /// ``` - pub fn max(&self, other: &Self) -> Self { - Point { - x: if self.x > other.x { - self.x.clone() - } else { - other.x.clone() - }, - y: if self.y > other.y { - self.y.clone() - } else { - other.y.clone() - }, - } - } - - /// Returns a new point with the minimum values of each dimension from `self` and `other`. - /// - /// # Arguments - /// - /// * `other` - A reference to another `Point` to compare with `self`. - /// - /// # Examples - /// - /// ``` - /// # use gpui::Point; - /// let p1 = Point { x: 3, y: 7 }; - /// let p2 = Point { x: 5, y: 2 }; - /// let min_point = p1.min(&p2); - /// assert_eq!(min_point, Point { x: 3, y: 2 }); - /// ``` - pub fn min(&self, other: &Self) -> Self { - Point { - x: if self.x <= other.x { - self.x.clone() - } else { - other.x.clone() - }, - y: if self.y <= other.y { - self.y.clone() - } else { - other.y.clone() - }, - } - } - - /// Clamps the point to a specified range. - /// - /// Given a minimum point and a maximum point, this method constrains the current point - /// such that its coordinates do not exceed the range defined by the minimum and maximum points. - /// If the current point's coordinates are less than the minimum, they are set to the minimum. - /// If they are greater than the maximum, they are set to the maximum. - /// - /// # Arguments - /// - /// * `min` - A reference to a `Point` representing the minimum allowable coordinates. - /// * `max` - A reference to a `Point` representing the maximum allowable coordinates. - /// - /// # Examples - /// - /// ``` - /// # use gpui::Point; - /// let p = Point { x: 10, y: 20 }; - /// let min = Point { x: 0, y: 5 }; - /// let max = Point { x: 15, y: 25 }; - /// let clamped_p = p.clamp(&min, &max); - /// assert_eq!(clamped_p, Point { x: 10, y: 20 }); - /// - /// let p_out_of_bounds = Point { x: -5, y: 30 }; - /// let clamped_p_out_of_bounds = p_out_of_bounds.clamp(&min, &max); - /// assert_eq!(clamped_p_out_of_bounds, Point { x: 0, y: 25 }); - /// ``` - pub fn clamp(&self, min: &Self, max: &Self) -> Self { - self.max(min).min(max) - } -} - -impl Clone for Point { - fn clone(&self) -> Self { - Self { - x: self.x.clone(), - y: self.y.clone(), - } - } -} - -impl Display for Point { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "({}, {})", self.x, self.y) - } -} - -/// A structure representing a two-dimensional size with width and height in a given unit. -/// -/// This struct is generic over the type `T`, which can be any type that implements `Clone`, `Default`, and `Debug`. -/// It is commonly used to specify dimensions for elements in a UI, such as a window or element. -#[derive( - Add, Clone, Copy, Default, Deserialize, Div, Hash, Neg, PartialEq, Refineable, Serialize, Sub, -)] -#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -#[repr(C)] -pub struct Size { - /// The width component of the size. - pub width: T, - /// The height component of the size. - pub height: T, -} - -impl Size { - /// Create a new Size, a synonym for [`size`] - pub fn new(width: T, height: T) -> Self { - size(width, height) - } -} - -/// Constructs a new `Size` with the provided width and height. -/// -/// # Arguments -/// -/// * `width` - The width component of the `Size`. -/// * `height` - The height component of the `Size`. -/// -/// # Examples -/// -/// ``` -/// use gpui::size; -/// let my_size = size(10, 20); -/// assert_eq!(my_size.width, 10); -/// assert_eq!(my_size.height, 20); -/// ``` -pub const fn size(width: T, height: T) -> Size -where - T: Clone + Debug + Default + PartialEq, -{ - Size { width, height } -} - -impl Size -where - T: Clone + Debug + Default + PartialEq, -{ - /// Applies a function to the width and height of the size, producing a new `Size`. - /// - /// This method allows for converting a `Size` to a `Size` by specifying a closure - /// that defines how to convert between the two types. The closure is applied to both the `width` - /// and `height`, resulting in a new size of the desired type. - /// - /// # Arguments - /// - /// * `f` - A closure that takes a value of type `T` and returns a value of type `U`. - /// - /// # Examples - /// - /// ``` - /// # use gpui::Size; - /// let my_size = Size { width: 10, height: 20 }; - /// let my_new_size = my_size.map(|dimension| dimension as f32 * 1.5); - /// assert_eq!(my_new_size, Size { width: 15.0, height: 30.0 }); - /// ``` - pub fn map(&self, f: impl Fn(T) -> U) -> Size - where - U: Clone + Debug + Default + PartialEq, - { - Size { - width: f(self.width.clone()), - height: f(self.height.clone()), - } - } -} - -impl Size -where - T: Clone + Debug + Default + PartialEq + Half, -{ - /// Compute the center point of the size.g - pub fn center(&self) -> Point { - Point { - x: self.width.half(), - y: self.height.half(), - } - } -} - -impl Size { - /// Scales the size by a given factor. - /// - /// This method multiplies both the width and height by the provided scaling factor, - /// resulting in a new `Size` that is proportionally larger or smaller - /// depending on the factor. - /// - /// # Arguments - /// - /// * `factor` - The scaling factor to apply to the width and height. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Size, Pixels, ScaledPixels}; - /// let size = Size { width: Pixels::from(100.0), height: Pixels::from(50.0) }; - /// let scaled_size = size.scale(2.0); - /// assert_eq!(scaled_size, Size { width: ScaledPixels::from(200.0), height: ScaledPixels::from(100.0) }); - /// ``` - pub fn scale(&self, factor: f32) -> Size { - Size { - width: self.width.scale(factor), - height: self.height.scale(factor), - } - } -} - -impl Along for Size -where - T: Clone + Debug + Default + PartialEq, -{ - type Unit = T; - - fn along(&self, axis: Axis) -> T { - match axis { - Axis::Horizontal => self.width.clone(), - Axis::Vertical => self.height.clone(), - } - } - - /// Returns the value of this size along the given axis. - fn apply_along(&self, axis: Axis, f: impl FnOnce(T) -> T) -> Self { - match axis { - Axis::Horizontal => Size { - width: f(self.width.clone()), - height: self.height.clone(), - }, - Axis::Vertical => Size { - width: self.width.clone(), - height: f(self.height.clone()), - }, - } - } -} - -impl Size -where - T: PartialOrd + Clone + Debug + Default + PartialEq, -{ - /// Returns a new `Size` with the maximum width and height from `self` and `other`. - /// - /// # Arguments - /// - /// * `other` - A reference to another `Size` to compare with `self`. - /// - /// # Examples - /// - /// ``` - /// # use gpui::Size; - /// let size1 = Size { width: 30, height: 40 }; - /// let size2 = Size { width: 50, height: 20 }; - /// let max_size = size1.max(&size2); - /// assert_eq!(max_size, Size { width: 50, height: 40 }); - /// ``` - pub fn max(&self, other: &Self) -> Self { - Size { - width: if self.width >= other.width { - self.width.clone() - } else { - other.width.clone() - }, - height: if self.height >= other.height { - self.height.clone() - } else { - other.height.clone() - }, - } - } - - /// Returns a new `Size` with the minimum width and height from `self` and `other`. - /// - /// # Arguments - /// - /// * `other` - A reference to another `Size` to compare with `self`. - /// - /// # Examples - /// - /// ``` - /// # use gpui::Size; - /// let size1 = Size { width: 30, height: 40 }; - /// let size2 = Size { width: 50, height: 20 }; - /// let min_size = size1.min(&size2); - /// assert_eq!(min_size, Size { width: 30, height: 20 }); - /// ``` - pub fn min(&self, other: &Self) -> Self { - Size { - width: if self.width >= other.width { - other.width.clone() - } else { - self.width.clone() - }, - height: if self.height >= other.height { - other.height.clone() - } else { - self.height.clone() - }, - } - } -} - -impl Mul for Size -where - T: Mul + Clone + Debug + Default + PartialEq, - Rhs: Clone + Debug + Default + PartialEq, -{ - type Output = Size; - - fn mul(self, rhs: Rhs) -> Self::Output { - Size { - width: self.width * rhs.clone(), - height: self.height * rhs, - } - } -} - -impl MulAssign for Size -where - T: Mul + Clone + Debug + Default + PartialEq, - S: Clone, -{ - fn mul_assign(&mut self, rhs: S) { - self.width = self.width.clone() * rhs.clone(); - self.height = self.height.clone() * rhs; - } -} - -impl Eq for Size where T: Eq + Clone + Debug + Default + PartialEq {} - -impl Debug for Size -where - T: Clone + Debug + Default + PartialEq, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Size {{ {:?} × {:?} }}", self.width, self.height) - } -} - -impl Display for Size { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} × {}", self.width, self.height) - } -} - -impl From> for Size { - fn from(point: Point) -> Self { - Self { - width: point.x, - height: point.y, - } - } -} - -impl From> for Size { - fn from(size: Size) -> Self { - Size { - width: size.width.into(), - height: size.height.into(), - } - } -} - -impl From> for Size { - fn from(size: Size) -> Self { - Size { - width: size.width.into(), - height: size.height.into(), - } - } -} - -impl Size { - /// Returns a `Size` with both width and height set to fill the available space. - /// - /// This function creates a `Size` instance where both the width and height are set to `Length::Definite(DefiniteLength::Fraction(1.0))`, - /// which represents 100% of the available space in both dimensions. - /// - /// # Returns - /// - /// A `Size` that will fill the available space when used in a layout. - pub fn full() -> Self { - Self { - width: relative(1.).into(), - height: relative(1.).into(), - } - } -} - -impl Size { - /// Returns a `Size` with both width and height set to `auto`, which allows the layout engine to determine the size. - /// - /// This function creates a `Size` instance where both the width and height are set to `Length::Auto`, - /// indicating that their size should be computed based on the layout context, such as the content size or - /// available space. - /// - /// # Returns - /// - /// A `Size` with width and height set to `Length::Auto`. - pub fn auto() -> Self { - Self { - width: Length::Auto, - height: Length::Auto, - } - } -} - -/// Represents a rectangular area in a 2D space with an origin point and a size. -/// -/// The `Bounds` struct is generic over a type `T` which represents the type of the coordinate system. -/// The origin is represented as a `Point` which defines the top left corner of the rectangle, -/// and the size is represented as a `Size` which defines the width and height of the rectangle. -/// -/// # Examples -/// -/// ``` -/// # use gpui::{Bounds, Point, Size}; -/// let origin = Point { x: 0, y: 0 }; -/// let size = Size { width: 10, height: 20 }; -/// let bounds = Bounds::new(origin, size); -/// -/// assert_eq!(bounds.origin, origin); -/// assert_eq!(bounds.size, size); -/// ``` -#[derive(Refineable, Copy, Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)] -#[refineable(Debug)] -#[repr(C)] -pub struct Bounds { - /// The origin point of this area. - pub origin: Point, - /// The size of the rectangle. - pub size: Size, -} - -/// Create a bounds with the given origin and size -pub fn bounds( - origin: Point, - size: Size, -) -> Bounds { - Bounds { origin, size } -} - -impl Bounds { - /// Generate a centered bounds for the given display or primary display if none is provided - pub fn centered(display_id: Option, size: Size, cx: &App) -> Self { - let display = display_id - .and_then(|id| cx.find_display(id)) - .or_else(|| cx.primary_display()); - - display - .map(|display| Bounds::centered_at(display.bounds().center(), size)) - .unwrap_or_else(|| Bounds { - origin: point(px(0.), px(0.)), - size, - }) - } - - /// Generate maximized bounds for the given display or primary display if none is provided - pub fn maximized(display_id: Option, cx: &App) -> Self { - let display = display_id - .and_then(|id| cx.find_display(id)) - .or_else(|| cx.primary_display()); - - display - .map(|display| display.bounds()) - .unwrap_or_else(|| Bounds { - origin: point(px(0.), px(0.)), - size: size(px(1024.), px(768.)), - }) - } -} - -impl Bounds -where - T: Clone + Debug + Default + PartialEq, -{ - /// Creates a new `Bounds` with the specified origin and size. - /// - /// # Arguments - /// - /// * `origin` - A `Point` representing the origin of the bounds. - /// * `size` - A `Size` representing the size of the bounds. - /// - /// # Returns - /// - /// Returns a `Bounds` that has the given origin and size. - pub fn new(origin: Point, size: Size) -> Self { - Bounds { origin, size } - } -} - -impl Bounds -where - T: Sub + Clone + Debug + Default + PartialEq, -{ - /// Constructs a `Bounds` from two corner points: the top left and bottom right corners. - /// - /// This function calculates the origin and size of the `Bounds` based on the provided corner points. - /// The origin is set to the top left corner, and the size is determined by the difference between - /// the x and y coordinates of the bottom right and top left points. - /// - /// # Arguments - /// - /// * `top_left` - A `Point` representing the top left corner of the rectangle. - /// * `bottom_right` - A `Point` representing the bottom right corner of the rectangle. - /// - /// # Returns - /// - /// Returns a `Bounds` that encompasses the area defined by the two corner points. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point}; - /// let top_left = Point { x: 0, y: 0 }; - /// let bottom_right = Point { x: 10, y: 10 }; - /// let bounds = Bounds::from_corners(top_left, bottom_right); - /// - /// assert_eq!(bounds.origin, top_left); - /// assert_eq!(bounds.size.width, 10); - /// assert_eq!(bounds.size.height, 10); - /// ``` - pub fn from_corners(top_left: Point, bottom_right: Point) -> Self { - let origin = Point { - x: top_left.x.clone(), - y: top_left.y.clone(), - }; - let size = Size { - width: bottom_right.x - top_left.x, - height: bottom_right.y - top_left.y, - }; - Bounds { origin, size } - } -} - -impl Bounds -where - T: Sub + Half + Clone + Debug + Default + PartialEq, -{ - /// Constructs a `Bounds` from a corner point and size. The specified corner will be placed at - /// the specified origin. - pub fn from_anchor_and_size(corner: Anchor, origin: Point, size: Size) -> Bounds { - let origin = match corner { - Anchor::TopLeft => origin, - Anchor::TopRight => Point { - x: origin.x - size.width.clone(), - y: origin.y, - }, - Anchor::BottomLeft => Point { - x: origin.x, - y: origin.y - size.height.clone(), - }, - Anchor::BottomRight => Point { - x: origin.x - size.width.clone(), - y: origin.y - size.height.clone(), - }, - Anchor::TopCenter => Point { - x: origin.x - size.width.half(), - y: origin.y, - }, - Anchor::BottomCenter => Point { - x: origin.x - size.width.half(), - y: origin.y - size.height.clone(), - }, - Anchor::LeftCenter => Point { - x: origin.x, - y: origin.y - size.height.half(), - }, - Anchor::RightCenter => Point { - x: origin.x - size.width.clone(), - y: origin.y - size.height.half(), - }, - }; - - Bounds { origin, size } - } -} - -impl Bounds -where - T: Sub + Half + Clone + Debug + Default + PartialEq, -{ - /// Creates a new bounds centered at the given point. - pub fn centered_at(center: Point, size: Size) -> Self { - let origin = Point { - x: center.x - size.width.half(), - y: center.y - size.height.half(), - }; - Self::new(origin, size) - } -} - -impl Bounds -where - T: Add + Half + Clone + Debug + Default + PartialEq, -{ - /// Returns the top center point of the bounds. - pub fn top_center(&self) -> Point { - Point { - x: self.origin.x.clone() + self.size.width.half(), - y: self.origin.y.clone(), - } - } - - /// Returns the bottom center point of the bounds. - pub fn bottom_center(&self) -> Point { - Point { - x: self.origin.x.clone() + self.size.width.half(), - y: self.origin.y.clone() + self.size.height.clone(), - } - } - - /// Returns the left center point of the bounds. - pub fn left_center(&self) -> Point { - Point { - x: self.origin.x.clone(), - y: self.origin.y.clone() + self.size.height.half(), - } - } - - /// Returns the right center point of the bounds. - pub fn right_center(&self) -> Point { - Point { - x: self.origin.x.clone() + self.size.width.clone(), - y: self.origin.y.clone() + self.size.height.half(), - } - } -} - -impl Bounds -where - T: PartialOrd + Add + Clone + Debug + Default + PartialEq, -{ - /// Checks if this `Bounds` intersects with another `Bounds`. - /// - /// Two `Bounds` instances intersect if they overlap in the 2D space they occupy. - /// This method checks if there is any overlapping area between the two bounds. - /// - /// # Arguments - /// - /// * `other` - A reference to another `Bounds` to check for intersection with. - /// - /// # Returns - /// - /// Returns `true` if there is any intersection between the two bounds, `false` otherwise. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let bounds1 = Bounds { - /// origin: Point { x: 0, y: 0 }, - /// size: Size { width: 10, height: 10 }, - /// }; - /// let bounds2 = Bounds { - /// origin: Point { x: 5, y: 5 }, - /// size: Size { width: 10, height: 10 }, - /// }; - /// let bounds3 = Bounds { - /// origin: Point { x: 20, y: 20 }, - /// size: Size { width: 10, height: 10 }, - /// }; - /// - /// assert_eq!(bounds1.intersects(&bounds2), true); // Overlapping bounds - /// assert_eq!(bounds1.intersects(&bounds3), false); // Non-overlapping bounds - /// ``` - pub fn intersects(&self, other: &Bounds) -> bool { - let my_lower_right = self.bottom_right(); - let their_lower_right = other.bottom_right(); - - self.origin.x < their_lower_right.x - && my_lower_right.x > other.origin.x - && self.origin.y < their_lower_right.y - && my_lower_right.y > other.origin.y - } -} - -impl Bounds -where - T: Add + Half + Clone + Debug + Default + PartialEq, -{ - /// Returns the center point of the bounds. - /// - /// Calculates the center by taking the origin's x and y coordinates and adding half the width and height - /// of the bounds, respectively. The center is represented as a `Point` where `T` is the type of the - /// coordinate system. - /// - /// # Returns - /// - /// A `Point` representing the center of the bounds. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let bounds = Bounds { - /// origin: Point { x: 0, y: 0 }, - /// size: Size { width: 10, height: 20 }, - /// }; - /// let center = bounds.center(); - /// assert_eq!(center, Point { x: 5, y: 10 }); - /// ``` - pub fn center(&self) -> Point { - Point { - x: self.origin.x.clone() + self.size.width.clone().half(), - y: self.origin.y.clone() + self.size.height.clone().half(), - } - } -} - -impl Bounds -where - T: Add + Clone + Debug + Default + PartialEq, -{ - /// Calculates the half perimeter of a rectangle defined by the bounds. - /// - /// The half perimeter is calculated as the sum of the width and the height of the rectangle. - /// This method is generic over the type `T` which must implement the `Sub` trait to allow - /// calculation of the width and height from the bounds' origin and size, as well as the `Add` trait - /// to sum the width and height for the half perimeter. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let bounds = Bounds { - /// origin: Point { x: 0, y: 0 }, - /// size: Size { width: 10, height: 20 }, - /// }; - /// let half_perimeter = bounds.half_perimeter(); - /// assert_eq!(half_perimeter, 30); - /// ``` - pub fn half_perimeter(&self) -> T { - self.size.width.clone() + self.size.height.clone() - } -} - -impl Bounds -where - T: Add + Sub + Clone + Debug + Default + PartialEq, -{ - /// Dilates the bounds by a specified amount in all directions. - /// - /// This method expands the bounds by the given `amount`, increasing the size - /// and adjusting the origin so that the bounds grow outwards equally in all directions. - /// The resulting bounds will have its width and height increased by twice the `amount` - /// (since it grows in both directions), and the origin will be moved by `-amount` - /// in both the x and y directions. - /// - /// # Arguments - /// - /// * `amount` - The amount by which to dilate the bounds. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let mut bounds = Bounds { - /// origin: Point { x: 10, y: 10 }, - /// size: Size { width: 10, height: 10 }, - /// }; - /// let expanded_bounds = bounds.dilate(5); - /// assert_eq!(expanded_bounds, Bounds { - /// origin: Point { x: 5, y: 5 }, - /// size: Size { width: 20, height: 20 }, - /// }); - /// ``` - #[must_use] - pub fn dilate(&self, amount: T) -> Bounds { - let double_amount = amount.clone() + amount.clone(); - Bounds { - origin: self.origin.clone() - point(amount.clone(), amount), - size: self.size.clone() + size(double_amount.clone(), double_amount), - } - } - - /// Extends the bounds different amounts in each direction. - #[must_use] - pub fn extend(&self, amount: Edges) -> Bounds { - Bounds { - origin: self.origin.clone() - point(amount.left.clone(), amount.top.clone()), - size: self.size.clone() - + size( - amount.left.clone() + amount.right.clone(), - amount.top.clone() + amount.bottom, - ), - } - } -} - -impl Bounds -where - T: Add - + Sub - + Neg - + Clone - + Debug - + Default - + PartialEq, -{ - /// Inset the bounds by a specified amount. Equivalent to `dilate` with the amount negated. - /// - /// Note that this may panic if T does not support negative values. - pub fn inset(&self, amount: T) -> Self { - self.dilate(-amount) - } -} - -impl + Sub + Clone + Debug + Default + PartialEq> - Bounds -{ - /// Calculates the intersection of two `Bounds` objects. - /// - /// This method computes the overlapping region of two `Bounds`. If the bounds do not intersect, - /// the resulting `Bounds` will have a size with width and height of zero. - /// - /// # Arguments - /// - /// * `other` - A reference to another `Bounds` to intersect with. - /// - /// # Returns - /// - /// Returns a `Bounds` representing the intersection area. If there is no intersection, - /// the returned `Bounds` will have a size with width and height of zero. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let bounds1 = Bounds { - /// origin: Point { x: 0, y: 0 }, - /// size: Size { width: 10, height: 10 }, - /// }; - /// let bounds2 = Bounds { - /// origin: Point { x: 5, y: 5 }, - /// size: Size { width: 10, height: 10 }, - /// }; - /// let intersection = bounds1.intersect(&bounds2); - /// - /// assert_eq!(intersection, Bounds { - /// origin: Point { x: 5, y: 5 }, - /// size: Size { width: 5, height: 5 }, - /// }); - /// ``` - pub fn intersect(&self, other: &Self) -> Self { - let upper_left = self.origin.max(&other.origin); - let bottom_right = self - .bottom_right() - .min(&other.bottom_right()) - .max(&upper_left); - Self::from_corners(upper_left, bottom_right) - } - - /// Computes the union of two `Bounds`. - /// - /// This method calculates the smallest `Bounds` that contains both the current `Bounds` and the `other` `Bounds`. - /// The resulting `Bounds` will have an origin that is the minimum of the origins of the two `Bounds`, - /// and a size that encompasses the furthest extents of both `Bounds`. - /// - /// # Arguments - /// - /// * `other` - A reference to another `Bounds` to create a union with. - /// - /// # Returns - /// - /// Returns a `Bounds` representing the union of the two `Bounds`. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let bounds1 = Bounds { - /// origin: Point { x: 0, y: 0 }, - /// size: Size { width: 10, height: 10 }, - /// }; - /// let bounds2 = Bounds { - /// origin: Point { x: 5, y: 5 }, - /// size: Size { width: 15, height: 15 }, - /// }; - /// let union_bounds = bounds1.union(&bounds2); - /// - /// assert_eq!(union_bounds, Bounds { - /// origin: Point { x: 0, y: 0 }, - /// size: Size { width: 20, height: 20 }, - /// }); - /// ``` - pub fn union(&self, other: &Self) -> Self { - let top_left = self.origin.min(&other.origin); - let bottom_right = self.bottom_right().max(&other.bottom_right()); - Bounds::from_corners(top_left, bottom_right) - } -} - -impl Bounds -where - T: Add + Sub + Clone + Debug + Default + PartialEq, -{ - /// Computes the space available within outer bounds. - pub fn space_within(&self, outer: &Self) -> Edges { - Edges { - top: self.top() - outer.top(), - right: outer.right() - self.right(), - bottom: outer.bottom() - self.bottom(), - left: self.left() - outer.left(), - } - } -} - -impl Mul for Bounds -where - T: Mul + Clone + Debug + Default + PartialEq, - Point: Mul>, - Rhs: Clone + Debug + Default + PartialEq, -{ - type Output = Bounds; - - fn mul(self, rhs: Rhs) -> Self::Output { - Bounds { - origin: self.origin * rhs.clone(), - size: self.size * rhs, - } - } -} - -impl MulAssign for Bounds -where - T: Mul + Clone + Debug + Default + PartialEq, - S: Clone, -{ - fn mul_assign(&mut self, rhs: S) { - self.origin *= rhs.clone(); - self.size *= rhs; - } -} - -impl Div for Bounds -where - Size: Div>, - T: Div + Clone + Debug + Default + PartialEq, - S: Clone, -{ - type Output = Self; - - fn div(self, rhs: S) -> Self { - Self { - origin: self.origin / rhs.clone(), - size: self.size / rhs, - } - } -} - -impl Add> for Bounds -where - T: Add + Clone + Debug + Default + PartialEq, -{ - type Output = Self; - - fn add(self, rhs: Point) -> Self { - Self { - origin: self.origin + rhs, - size: self.size, - } - } -} - -impl Sub> for Bounds -where - T: Sub + Clone + Debug + Default + PartialEq, -{ - type Output = Self; - - fn sub(self, rhs: Point) -> Self { - Self { - origin: self.origin - rhs, - size: self.size, - } - } -} - -impl From> for Point { - fn from(size: Size) -> Self { - Self { - x: size.width, - y: size.height, - } - } -} - -impl Bounds -where - T: Add + Clone + Debug + Default + PartialEq, -{ - /// Returns the top edge of the bounds. - /// - /// # Returns - /// - /// A value of type `T` representing the y-coordinate of the top edge of the bounds. - pub fn top(&self) -> T { - self.origin.y.clone() - } - - /// Returns the bottom edge of the bounds. - /// - /// # Returns - /// - /// A value of type `T` representing the y-coordinate of the bottom edge of the bounds. - pub fn bottom(&self) -> T { - self.origin.y.clone() + self.size.height.clone() - } - - /// Returns the left edge of the bounds. - /// - /// # Returns - /// - /// A value of type `T` representing the x-coordinate of the left edge of the bounds. - pub fn left(&self) -> T { - self.origin.x.clone() - } - - /// Returns the right edge of the bounds. - /// - /// # Returns - /// - /// A value of type `T` representing the x-coordinate of the right edge of the bounds. - pub fn right(&self) -> T { - self.origin.x.clone() + self.size.width.clone() - } - - /// Returns the top right corner point of the bounds. - /// - /// # Returns - /// - /// A `Point` representing the top right corner of the bounds. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let bounds = Bounds { - /// origin: Point { x: 0, y: 0 }, - /// size: Size { width: 10, height: 20 }, - /// }; - /// let top_right = bounds.top_right(); - /// assert_eq!(top_right, Point { x: 10, y: 0 }); - /// ``` - pub fn top_right(&self) -> Point { - Point { - x: self.origin.x.clone() + self.size.width.clone(), - y: self.origin.y.clone(), - } - } - - /// Returns the bottom right corner point of the bounds. - /// - /// # Returns - /// - /// A `Point` representing the bottom right corner of the bounds. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let bounds = Bounds { - /// origin: Point { x: 0, y: 0 }, - /// size: Size { width: 10, height: 20 }, - /// }; - /// let bottom_right = bounds.bottom_right(); - /// assert_eq!(bottom_right, Point { x: 10, y: 20 }); - /// ``` - pub fn bottom_right(&self) -> Point { - Point { - x: self.origin.x.clone() + self.size.width.clone(), - y: self.origin.y.clone() + self.size.height.clone(), - } - } - - /// Returns the bottom left corner point of the bounds. - /// - /// # Returns - /// - /// A `Point` representing the bottom left corner of the bounds. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let bounds = Bounds { - /// origin: Point { x: 0, y: 0 }, - /// size: Size { width: 10, height: 20 }, - /// }; - /// let bottom_left = bounds.bottom_left(); - /// assert_eq!(bottom_left, Point { x: 0, y: 20 }); - /// ``` - pub fn bottom_left(&self) -> Point { - Point { - x: self.origin.x.clone(), - y: self.origin.y.clone() + self.size.height.clone(), - } - } -} - -impl Bounds -where - T: Add + Half + Clone + Debug + Default + PartialEq, -{ - /// Returns the requested corner point of the bounds. - /// - /// # Returns - /// - /// A `Point` representing the corner of the bounds requested by the parameter. - /// - /// # Examples - /// - /// ``` - /// use gpui::{Bounds, Anchor, Point, Size}; - /// let bounds = Bounds { - /// origin: Point { x: 0, y: 0 }, - /// size: Size { width: 10, height: 20 }, - /// }; - /// let bottom_left = bounds.corner(Anchor::BottomLeft); - /// assert_eq!(bottom_left, Point { x: 0, y: 20 }); - /// ``` - pub fn corner(&self, corner: Anchor) -> Point { - match corner { - Anchor::TopLeft => self.origin.clone(), - Anchor::TopRight => self.top_right(), - Anchor::BottomLeft => self.bottom_left(), - Anchor::BottomRight => self.bottom_right(), - Anchor::TopCenter => self.top_center(), - Anchor::BottomCenter => self.bottom_center(), - Anchor::LeftCenter => self.left_center(), - Anchor::RightCenter => self.right_center(), - } - } -} - -impl Bounds -where - T: Add + PartialOrd + Clone + Debug + Default + PartialEq, -{ - /// Checks if the given point is within the bounds. - /// - /// This method determines whether a point lies inside the rectangle defined by the bounds, - /// including the edges. The point is considered inside if its x-coordinate is greater than - /// or equal to the left edge and less than or equal to the right edge, and its y-coordinate - /// is greater than or equal to the top edge and less than or equal to the bottom edge of the bounds. - /// - /// # Arguments - /// - /// * `point` - A reference to a `Point` that represents the point to check. - /// - /// # Returns - /// - /// Returns `true` if the point is within the bounds, `false` otherwise. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Point, Bounds, Size}; - /// let bounds = Bounds { - /// origin: Point { x: 0, y: 0 }, - /// size: Size { width: 10, height: 10 }, - /// }; - /// let inside_point = Point { x: 5, y: 5 }; - /// let outside_point = Point { x: 15, y: 15 }; - /// - /// assert!(bounds.contains(&inside_point)); - /// assert!(!bounds.contains(&outside_point)); - /// ``` - pub fn contains(&self, point: &Point) -> bool { - point.x >= self.origin.x - && point.x < self.origin.x.clone() + self.size.width.clone() - && point.y >= self.origin.y - && point.y < self.origin.y.clone() + self.size.height.clone() - } - - /// Checks if this bounds is completely contained within another bounds. - /// - /// This method determines whether the current bounds is entirely enclosed by the given bounds. - /// A bounds is considered to be contained within another if its origin (top-left corner) and - /// its bottom-right corner are both contained within the other bounds. - /// - /// # Arguments - /// - /// * `other` - A reference to another `Bounds` that might contain this bounds. - /// - /// # Returns - /// - /// Returns `true` if this bounds is completely inside the other bounds, `false` otherwise. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let outer_bounds = Bounds { - /// origin: Point { x: 0, y: 0 }, - /// size: Size { width: 20, height: 20 }, - /// }; - /// let inner_bounds = Bounds { - /// origin: Point { x: 5, y: 5 }, - /// size: Size { width: 10, height: 10 }, - /// }; - /// let overlapping_bounds = Bounds { - /// origin: Point { x: 15, y: 15 }, - /// size: Size { width: 10, height: 10 }, - /// }; - /// - /// assert!(inner_bounds.is_contained_within(&outer_bounds)); - /// assert!(!overlapping_bounds.is_contained_within(&outer_bounds)); - /// ``` - pub fn is_contained_within(&self, other: &Self) -> bool { - other.contains(&self.origin) && other.contains(&self.bottom_right()) - } - - /// Applies a function to the origin and size of the bounds, producing a new `Bounds`. - /// - /// This method allows for converting a `Bounds` to a `Bounds` by specifying a closure - /// that defines how to convert between the two types. The closure is applied to the `origin` and - /// `size` fields, resulting in new bounds of the desired type. - /// - /// # Arguments - /// - /// * `f` - A closure that takes a value of type `T` and returns a value of type `U`. - /// - /// # Returns - /// - /// Returns a new `Bounds` with the origin and size mapped by the provided function. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let bounds = Bounds { - /// origin: Point { x: 10.0, y: 10.0 }, - /// size: Size { width: 10.0, height: 20.0 }, - /// }; - /// let new_bounds = bounds.map(|value| value as f64 * 1.5); - /// - /// assert_eq!(new_bounds, Bounds { - /// origin: Point { x: 15.0, y: 15.0 }, - /// size: Size { width: 15.0, height: 30.0 }, - /// }); - /// ``` - pub fn map(&self, f: impl Fn(T) -> U) -> Bounds - where - U: Clone + Debug + Default + PartialEq, - { - Bounds { - origin: self.origin.map(&f), - size: self.size.map(f), - } - } - - /// Applies a function to the origin of the bounds, producing a new `Bounds` with the new origin - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let bounds = Bounds { - /// origin: Point { x: 10.0, y: 10.0 }, - /// size: Size { width: 10.0, height: 20.0 }, - /// }; - /// let new_bounds = bounds.map_origin(|value| value * 1.5); - /// - /// assert_eq!(new_bounds, Bounds { - /// origin: Point { x: 15.0, y: 15.0 }, - /// size: Size { width: 10.0, height: 20.0 }, - /// }); - /// ``` - pub fn map_origin(self, f: impl Fn(T) -> T) -> Bounds { - Bounds { - origin: self.origin.map(f), - size: self.size, - } - } - - /// Applies a function to the origin of the bounds, producing a new `Bounds` with the new origin - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let bounds = Bounds { - /// origin: Point { x: 10.0, y: 10.0 }, - /// size: Size { width: 10.0, height: 20.0 }, - /// }; - /// let new_bounds = bounds.map_size(|value| value * 1.5); - /// - /// assert_eq!(new_bounds, Bounds { - /// origin: Point { x: 10.0, y: 10.0 }, - /// size: Size { width: 15.0, height: 30.0 }, - /// }); - /// ``` - pub fn map_size(self, f: impl Fn(T) -> T) -> Bounds { - Bounds { - origin: self.origin, - size: self.size.map(f), - } - } -} - -impl Bounds -where - T: Add + Sub + PartialOrd + Clone + Debug + Default + PartialEq, -{ - /// Convert a point to the coordinate space defined by this Bounds - pub fn localize(&self, point: &Point) -> Option> { - self.contains(point) - .then(|| point.relative_to(&self.origin)) - } -} - -/// Checks if the bounds represent an empty area. -/// -/// # Returns -/// -/// Returns `true` if either the width or the height of the bounds is less than or equal to zero, indicating an empty area. -impl Bounds { - /// Checks if the bounds represent an empty area. - /// - /// # Returns - /// - /// Returns `true` if either the width or the height of the bounds is less than or equal to zero, indicating an empty area. - #[must_use] - pub fn is_empty(&self) -> bool { - self.size.width <= T::default() || self.size.height <= T::default() - } -} - -impl> Display for Bounds { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{} - {} (size {})", - self.origin, - self.bottom_right(), - self.size - ) - } -} - -impl Size { - /// Converts the size from physical to logical pixels. - pub fn to_pixels(self, scale_factor: f32) -> Size { - size( - px(self.width.0 as f32 / scale_factor), - px(self.height.0 as f32 / scale_factor), - ) - } -} - -impl Size { - /// Converts the size from logical to physical pixels. - pub fn to_device_pixels(self, scale_factor: f32) -> Size { - size( - DevicePixels((self.width.0 * scale_factor).round() as i32), - DevicePixels((self.height.0 * scale_factor).round() as i32), - ) - } -} - -impl Bounds { - /// Scales the bounds by a given factor, typically used to adjust for display scaling. - /// - /// This method multiplies the origin and size of the bounds by the provided scaling factor, - /// resulting in a new `Bounds` that is proportionally larger or smaller - /// depending on the scaling factor. This can be used to ensure that the bounds are properly - /// scaled for different display densities. - /// - /// # Arguments - /// - /// * `factor` - The scaling factor to apply to the origin and size, typically the display's scaling factor. - /// - /// # Returns - /// - /// Returns a new `Bounds` that represents the scaled bounds. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size, Pixels, ScaledPixels, DevicePixels}; - /// let bounds = Bounds { - /// origin: Point { x: Pixels::from(10.0), y: Pixels::from(20.0) }, - /// size: Size { width: Pixels::from(30.0), height: Pixels::from(40.0) }, - /// }; - /// let display_scale_factor = 2.0; - /// let scaled_bounds = bounds.scale(display_scale_factor); - /// assert_eq!(scaled_bounds, Bounds { - /// origin: Point { - /// x: ScaledPixels::from(20.0), - /// y: ScaledPixels::from(40.0), - /// }, - /// size: Size { - /// width: ScaledPixels::from(60.0), - /// height: ScaledPixels::from(80.0) - /// }, - /// }); - /// ``` - pub fn scale(&self, factor: f32) -> Bounds { - Bounds { - origin: self.origin.scale(factor), - size: self.size.scale(factor), - } - } - - /// Convert the bounds from logical pixels to physical pixels - pub fn to_device_pixels(self, factor: f32) -> Bounds { - Bounds { - origin: point( - DevicePixels((self.origin.x.0 * factor).round() as i32), - DevicePixels((self.origin.y.0 * factor).round() as i32), - ), - size: self.size.to_device_pixels(factor), - } - } -} - -impl Bounds { - /// Convert the bounds from physical pixels to logical pixels - pub fn to_pixels(self, scale_factor: f32) -> Bounds { - Bounds { - origin: point( - px(self.origin.x.0 as f32 / scale_factor), - px(self.origin.y.0 as f32 / scale_factor), - ), - size: self.size.to_pixels(scale_factor), - } - } -} - -/// Represents the edges of a box in a 2D space, such as padding or margin. -/// -/// Each field represents the size of the edge on one side of the box: `top`, `right`, `bottom`, and `left`. -/// -/// # Examples -/// -/// ``` -/// # use gpui::Edges; -/// let edges = Edges { -/// top: 10.0, -/// right: 20.0, -/// bottom: 30.0, -/// left: 40.0, -/// }; -/// -/// assert_eq!(edges.top, 10.0); -/// assert_eq!(edges.right, 20.0); -/// assert_eq!(edges.bottom, 30.0); -/// assert_eq!(edges.left, 40.0); -/// ``` -#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)] -#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -#[repr(C)] -pub struct Edges { - /// The size of the top edge. - pub top: T, - /// The size of the right edge. - pub right: T, - /// The size of the bottom edge. - pub bottom: T, - /// The size of the left edge. - pub left: T, -} - -impl Mul for Edges -where - T: Mul + Clone + Debug + Default + PartialEq, -{ - type Output = Self; - - fn mul(self, rhs: Self) -> Self::Output { - Self { - top: self.top.clone() * rhs.top, - right: self.right.clone() * rhs.right, - bottom: self.bottom.clone() * rhs.bottom, - left: self.left * rhs.left, - } - } -} - -impl MulAssign for Edges -where - T: Mul + Clone + Debug + Default + PartialEq, - S: Clone, -{ - fn mul_assign(&mut self, rhs: S) { - self.top = self.top.clone() * rhs.clone(); - self.right = self.right.clone() * rhs.clone(); - self.bottom = self.bottom.clone() * rhs.clone(); - self.left = self.left.clone() * rhs; - } -} - -impl Copy for Edges {} - -impl Edges { - /// Constructs `Edges` where all sides are set to the same specified value. - /// - /// This function creates an `Edges` instance with the `top`, `right`, `bottom`, and `left` fields all initialized - /// to the same value provided as an argument. This is useful when you want to have uniform edges around a box, - /// such as padding or margin with the same size on all sides. - /// - /// # Arguments - /// - /// * `value` - The value to set for all four sides of the edges. - /// - /// # Returns - /// - /// An `Edges` instance with all sides set to the given value. - /// - /// # Examples - /// - /// ``` - /// # use gpui::Edges; - /// let uniform_edges = Edges::all(10.0); - /// assert_eq!(uniform_edges.top, 10.0); - /// assert_eq!(uniform_edges.right, 10.0); - /// assert_eq!(uniform_edges.bottom, 10.0); - /// assert_eq!(uniform_edges.left, 10.0); - /// ``` - pub fn all(value: T) -> Self { - Self { - top: value.clone(), - right: value.clone(), - bottom: value.clone(), - left: value, - } - } - - /// Applies a function to each field of the `Edges`, producing a new `Edges`. - /// - /// This method allows for converting an `Edges` to an `Edges` by specifying a closure - /// that defines how to convert between the two types. The closure is applied to each field - /// (`top`, `right`, `bottom`, `left`), resulting in new edges of the desired type. - /// - /// # Arguments - /// - /// * `f` - A closure that takes a reference to a value of type `T` and returns a value of type `U`. - /// - /// # Returns - /// - /// Returns a new `Edges` with each field mapped by the provided function. - /// - /// # Examples - /// - /// ``` - /// # use gpui::Edges; - /// let edges = Edges { top: 10, right: 20, bottom: 30, left: 40 }; - /// let edges_float = edges.map(|&value| value as f32 * 1.1); - /// assert_eq!(edges_float, Edges { top: 11.0, right: 22.0, bottom: 33.0, left: 44.0 }); - /// ``` - pub fn map(&self, f: impl Fn(&T) -> U) -> Edges - where - U: Clone + Debug + Default + PartialEq, - { - Edges { - top: f(&self.top), - right: f(&self.right), - bottom: f(&self.bottom), - left: f(&self.left), - } - } - - /// Checks if any of the edges satisfy a given predicate. - /// - /// This method applies a predicate function to each field of the `Edges` and returns `true` if any field satisfies the predicate. - /// - /// # Arguments - /// - /// * `predicate` - A closure that takes a reference to a value of type `T` and returns a `bool`. - /// - /// # Returns - /// - /// Returns `true` if the predicate returns `true` for any of the edge values, `false` otherwise. - /// - /// # Examples - /// - /// ``` - /// # use gpui::Edges; - /// let edges = Edges { - /// top: 10, - /// right: 0, - /// bottom: 5, - /// left: 0, - /// }; - /// - /// assert!(edges.any(|value| *value == 0)); - /// assert!(edges.any(|value| *value > 0)); - /// assert!(!edges.any(|value| *value > 10)); - /// ``` - pub fn any bool>(&self, predicate: F) -> bool { - predicate(&self.top) - || predicate(&self.right) - || predicate(&self.bottom) - || predicate(&self.left) - } -} - -impl Edges { - /// Sets the edges of the `Edges` struct to `auto`, which is a special value that allows the layout engine to automatically determine the size of the edges. - /// - /// This is typically used in layout contexts where the exact size of the edges is not important, or when the size should be calculated based on the content or container. - /// - /// # Returns - /// - /// Returns an `Edges` with all edges set to `Length::Auto`. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Edges, Length}; - /// let auto_edges = Edges::auto(); - /// assert_eq!(auto_edges.top, Length::Auto); - /// assert_eq!(auto_edges.right, Length::Auto); - /// assert_eq!(auto_edges.bottom, Length::Auto); - /// assert_eq!(auto_edges.left, Length::Auto); - /// ``` - pub fn auto() -> Self { - Self { - top: Length::Auto, - right: Length::Auto, - bottom: Length::Auto, - left: Length::Auto, - } - } - - /// Sets the edges of the `Edges` struct to zero, which means no size or thickness. - /// - /// This is typically used when you want to specify that a box (like a padding or margin area) - /// should have no edges, effectively making it non-existent or invisible in layout calculations. - /// - /// # Returns - /// - /// Returns an `Edges` with all edges set to zero length. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{DefiniteLength, Edges, Length, Pixels}; - /// let no_edges = Edges::::zero(); - /// assert_eq!(no_edges.top, Length::Definite(DefiniteLength::from(Pixels::ZERO))); - /// assert_eq!(no_edges.right, Length::Definite(DefiniteLength::from(Pixels::ZERO))); - /// assert_eq!(no_edges.bottom, Length::Definite(DefiniteLength::from(Pixels::ZERO))); - /// assert_eq!(no_edges.left, Length::Definite(DefiniteLength::from(Pixels::ZERO))); - /// ``` - pub fn zero() -> Self { - Self { - top: px(0.).into(), - right: px(0.).into(), - bottom: px(0.).into(), - left: px(0.).into(), - } - } -} - -impl Edges { - /// Sets the edges of the `Edges` struct to zero, which means no size or thickness. - /// - /// This is typically used when you want to specify that a box (like a padding or margin area) - /// should have no edges, effectively making it non-existent or invisible in layout calculations. - /// - /// # Returns - /// - /// Returns an `Edges` with all edges set to zero length. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{px, DefiniteLength, Edges}; - /// let no_edges = Edges::::zero(); - /// assert_eq!(no_edges.top, DefiniteLength::from(px(0.))); - /// assert_eq!(no_edges.right, DefiniteLength::from(px(0.))); - /// assert_eq!(no_edges.bottom, DefiniteLength::from(px(0.))); - /// assert_eq!(no_edges.left, DefiniteLength::from(px(0.))); - /// ``` - pub fn zero() -> Self { - Self { - top: px(0.).into(), - right: px(0.).into(), - bottom: px(0.).into(), - left: px(0.).into(), - } - } - - /// Converts the `DefiniteLength` to `Pixels` based on the parent size and the REM size. - /// - /// This method allows for a `DefiniteLength` value to be converted into pixels, taking into account - /// the size of the parent element (for percentage-based lengths) and the size of a rem unit (for rem-based lengths). - /// - /// # Arguments - /// - /// * `parent_size` - `Size` representing the size of the parent element. - /// * `rem_size` - `Pixels` representing the size of one REM unit. - /// - /// # Returns - /// - /// Returns an `Edges` representing the edges with lengths converted to pixels. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Edges, DefiniteLength, px, AbsoluteLength, rems, Size}; - /// let edges = Edges { - /// top: DefiniteLength::Absolute(AbsoluteLength::Pixels(px(10.0))), - /// right: DefiniteLength::Fraction(0.5), - /// bottom: DefiniteLength::Absolute(AbsoluteLength::Rems(rems(2.0))), - /// left: DefiniteLength::Fraction(0.25), - /// }; - /// let parent_size = Size { - /// width: AbsoluteLength::Pixels(px(200.0)), - /// height: AbsoluteLength::Pixels(px(100.0)), - /// }; - /// let rem_size = px(16.0); - /// let edges_in_pixels = edges.to_pixels(parent_size, rem_size); - /// - /// assert_eq!(edges_in_pixels.top, px(10.0)); // Absolute length in pixels - /// assert_eq!(edges_in_pixels.right, px(100.0)); // 50% of parent width - /// assert_eq!(edges_in_pixels.bottom, px(32.0)); // 2 rems - /// assert_eq!(edges_in_pixels.left, px(50.0)); // 25% of parent width - /// ``` - pub fn to_pixels(self, parent_size: Size, rem_size: Pixels) -> Edges { - Edges { - top: self.top.to_pixels(parent_size.height, rem_size), - right: self.right.to_pixels(parent_size.width, rem_size), - bottom: self.bottom.to_pixels(parent_size.height, rem_size), - left: self.left.to_pixels(parent_size.width, rem_size), - } - } -} - -impl Edges { - /// Sets the edges of the `Edges` struct to zero, which means no size or thickness. - /// - /// This is typically used when you want to specify that a box (like a padding or margin area) - /// should have no edges, effectively making it non-existent or invisible in layout calculations. - /// - /// # Returns - /// - /// Returns an `Edges` with all edges set to zero length. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{AbsoluteLength, Edges, Pixels}; - /// let no_edges = Edges::::zero(); - /// assert_eq!(no_edges.top, AbsoluteLength::Pixels(Pixels::ZERO)); - /// assert_eq!(no_edges.right, AbsoluteLength::Pixels(Pixels::ZERO)); - /// assert_eq!(no_edges.bottom, AbsoluteLength::Pixels(Pixels::ZERO)); - /// assert_eq!(no_edges.left, AbsoluteLength::Pixels(Pixels::ZERO)); - /// ``` - pub fn zero() -> Self { - Self { - top: px(0.).into(), - right: px(0.).into(), - bottom: px(0.).into(), - left: px(0.).into(), - } - } - - /// Converts the `AbsoluteLength` to `Pixels` based on the `rem_size`. - /// - /// If the `AbsoluteLength` is already in pixels, it simply returns the corresponding `Pixels` value. - /// If the `AbsoluteLength` is in rems, it multiplies the number of rems by the `rem_size` to convert it to pixels. - /// - /// # Arguments - /// - /// * `rem_size` - The size of one rem unit in pixels. - /// - /// # Returns - /// - /// Returns an `Edges` representing the edges with lengths converted to pixels. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Edges, AbsoluteLength, Pixels, px, rems}; - /// let edges = Edges { - /// top: AbsoluteLength::Pixels(px(10.0)), - /// right: AbsoluteLength::Rems(rems(1.0)), - /// bottom: AbsoluteLength::Pixels(px(20.0)), - /// left: AbsoluteLength::Rems(rems(2.0)), - /// }; - /// let rem_size = px(16.0); - /// let edges_in_pixels = edges.to_pixels(rem_size); - /// - /// assert_eq!(edges_in_pixels.top, px(10.0)); // Already in pixels - /// assert_eq!(edges_in_pixels.right, px(16.0)); // 1 rem converted to pixels - /// assert_eq!(edges_in_pixels.bottom, px(20.0)); // Already in pixels - /// assert_eq!(edges_in_pixels.left, px(32.0)); // 2 rems converted to pixels - /// ``` - pub fn to_pixels(self, rem_size: Pixels) -> Edges { - Edges { - top: self.top.to_pixels(rem_size), - right: self.right.to_pixels(rem_size), - bottom: self.bottom.to_pixels(rem_size), - left: self.left.to_pixels(rem_size), - } - } -} - -impl Edges { - /// Scales the `Edges` by a given factor, returning `Edges`. - /// - /// This method is typically used for adjusting the edge sizes for different display densities or scaling factors. - /// - /// # Arguments - /// - /// * `factor` - The scaling factor to apply to each edge. - /// - /// # Returns - /// - /// Returns a new `Edges` where each edge is the result of scaling the original edge by the given factor. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Edges, Pixels, ScaledPixels}; - /// let edges = Edges { - /// top: Pixels::from(10.0), - /// right: Pixels::from(20.0), - /// bottom: Pixels::from(30.0), - /// left: Pixels::from(40.0), - /// }; - /// let scaled_edges = edges.scale(2.0); - /// assert_eq!(scaled_edges.top, ScaledPixels::from(20.0)); - /// assert_eq!(scaled_edges.right, ScaledPixels::from(40.0)); - /// assert_eq!(scaled_edges.bottom, ScaledPixels::from(60.0)); - /// assert_eq!(scaled_edges.left, ScaledPixels::from(80.0)); - /// ``` - pub fn scale(&self, factor: f32) -> Edges { - Edges { - top: self.top.scale(factor), - right: self.right.scale(factor), - bottom: self.bottom.scale(factor), - left: self.left.scale(factor), - } - } - - /// Returns the maximum value of any edge. - /// - /// # Returns - /// - /// The maximum `Pixels` value among all four edges. - pub fn max(&self) -> Pixels { - self.top.max(self.right).max(self.bottom).max(self.left) - } -} - -impl From for Edges { - fn from(val: f32) -> Self { - let val: Pixels = val.into(); - val.into() - } -} - -impl From for Edges { - fn from(val: Pixels) -> Self { - Edges { - top: val, - right: val, - bottom: val, - left: val, - } - } -} - -/// Identifies a reference point on a 2D box, used to anchor positioned elements. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Anchor { - /// The top left corner - TopLeft, - /// The top right corner - TopRight, - /// The bottom left corner - BottomLeft, - /// The bottom right corner - BottomRight, - /// The top center position - TopCenter, - /// The bottom center position - BottomCenter, - /// The left center position - LeftCenter, - /// The right center position - RightCenter, -} - -impl Anchor { - /// Returns the directly opposite corner. - /// - /// # Examples - /// - /// ``` - /// # use gpui::Anchor; - /// assert_eq!(Anchor::TopLeft.opposite(), Anchor::BottomRight); - /// ``` - #[must_use] - pub fn opposite(self) -> Self { - match self { - Anchor::TopLeft => Anchor::BottomRight, - Anchor::TopRight => Anchor::BottomLeft, - Anchor::BottomLeft => Anchor::TopRight, - Anchor::BottomRight => Anchor::TopLeft, - Anchor::TopCenter => Anchor::BottomCenter, - Anchor::BottomCenter => Anchor::TopCenter, - Anchor::LeftCenter => Anchor::RightCenter, - Anchor::RightCenter => Anchor::LeftCenter, - } - } - - /// Returns the corner across from this corner, moving along the specified axis. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Axis, Anchor}; - /// let result = Anchor::TopLeft.other_side_along(Axis::Horizontal); - /// assert_eq!(result, Anchor::TopRight); - /// ``` - #[must_use] - pub fn other_side_along(self, axis: Axis) -> Self { - match axis { - Axis::Vertical => match self { - Anchor::TopLeft => Anchor::BottomLeft, - Anchor::TopRight => Anchor::BottomRight, - Anchor::BottomLeft => Anchor::TopLeft, - Anchor::BottomRight => Anchor::TopRight, - Anchor::TopCenter => Anchor::BottomCenter, - Anchor::BottomCenter => Anchor::TopCenter, - Anchor::LeftCenter => Anchor::LeftCenter, - Anchor::RightCenter => Anchor::RightCenter, - }, - Axis::Horizontal => match self { - Anchor::TopLeft => Anchor::TopRight, - Anchor::TopRight => Anchor::TopLeft, - Anchor::BottomLeft => Anchor::BottomRight, - Anchor::BottomRight => Anchor::BottomLeft, - Anchor::TopCenter => Anchor::TopCenter, - Anchor::BottomCenter => Anchor::BottomCenter, - Anchor::LeftCenter => Anchor::RightCenter, - Anchor::RightCenter => Anchor::LeftCenter, - }, - } - } - - /// Returns true if at the center. - #[inline] - pub fn is_center(&self) -> bool { - matches!( - self, - Self::TopCenter | Self::BottomCenter | Self::LeftCenter | Self::RightCenter - ) - } -} - -/// Represents the corners of a box in a 2D space, such as border radius. -/// -/// Each field represents the size of the corner on one side of the box: `top_left`, `top_right`, `bottom_right`, and `bottom_left`. -#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)] -#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -#[repr(C)] -pub struct Corners { - /// The value associated with the top left corner. - pub top_left: T, - /// The value associated with the top right corner. - pub top_right: T, - /// The value associated with the bottom right corner. - pub bottom_right: T, - /// The value associated with the bottom left corner. - pub bottom_left: T, -} - -impl Corners -where - T: Add + Half + Clone + Debug + Default + PartialEq, -{ - /// Constructs `Corners` where all sides are set to the same specified value. - /// - /// This function creates a `Corners` instance with the `top_left`, `top_right`, `bottom_right`, and `bottom_left` fields all initialized - /// to the same value provided as an argument. This is useful when you want to have uniform corners around a box, - /// such as a uniform border radius on a rectangle. - /// - /// # Arguments - /// - /// * `value` - The value to set for all four corners. - /// - /// # Returns - /// - /// An `Corners` instance with all corners set to the given value. - /// - /// # Examples - /// - /// ``` - /// # use gpui::Corners; - /// let uniform_corners = Corners::all(5.0); - /// assert_eq!(uniform_corners.top_left, 5.0); - /// assert_eq!(uniform_corners.top_right, 5.0); - /// assert_eq!(uniform_corners.bottom_right, 5.0); - /// assert_eq!(uniform_corners.bottom_left, 5.0); - /// ``` - pub fn all(value: T) -> Self { - Self { - top_left: value.clone(), - top_right: value.clone(), - bottom_right: value.clone(), - bottom_left: value, - } - } - - /// Returns the requested corner value, supporting all eight corner positions. - /// - /// For the four basic corners (TopLeft, TopRight, BottomLeft, BottomRight), - /// this returns the corresponding field value directly. - /// - /// For the center positions (TopCenter, BottomCenter, LeftCenter, RightCenter), - /// this calculates the average of the two adjacent corners. - /// - /// # Returns - /// - /// A value of type `T` representing the corner requested by the parameter. - /// - /// # Examples - /// - /// Basic corner positions: - /// - /// ``` - /// # use gpui::{Anchor, Corners}; - /// let corners = Corners { - /// top_left: 10, - /// top_right: 20, - /// bottom_left: 30, - /// bottom_right: 40 - /// }; - /// assert_eq!(corners.corner(Anchor::TopLeft), 10); - /// assert_eq!(corners.corner(Anchor::BottomRight), 40); - /// ``` - /// - /// Center positions (calculated as average of adjacent corners): - /// - /// ``` - /// # use gpui::{Anchor, Corners}; - /// let corners = Corners { - /// top_left: 10, - /// top_right: 20, - /// bottom_left: 30, - /// bottom_right: 40 - /// }; - /// assert_eq!(corners.corner(Anchor::TopCenter), 15); - /// assert_eq!(corners.corner(Anchor::BottomCenter), 35); - /// assert_eq!(corners.corner(Anchor::LeftCenter), 20); - /// assert_eq!(corners.corner(Anchor::RightCenter), 30); - /// ``` - #[must_use] - pub fn corner(&self, corner: Anchor) -> T { - match corner { - Anchor::TopLeft => self.top_left.clone(), - Anchor::TopRight => self.top_right.clone(), - Anchor::BottomLeft => self.bottom_left.clone(), - Anchor::BottomRight => self.bottom_right.clone(), - Anchor::TopCenter => (self.top_left.clone() + self.top_right.clone()).half(), - Anchor::BottomCenter => (self.bottom_left.clone() + self.bottom_right.clone()).half(), - Anchor::LeftCenter => (self.top_left.clone() + self.bottom_left.clone()).half(), - Anchor::RightCenter => (self.top_right.clone() + self.bottom_right.clone()).half(), - } - } -} - -impl Corners { - /// Converts the `AbsoluteLength` to `Pixels` based on the provided rem size. - /// - /// # Arguments - /// - /// * `rem_size` - The size of one REM unit in pixels, used for conversion if the `AbsoluteLength` is in REMs. - /// - /// # Returns - /// - /// Returns a `Corners` instance with each corner's length converted to pixels. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Corners, AbsoluteLength, Pixels, Rems, Size}; - /// let corners = Corners { - /// top_left: AbsoluteLength::Pixels(Pixels::from(15.0)), - /// top_right: AbsoluteLength::Rems(Rems(1.0)), - /// bottom_right: AbsoluteLength::Pixels(Pixels::from(30.0)), - /// bottom_left: AbsoluteLength::Rems(Rems(2.0)), - /// }; - /// let rem_size = Pixels::from(16.0); - /// let corners_in_pixels = corners.to_pixels(rem_size); - /// - /// assert_eq!(corners_in_pixels.top_left, Pixels::from(15.0)); - /// assert_eq!(corners_in_pixels.top_right, Pixels::from(16.0)); // 1 rem converted to pixels - /// assert_eq!(corners_in_pixels.bottom_right, Pixels::from(30.0)); - /// assert_eq!(corners_in_pixels.bottom_left, Pixels::from(32.0)); // 2 rems converted to pixels - /// ``` - pub fn to_pixels(self, rem_size: Pixels) -> Corners { - Corners { - top_left: self.top_left.to_pixels(rem_size), - top_right: self.top_right.to_pixels(rem_size), - bottom_right: self.bottom_right.to_pixels(rem_size), - bottom_left: self.bottom_left.to_pixels(rem_size), - } - } -} - -impl Corners { - /// Scales the `Corners` by a given factor, returning `Corners`. - /// - /// This method is typically used for adjusting the corner sizes for different display densities or scaling factors. - /// - /// # Arguments - /// - /// * `factor` - The scaling factor to apply to each corner. - /// - /// # Returns - /// - /// Returns a new `Corners` where each corner is the result of scaling the original corner by the given factor. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Corners, Pixels, ScaledPixels}; - /// let corners = Corners { - /// top_left: Pixels::from(10.0), - /// top_right: Pixels::from(20.0), - /// bottom_right: Pixels::from(30.0), - /// bottom_left: Pixels::from(40.0), - /// }; - /// let scaled_corners = corners.scale(2.0); - /// assert_eq!(scaled_corners.top_left, ScaledPixels::from(20.0)); - /// assert_eq!(scaled_corners.top_right, ScaledPixels::from(40.0)); - /// assert_eq!(scaled_corners.bottom_right, ScaledPixels::from(60.0)); - /// assert_eq!(scaled_corners.bottom_left, ScaledPixels::from(80.0)); - /// ``` - #[must_use] - pub fn scale(&self, factor: f32) -> Corners { - Corners { - top_left: self.top_left.scale(factor), - top_right: self.top_right.scale(factor), - bottom_right: self.bottom_right.scale(factor), - bottom_left: self.bottom_left.scale(factor), - } - } - - /// Returns the maximum value of any corner. - /// - /// # Returns - /// - /// The maximum `Pixels` value among all four corners. - #[must_use] - pub fn max(&self) -> Pixels { - self.top_left - .max(self.top_right) - .max(self.bottom_right) - .max(self.bottom_left) - } -} - -impl + Ord + Clone + Debug + Default + PartialEq> Corners { - /// Clamps corner radii to be less than or equal to half the shortest side of a quad. - /// - /// # Arguments - /// - /// * `size` - The size of the quad which limits the size of the corner radii. - /// - /// # Returns - /// - /// Anchor radii values clamped to fit. - #[must_use] - pub fn clamp_radii_for_quad_size(self, size: Size) -> Corners { - let max = cmp::min(size.width, size.height) / 2.; - Corners { - top_left: cmp::min(self.top_left, max.clone()), - top_right: cmp::min(self.top_right, max.clone()), - bottom_right: cmp::min(self.bottom_right, max.clone()), - bottom_left: cmp::min(self.bottom_left, max), - } - } -} - -impl Corners { - /// Applies a function to each field of the `Corners`, producing a new `Corners`. - /// - /// This method allows for converting a `Corners` to a `Corners` by specifying a closure - /// that defines how to convert between the two types. The closure is applied to each field - /// (`top_left`, `top_right`, `bottom_right`, `bottom_left`), resulting in new corners of the desired type. - /// - /// # Arguments - /// - /// * `f` - A closure that takes a reference to a value of type `T` and returns a value of type `U`. - /// - /// # Returns - /// - /// Returns a new `Corners` with each field mapped by the provided function. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Corners, Pixels, Rems}; - /// let corners = Corners { - /// top_left: Pixels::from(10.0), - /// top_right: Pixels::from(20.0), - /// bottom_right: Pixels::from(30.0), - /// bottom_left: Pixels::from(40.0), - /// }; - /// let corners_in_rems = corners.map(|&px| Rems(f32::from(px) / 16.0)); - /// assert_eq!(corners_in_rems, Corners { - /// top_left: Rems(0.625), - /// top_right: Rems(1.25), - /// bottom_right: Rems(1.875), - /// bottom_left: Rems(2.5), - /// }); - /// ``` - #[must_use] - pub fn map(&self, f: impl Fn(&T) -> U) -> Corners - where - U: Clone + Debug + Default + PartialEq, - { - Corners { - top_left: f(&self.top_left), - top_right: f(&self.top_right), - bottom_right: f(&self.bottom_right), - bottom_left: f(&self.bottom_left), - } - } -} - -impl Mul for Corners -where - T: Mul + Clone + Debug + Default + PartialEq, -{ - type Output = Self; - - fn mul(self, rhs: Self) -> Self::Output { - Self { - top_left: self.top_left.clone() * rhs.top_left, - top_right: self.top_right.clone() * rhs.top_right, - bottom_right: self.bottom_right.clone() * rhs.bottom_right, - bottom_left: self.bottom_left * rhs.bottom_left, - } - } -} - -impl MulAssign for Corners -where - T: Mul + Clone + Debug + Default + PartialEq, - S: Clone, -{ - fn mul_assign(&mut self, rhs: S) { - self.top_left = self.top_left.clone() * rhs.clone(); - self.top_right = self.top_right.clone() * rhs.clone(); - self.bottom_right = self.bottom_right.clone() * rhs.clone(); - self.bottom_left = self.bottom_left.clone() * rhs; - } -} - -impl Copy for Corners where T: Copy + Clone + Debug + Default + PartialEq {} - -impl From for Corners { - fn from(val: f32) -> Self { - Corners { - top_left: val.into(), - top_right: val.into(), - bottom_right: val.into(), - bottom_left: val.into(), - } - } -} - -impl From for Corners { - fn from(val: Pixels) -> Self { - Corners { - top_left: val, - top_right: val, - bottom_right: val, - bottom_left: val, - } - } -} - -/// Represents an angle in Radians -#[derive( - Clone, - Copy, - Default, - Add, - AddAssign, - Sub, - SubAssign, - Neg, - Div, - DivAssign, - PartialEq, - Serialize, - Deserialize, - Debug, -)] -#[repr(transparent)] -pub struct Radians(pub f32); - -/// Create a `Radian` from a raw value -pub fn radians(value: f32) -> Radians { - Radians(value) -} - -/// A type representing a percentage value. -#[derive( - Clone, - Copy, - Default, - Add, - AddAssign, - Sub, - SubAssign, - Neg, - Div, - DivAssign, - PartialEq, - Serialize, - Deserialize, - Debug, -)] -#[repr(transparent)] -pub struct Percentage(pub f32); - -/// Generate a `Radian` from a percentage of a full circle. -pub fn percentage(value: f32) -> Percentage { - debug_assert!( - (0.0..=1.0).contains(&value), - "Percentage must be between 0 and 1" - ); - Percentage(value) -} - -impl From for Radians { - fn from(value: Percentage) -> Self { - radians(value.0 * std::f32::consts::PI * 2.0) - } -} - -/// Represents a length in pixels, the base unit of measurement in the UI framework. -/// -/// `Pixels` is a value type that represents an absolute length in pixels, which is used -/// for specifying sizes, positions, and distances in the UI. It is the fundamental unit -/// of measurement for all visual elements and layout calculations. -/// -/// The inner value is an `f32`, allowing for sub-pixel precision which can be useful for -/// anti-aliasing and animations. However, when applied to actual pixel grids, the value -/// is typically rounded to the nearest integer. -/// -/// # Examples -/// -/// ``` -/// use gpui::{Pixels, ScaledPixels}; -/// -/// // Define a length of 10 pixels -/// let length = Pixels::from(10.0); -/// -/// // Define a length and scale it by a factor of 2 -/// let scaled_length = length.scale(2.0); -/// assert_eq!(scaled_length, ScaledPixels::from(20.0)); -/// ``` -#[derive( - Clone, - Copy, - Default, - Add, - AddAssign, - Sub, - SubAssign, - Neg, - Div, - DivAssign, - PartialEq, - Serialize, - Deserialize, - JsonSchema, -)] -#[repr(transparent)] -pub struct Pixels(pub(crate) f32); - -impl Div for Pixels { - type Output = f32; - - fn div(self, rhs: Self) -> Self::Output { - self.0 / rhs.0 - } -} - -impl std::ops::DivAssign for Pixels { - fn div_assign(&mut self, rhs: Self) { - *self = Self(self.0 / rhs.0); - } -} - -impl std::ops::RemAssign for Pixels { - fn rem_assign(&mut self, rhs: Self) { - self.0 %= rhs.0; - } -} - -impl std::ops::Rem for Pixels { - type Output = Self; - - fn rem(self, rhs: Self) -> Self { - Self(self.0 % rhs.0) - } -} - -impl Mul for Pixels { - type Output = Self; - - fn mul(self, rhs: f32) -> Self { - Self(self.0 * rhs) - } -} - -impl Mul for f32 { - type Output = Pixels; - - fn mul(self, rhs: Pixels) -> Self::Output { - rhs * self - } -} - -impl Mul for Pixels { - type Output = Self; - - fn mul(self, rhs: usize) -> Self { - self * (rhs as f32) - } -} - -impl Mul for usize { - type Output = Pixels; - - fn mul(self, rhs: Pixels) -> Pixels { - rhs * self - } -} - -impl MulAssign for Pixels { - fn mul_assign(&mut self, rhs: f32) { - self.0 *= rhs; - } -} - -impl Display for Pixels { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}px", self.0) - } -} - -impl Debug for Pixels { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - Display::fmt(self, f) - } -} - -impl std::iter::Sum for Pixels { - fn sum>(iter: I) -> Self { - iter.fold(Self::ZERO, |a, b| a + b) - } -} - -impl<'a> std::iter::Sum<&'a Pixels> for Pixels { - fn sum>(iter: I) -> Self { - iter.fold(Self::ZERO, |a, b| a + *b) - } -} - -impl TryFrom<&'_ str> for Pixels { - type Error = anyhow::Error; - - fn try_from(value: &'_ str) -> Result { - value - .strip_suffix("px") - .context("expected 'px' suffix") - .and_then(|number| Ok(number.parse()?)) - .map(Self) - } -} - -impl Pixels { - /// Represents zero pixels. - pub const ZERO: Pixels = Pixels(0.0); - /// The maximum value that can be represented by `Pixels`. - pub const MAX: Pixels = Pixels(f32::MAX); - /// The minimum value that can be represented by `Pixels`. - pub const MIN: Pixels = Pixels(f32::MIN); - - /// Returns the raw `f32` value of this `Pixels`. - pub fn as_f32(self) -> f32 { - self.0 - } - - /// Floors the `Pixels` value to the nearest whole number. - /// - /// # Returns - /// - /// Returns a new `Pixels` instance with the floored value. - pub fn floor(&self) -> Self { - Self(self.0.floor()) - } - - /// Rounds the `Pixels` value to the nearest whole number. - /// - /// # Returns - /// - /// Returns a new `Pixels` instance with the rounded value. - pub fn round(&self) -> Self { - Self(self.0.round()) - } - - /// Returns the ceiling of the `Pixels` value to the nearest whole number. - /// - /// # Returns - /// - /// Returns a new `Pixels` instance with the ceiling value. - pub fn ceil(&self) -> Self { - Self(self.0.ceil()) - } - - /// Scales the `Pixels` value by a given factor, producing `ScaledPixels`. - /// - /// This method is used when adjusting pixel values for display scaling factors, - /// such as high DPI (dots per inch) or Retina displays, where the pixel density is higher and - /// thus requires scaling to maintain visual consistency and readability. - /// - /// The resulting `ScaledPixels` represent the scaled value which can be used for rendering - /// calculations where display scaling is considered. - #[must_use] - pub fn scale(&self, factor: f32) -> ScaledPixels { - ScaledPixels(self.0 * factor) - } - - /// Raises the `Pixels` value to a given power. - /// - /// # Arguments - /// - /// * `exponent` - The exponent to raise the `Pixels` value by. - /// - /// # Returns - /// - /// Returns a new `Pixels` instance with the value raised to the given exponent. - pub fn pow(&self, exponent: f32) -> Self { - Self(self.0.powf(exponent)) - } - - /// Returns the absolute value of the `Pixels`. - /// - /// # Returns - /// - /// A new `Pixels` instance with the absolute value of the original `Pixels`. - pub fn abs(&self) -> Self { - Self(self.0.abs()) - } - - /// Returns the sign of the `Pixels` value. - /// - /// # Returns - /// - /// Returns: - /// * `1.0` if the value is positive - /// * `-1.0` if the value is negative - pub fn signum(&self) -> f32 { - self.0.signum() - } - - /// Returns the f64 value of `Pixels`. - /// - /// # Returns - /// - /// A f64 value of the `Pixels`. - pub fn to_f64(self) -> f64 { - self.0 as f64 - } -} - -impl Eq for Pixels {} - -impl PartialOrd for Pixels { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Pixels { - fn cmp(&self, other: &Self) -> cmp::Ordering { - self.0.total_cmp(&other.0) - } -} - -impl std::hash::Hash for Pixels { - fn hash(&self, state: &mut H) { - self.0.to_bits().hash(state); - } -} - -impl From for Pixels { - fn from(pixels: f64) -> Self { - Pixels(pixels as f32) - } -} - -impl From for Pixels { - fn from(pixels: f32) -> Self { - Pixels(pixels) - } -} - -impl From for f32 { - fn from(pixels: Pixels) -> Self { - pixels.0 - } -} - -impl From<&Pixels> for f32 { - fn from(pixels: &Pixels) -> Self { - pixels.0 - } -} - -impl From for f64 { - fn from(pixels: Pixels) -> Self { - pixels.0 as f64 - } -} - -impl From for u32 { - fn from(pixels: Pixels) -> Self { - pixels.0 as u32 - } -} - -impl From<&Pixels> for u32 { - fn from(pixels: &Pixels) -> Self { - pixels.0 as u32 - } -} - -impl From for Pixels { - fn from(pixels: u32) -> Self { - Pixels(pixels as f32) - } -} - -impl From for usize { - fn from(pixels: Pixels) -> Self { - pixels.0 as usize - } -} - -impl From for Pixels { - fn from(pixels: usize) -> Self { - Pixels(pixels as f32) - } -} - -/// Represents physical pixels on the display. -/// -/// `DevicePixels` is a unit of measurement that refers to the actual pixels on a device's screen. -/// This type is used when precise pixel manipulation is required, such as rendering graphics or -/// interfacing with hardware that operates on the pixel level. Unlike logical pixels that may be -/// affected by the device's scale factor, `DevicePixels` always correspond to real pixels on the -/// display. -#[derive( - Add, - AddAssign, - Clone, - Copy, - Default, - Div, - Eq, - Hash, - Ord, - PartialEq, - PartialOrd, - Sub, - SubAssign, - Serialize, - Deserialize, -)] -#[repr(transparent)] -pub struct DevicePixels(pub i32); - -impl DevicePixels { - /// Converts the `DevicePixels` value to the number of bytes needed to represent it in memory. - /// - /// This function is useful when working with graphical data that needs to be stored in a buffer, - /// such as images or framebuffers, where each pixel may be represented by a specific number of bytes. - /// - /// # Arguments - /// - /// * `bytes_per_pixel` - The number of bytes used to represent a single pixel. - /// - /// # Returns - /// - /// The number of bytes required to represent the `DevicePixels` value in memory. - /// - /// # Examples - /// - /// ``` - /// # use gpui::DevicePixels; - /// let pixels = DevicePixels(10); // 10 device pixels - /// let bytes_per_pixel = 4; // Assume each pixel is represented by 4 bytes (e.g., RGBA) - /// let total_bytes = pixels.to_bytes(bytes_per_pixel); - /// assert_eq!(total_bytes, 40); // 10 pixels * 4 bytes/pixel = 40 bytes - /// ``` - pub fn to_bytes(self, bytes_per_pixel: u8) -> u32 { - self.0 as u32 * bytes_per_pixel as u32 - } -} - -impl fmt::Debug for DevicePixels { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} px (device)", self.0) - } -} - -impl From for i32 { - fn from(device_pixels: DevicePixels) -> Self { - device_pixels.0 - } -} - -impl From for DevicePixels { - fn from(device_pixels: i32) -> Self { - DevicePixels(device_pixels) - } -} - -impl From for DevicePixels { - fn from(device_pixels: u32) -> Self { - DevicePixels(device_pixels as i32) - } -} - -impl From for u32 { - fn from(device_pixels: DevicePixels) -> Self { - device_pixels.0 as u32 - } -} - -impl From for u64 { - fn from(device_pixels: DevicePixels) -> Self { - device_pixels.0 as u64 - } -} - -impl From for DevicePixels { - fn from(device_pixels: u64) -> Self { - DevicePixels(device_pixels as i32) - } -} - -impl From for usize { - fn from(device_pixels: DevicePixels) -> Self { - device_pixels.0 as usize - } -} - -impl From for DevicePixels { - fn from(device_pixels: usize) -> Self { - DevicePixels(device_pixels as i32) - } -} - -/// Represents scaled pixels that take into account the device's scale factor. -/// -/// `ScaledPixels` are used to ensure that UI elements appear at the correct size on devices -/// with different pixel densities. When a device has a higher scale factor (such as Retina displays), -/// a single logical pixel may correspond to multiple physical pixels. By using `ScaledPixels`, -/// dimensions and positions can be specified in a way that scales appropriately across different -/// display resolutions. -#[derive(Clone, Copy, Default, Add, AddAssign, Sub, SubAssign, Div, DivAssign, PartialEq)] -#[repr(transparent)] -pub struct ScaledPixels(pub f32); - -impl ScaledPixels { - /// Returns the raw `f32` value of this `ScaledPixels`. - pub fn as_f32(self) -> f32 { - self.0 - } - - /// Floors the `ScaledPixels` value to the nearest whole number. - /// - /// # Returns - /// - /// Returns a new `ScaledPixels` instance with the floored value. - pub fn floor(&self) -> Self { - Self(self.0.floor()) - } - - /// Rounds the `ScaledPixels` value to the nearest whole number. - /// - /// # Returns - /// - /// Returns a new `ScaledPixels` instance with the rounded value. - pub fn round(&self) -> Self { - Self(self.0.round()) - } - - /// Ceils the `ScaledPixels` value to the nearest whole number. - /// - /// # Returns - /// - /// Returns a new `ScaledPixels` instance with the ceiled value. - pub fn ceil(&self) -> Self { - Self(self.0.ceil()) - } -} - -impl Eq for ScaledPixels {} - -impl PartialOrd for ScaledPixels { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for ScaledPixels { - fn cmp(&self, other: &Self) -> cmp::Ordering { - self.0.total_cmp(&other.0) - } -} - -impl Debug for ScaledPixels { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}px (scaled)", self.0) - } -} - -impl From for DevicePixels { - fn from(scaled: ScaledPixels) -> Self { - DevicePixels(scaled.0.ceil() as i32) - } -} - -impl From for ScaledPixels { - fn from(device: DevicePixels) -> Self { - ScaledPixels(device.0 as f32) - } -} - -impl From for f64 { - fn from(scaled_pixels: ScaledPixels) -> Self { - scaled_pixels.0 as f64 - } -} - -impl From for u32 { - fn from(pixels: ScaledPixels) -> Self { - pixels.0 as u32 - } -} - -impl From for ScaledPixels { - fn from(pixels: f32) -> Self { - Self(pixels) - } -} - -impl Div for ScaledPixels { - type Output = f32; - - fn div(self, rhs: Self) -> Self::Output { - self.0 / rhs.0 - } -} - -impl std::ops::DivAssign for ScaledPixels { - fn div_assign(&mut self, rhs: Self) { - *self = Self(self.0 / rhs.0); - } -} - -impl std::ops::RemAssign for ScaledPixels { - fn rem_assign(&mut self, rhs: Self) { - self.0 %= rhs.0; - } -} - -impl std::ops::Rem for ScaledPixels { - type Output = Self; - - fn rem(self, rhs: Self) -> Self { - Self(self.0 % rhs.0) - } -} - -impl Mul for ScaledPixels { - type Output = Self; - - fn mul(self, rhs: f32) -> Self { - Self(self.0 * rhs) - } -} - -impl Mul for f32 { - type Output = ScaledPixels; - - fn mul(self, rhs: ScaledPixels) -> Self::Output { - rhs * self - } -} - -impl Mul for ScaledPixels { - type Output = Self; - - fn mul(self, rhs: usize) -> Self { - self * (rhs as f32) - } -} - -impl Mul for usize { - type Output = ScaledPixels; - - fn mul(self, rhs: ScaledPixels) -> ScaledPixels { - rhs * self - } -} - -impl MulAssign for ScaledPixels { - fn mul_assign(&mut self, rhs: f32) { - self.0 *= rhs; - } -} - -/// Represents a length in rems, a unit based on the font-size of the window, which can be assigned with [`Window::set_rem_size`][set_rem_size]. -/// -/// Rems are used for defining lengths that are scalable and consistent across different UI elements. -/// The value of `1rem` is typically equal to the font-size of the root element (often the `` element in browsers), -/// making it a flexible unit that adapts to the user's text size preferences. In this framework, `rems` serve a similar -/// purpose, allowing for scalable and accessible design that can adjust to different display settings or user preferences. -/// -/// For example, if the root element's font-size is `16px`, then `1rem` equals `16px`. A length of `2rems` would then be `32px`. -/// -/// [set_rem_size]: crate::Window::set_rem_size -#[derive(Clone, Copy, Default, Add, Sub, Mul, Div, Neg, PartialEq)] -pub struct Rems(pub f32); - -impl Rems { - /// A length of zero. - pub const ZERO: Self = Self(0.0); - /// Convert this Rem value to pixels. - pub fn to_pixels(self, rem_size: Pixels) -> Pixels { - self * rem_size - } - /// Convert from pixels to Rem - pub fn from_pixels(length: Pixels, window: &gpui::Window) -> Self { - Self(length / window.rem_size()) - } -} - -impl Mul for Rems { - type Output = Pixels; - - fn mul(self, other: Pixels) -> Pixels { - Pixels(self.0 * other.0) - } -} - -impl AddAssign for Rems { - fn add_assign(&mut self, rhs: Rems) { - self.0 += rhs.0 - } -} - -impl Display for Rems { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}rem", self.0) - } -} - -impl Debug for Rems { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - Display::fmt(self, f) - } -} - -impl TryFrom<&'_ str> for Rems { - type Error = anyhow::Error; - - fn try_from(value: &'_ str) -> Result { - value - .strip_suffix("rem") - .context("expected 'rem' suffix") - .and_then(|number| Ok(number.parse()?)) - .map(Self) - } -} - -/// Represents an absolute length in pixels or rems. -/// -/// `AbsoluteLength` can be either a fixed number of pixels, which is an absolute measurement not -/// affected by the current font size, or a number of rems, which is relative to the font size of -/// the root element. It is used for specifying dimensions that are either independent of or -/// related to the typographic scale. -#[derive(Clone, Copy, Neg, PartialEq)] -pub enum AbsoluteLength { - /// A length in pixels. - Pixels(Pixels), - /// A length in rems. - Rems(Rems), -} - -impl AbsoluteLength { - /// Checks if the absolute length is zero. - pub fn is_zero(&self) -> bool { - match self { - AbsoluteLength::Pixels(px) => px.0 == 0.0, - AbsoluteLength::Rems(rems) => rems.0 == 0.0, - } - } -} - -impl From for AbsoluteLength { - fn from(pixels: Pixels) -> Self { - AbsoluteLength::Pixels(pixels) - } -} - -impl From for AbsoluteLength { - fn from(rems: Rems) -> Self { - AbsoluteLength::Rems(rems) - } -} - -impl AbsoluteLength { - /// Converts an `AbsoluteLength` to `Pixels` based on a given `rem_size`. - /// - /// # Arguments - /// - /// * `rem_size` - The size of one rem in pixels. - /// - /// # Returns - /// - /// Returns the `AbsoluteLength` as `Pixels`. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{AbsoluteLength, Pixels, Rems}; - /// let length_in_pixels = AbsoluteLength::Pixels(Pixels::from(42.0)); - /// let length_in_rems = AbsoluteLength::Rems(Rems(2.0)); - /// let rem_size = Pixels::from(16.0); - /// - /// assert_eq!(length_in_pixels.to_pixels(rem_size), Pixels::from(42.0)); - /// assert_eq!(length_in_rems.to_pixels(rem_size), Pixels::from(32.0)); - /// ``` - pub fn to_pixels(self, rem_size: Pixels) -> Pixels { - match self { - AbsoluteLength::Pixels(pixels) => pixels, - AbsoluteLength::Rems(rems) => rems.to_pixels(rem_size), - } - } - - /// Converts an `AbsoluteLength` to `Rems` based on a given `rem_size`. - /// - /// # Arguments - /// - /// * `rem_size` - The size of one rem in pixels. - /// - /// # Returns - /// - /// Returns the `AbsoluteLength` as `Pixels`. - pub fn to_rems(self, rem_size: Pixels) -> Rems { - match self { - AbsoluteLength::Pixels(pixels) => Rems(pixels.0 / rem_size.0), - AbsoluteLength::Rems(rems) => rems, - } - } -} - -impl Default for AbsoluteLength { - fn default() -> Self { - px(0.).into() - } -} - -impl Display for AbsoluteLength { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Pixels(pixels) => write!(f, "{pixels}"), - Self::Rems(rems) => write!(f, "{rems}"), - } - } -} - -impl Debug for AbsoluteLength { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - Display::fmt(self, f) - } -} - -const EXPECTED_ABSOLUTE_LENGTH: &str = "number with 'px' or 'rem' suffix"; - -impl TryFrom<&'_ str> for AbsoluteLength { - type Error = anyhow::Error; - - fn try_from(value: &'_ str) -> Result { - if let Ok(pixels) = value.try_into() { - Ok(Self::Pixels(pixels)) - } else if let Ok(rems) = value.try_into() { - Ok(Self::Rems(rems)) - } else { - Err(anyhow!( - "invalid AbsoluteLength '{value}', expected {EXPECTED_ABSOLUTE_LENGTH}" - )) - } - } -} - -impl JsonSchema for AbsoluteLength { - fn schema_name() -> Cow<'static, str> { - "AbsoluteLength".into() - } - - fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema { - json_schema!({ - "type": "string", - "pattern": r"^-?\d+(\.\d+)?(px|rem)$" - }) - } -} - -impl<'de> Deserialize<'de> for AbsoluteLength { - fn deserialize>(deserializer: D) -> Result { - struct StringVisitor; - - impl de::Visitor<'_> for StringVisitor { - type Value = AbsoluteLength; - - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{EXPECTED_ABSOLUTE_LENGTH}") - } - - fn visit_str(self, value: &str) -> Result { - AbsoluteLength::try_from(value).map_err(E::custom) - } - } - - deserializer.deserialize_str(StringVisitor) - } -} - -impl Serialize for AbsoluteLength { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&format!("{self}")) - } -} - -/// A non-auto length that can be defined in pixels, rems, or percent of parent. -/// -/// This enum represents lengths that have a specific value, as opposed to lengths that are automatically -/// determined by the context. It includes absolute lengths in pixels or rems, and relative lengths as a -/// fraction of the parent's size. -#[derive(Clone, Copy, Neg, PartialEq)] -pub enum DefiniteLength { - /// An absolute length specified in pixels or rems. - Absolute(AbsoluteLength), - /// A relative length specified as a fraction of the parent's size, between 0 and 1. - Fraction(f32), -} - -impl DefiniteLength { - /// Converts the `DefiniteLength` to `Pixels` based on a given `base_size` and `rem_size`. - /// - /// If the `DefiniteLength` is an absolute length, it will be directly converted to `Pixels`. - /// If it is a fraction, the fraction will be multiplied by the `base_size` to get the length in pixels. - /// - /// # Arguments - /// - /// * `base_size` - The base size in `AbsoluteLength` to which the fraction will be applied. - /// * `rem_size` - The size of one rem in pixels, used to convert rems to pixels. - /// - /// # Returns - /// - /// Returns the `DefiniteLength` as `Pixels`. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{DefiniteLength, AbsoluteLength, Pixels, px, rems}; - /// let length_in_pixels = DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0))); - /// let length_in_rems = DefiniteLength::Absolute(AbsoluteLength::Rems(rems(2.0))); - /// let length_as_fraction = DefiniteLength::Fraction(0.5); - /// let base_size = AbsoluteLength::Pixels(px(100.0)); - /// let rem_size = px(16.0); - /// - /// assert_eq!(length_in_pixels.to_pixels(base_size, rem_size), Pixels::from(42.0)); - /// assert_eq!(length_in_rems.to_pixels(base_size, rem_size), Pixels::from(32.0)); - /// assert_eq!(length_as_fraction.to_pixels(base_size, rem_size), Pixels::from(50.0)); - /// ``` - pub fn to_pixels(self, base_size: AbsoluteLength, rem_size: Pixels) -> Pixels { - match self { - DefiniteLength::Absolute(size) => size.to_pixels(rem_size), - DefiniteLength::Fraction(fraction) => match base_size { - AbsoluteLength::Pixels(px) => px * fraction, - AbsoluteLength::Rems(rems) => rems * rem_size * fraction, - }, - } - } -} - -impl Debug for DefiniteLength { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - Display::fmt(self, f) - } -} - -impl Display for DefiniteLength { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - DefiniteLength::Absolute(length) => write!(f, "{length}"), - DefiniteLength::Fraction(fraction) => write!(f, "{}%", (fraction * 100.0) as i32), - } - } -} - -const EXPECTED_DEFINITE_LENGTH: &str = "expected number with 'px', 'rem', or '%' suffix"; - -impl TryFrom<&'_ str> for DefiniteLength { - type Error = anyhow::Error; - - fn try_from(value: &'_ str) -> Result { - if let Some(percentage) = value.strip_suffix('%') { - let fraction: f32 = percentage.parse::().with_context(|| { - format!("invalid DefiniteLength '{value}', expected {EXPECTED_DEFINITE_LENGTH}") - })?; - Ok(DefiniteLength::Fraction(fraction / 100.0)) - } else if let Ok(absolute_length) = value.try_into() { - Ok(DefiniteLength::Absolute(absolute_length)) - } else { - Err(anyhow!( - "invalid DefiniteLength '{value}', expected {EXPECTED_DEFINITE_LENGTH}" - )) - } - } -} - -impl JsonSchema for DefiniteLength { - fn schema_name() -> Cow<'static, str> { - "DefiniteLength".into() - } - - fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema { - json_schema!({ - "type": "string", - "pattern": r"^-?\d+(\.\d+)?(px|rem|%)$" - }) - } -} - -impl<'de> Deserialize<'de> for DefiniteLength { - fn deserialize>(deserializer: D) -> Result { - struct StringVisitor; - - impl de::Visitor<'_> for StringVisitor { - type Value = DefiniteLength; - - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{EXPECTED_DEFINITE_LENGTH}") - } - - fn visit_str(self, value: &str) -> Result { - DefiniteLength::try_from(value).map_err(E::custom) - } - } - - deserializer.deserialize_str(StringVisitor) - } -} - -impl Serialize for DefiniteLength { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&format!("{self}")) - } -} - -impl From for DefiniteLength { - fn from(pixels: Pixels) -> Self { - Self::Absolute(pixels.into()) - } -} - -impl From for DefiniteLength { - fn from(rems: Rems) -> Self { - Self::Absolute(rems.into()) - } -} - -impl From for DefiniteLength { - fn from(length: AbsoluteLength) -> Self { - Self::Absolute(length) - } -} - -impl Default for DefiniteLength { - fn default() -> Self { - Self::Absolute(AbsoluteLength::default()) - } -} - -/// A length that can be defined in pixels, rems, percent of parent, or auto. -#[derive(Clone, Copy, PartialEq)] -pub enum Length { - /// A definite length specified either in pixels, rems, or as a fraction of the parent's size. - Definite(DefiniteLength), - /// An automatic length that is determined by the context in which it is used. - Auto, -} - -impl Debug for Length { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - Display::fmt(self, f) - } -} - -impl Display for Length { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Length::Definite(definite_length) => write!(f, "{}", definite_length), - Length::Auto => write!(f, "auto"), - } - } -} - -const EXPECTED_LENGTH: &str = "expected 'auto' or number with 'px', 'rem', or '%' suffix"; - -impl TryFrom<&'_ str> for Length { - type Error = anyhow::Error; - - fn try_from(value: &'_ str) -> Result { - if value == "auto" { - Ok(Length::Auto) - } else if let Ok(definite_length) = value.try_into() { - Ok(Length::Definite(definite_length)) - } else { - Err(anyhow!( - "invalid Length '{value}', expected {EXPECTED_LENGTH}" - )) - } - } -} - -impl JsonSchema for Length { - fn schema_name() -> Cow<'static, str> { - "Length".into() - } - - fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema { - json_schema!({ - "type": "string", - "pattern": r"^(auto|-?\d+(\.\d+)?(px|rem|%))$" - }) - } -} - -impl<'de> Deserialize<'de> for Length { - fn deserialize>(deserializer: D) -> Result { - struct StringVisitor; - - impl de::Visitor<'_> for StringVisitor { - type Value = Length; - - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{EXPECTED_LENGTH}") - } - - fn visit_str(self, value: &str) -> Result { - Length::try_from(value).map_err(E::custom) - } - } - - deserializer.deserialize_str(StringVisitor) - } -} - -impl Serialize for Length { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&format!("{self}")) - } -} - -/// Constructs a `DefiniteLength` representing a relative fraction of a parent size. -/// -/// This function creates a `DefiniteLength` that is a specified fraction of a parent's dimension. -/// The fraction should be a floating-point number between 0.0 and 1.0, where 1.0 represents 100% of the parent's size. -/// -/// # Arguments -/// -/// * `fraction` - The fraction of the parent's size, between 0.0 and 1.0. -/// -/// # Returns -/// -/// A `DefiniteLength` representing the relative length as a fraction of the parent's size. -pub const fn relative(fraction: f32) -> DefiniteLength { - DefiniteLength::Fraction(fraction) -} - -/// Returns the Golden Ratio, i.e. `~(1.0 + sqrt(5.0)) / 2.0`. -pub const fn phi() -> DefiniteLength { - relative(1.618_034) -} - -/// Constructs a `Rems` value representing a length in rems. -/// -/// # Arguments -/// -/// * `rems` - The number of rems for the length. -/// -/// # Returns -/// -/// A `Rems` representing the specified number of rems. -pub const fn rems(rems: f32) -> Rems { - Rems(rems) -} - -/// Constructs a `Pixels` value representing a length in pixels. -/// -/// # Arguments -/// -/// * `pixels` - The number of pixels for the length. -/// -/// # Returns -/// -/// A `Pixels` representing the specified number of pixels. -pub const fn px(pixels: f32) -> Pixels { - Pixels(pixels) -} - -/// Returns a `Length` representing an automatic length. -/// -/// The `auto` length is often used in layout calculations where the length should be determined -/// by the layout context itself rather than being explicitly set. This is commonly used in CSS -/// for properties like `width`, `height`, `margin`, `padding`, etc., where `auto` can be used -/// to instruct the layout engine to calculate the size based on other factors like the size of the -/// container or the intrinsic size of the content. -/// -/// # Returns -/// -/// A `Length` variant set to `Auto`. -pub const fn auto() -> Length { - Length::Auto -} - -impl From for Length { - fn from(pixels: Pixels) -> Self { - Self::Definite(pixels.into()) - } -} - -impl From for Length { - fn from(rems: Rems) -> Self { - Self::Definite(rems.into()) - } -} - -impl From for Length { - fn from(length: DefiniteLength) -> Self { - Self::Definite(length) - } -} - -impl From for Length { - fn from(length: AbsoluteLength) -> Self { - Self::Definite(length.into()) - } -} - -impl Default for Length { - fn default() -> Self { - Self::Definite(DefiniteLength::default()) - } -} - -impl From<()> for Length { - fn from(_: ()) -> Self { - Self::Definite(DefiniteLength::default()) - } -} - -/// A location in a grid layout. -#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, JsonSchema, Default)] -pub struct GridLocation { - /// The rows this item uses within the grid. - pub row: Range, - /// The columns this item uses within the grid. - pub column: Range, -} - -/// The placement of an item within a grid layout's column or row. -#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, JsonSchema, Default)] -pub enum GridPlacement { - /// The grid line index to place this item. - Line(i16), - /// The number of grid lines to span. - Span(u16), - /// Automatically determine the placement, equivalent to Span(1) - #[default] - Auto, -} - -impl From for taffy::GridPlacement { - fn from(placement: GridPlacement) -> Self { - match placement { - GridPlacement::Line(index) => taffy::GridPlacement::from_line_index(index), - GridPlacement::Span(span) => taffy::GridPlacement::from_span(span), - GridPlacement::Auto => taffy::GridPlacement::Auto, - } - } -} - -/// Provides a trait for types that can calculate half of their value. -/// -/// The `Half` trait is used for types that can be evenly divided, returning a new instance of the same type -/// representing half of the original value. This is commonly used for types that represent measurements or sizes, -/// such as lengths or pixels, where halving is a frequent operation during layout calculations or animations. -pub trait Half { - /// Returns half of the current value. - /// - /// # Returns - /// - /// A new instance of the implementing type, representing half of the original value. - fn half(&self) -> Self; -} - -impl Half for i32 { - fn half(&self) -> Self { - self / 2 - } -} - -impl Half for f32 { - fn half(&self) -> Self { - self / 2. - } -} - -impl Half for DevicePixels { - fn half(&self) -> Self { - Self(self.0 / 2) - } -} - -impl Half for ScaledPixels { - fn half(&self) -> Self { - Self(self.0 / 2.) - } -} - -impl Half for Pixels { - fn half(&self) -> Self { - Self(self.0 / 2.) - } -} - -impl Half for Rems { - fn half(&self) -> Self { - Self(self.0 / 2.) - } -} - -/// A trait for checking if a value is zero. -/// -/// This trait provides a method to determine if a value is considered to be zero. -/// It is implemented for various numeric and length-related types where the concept -/// of zero is applicable. This can be useful for comparisons, optimizations, or -/// determining if an operation has a neutral effect. -pub trait IsZero { - /// Determines if the value is zero. - /// - /// # Returns - /// - /// Returns `true` if the value is zero, `false` otherwise. - fn is_zero(&self) -> bool; -} - -impl IsZero for DevicePixels { - fn is_zero(&self) -> bool { - self.0 == 0 - } -} - -impl IsZero for ScaledPixels { - fn is_zero(&self) -> bool { - self.0 == 0. - } -} - -impl IsZero for Pixels { - fn is_zero(&self) -> bool { - self.0 == 0. - } -} - -impl IsZero for Rems { - fn is_zero(&self) -> bool { - self.0 == 0. - } -} - -impl IsZero for AbsoluteLength { - fn is_zero(&self) -> bool { - match self { - AbsoluteLength::Pixels(pixels) => pixels.is_zero(), - AbsoluteLength::Rems(rems) => rems.is_zero(), - } - } -} - -impl IsZero for DefiniteLength { - fn is_zero(&self) -> bool { - match self { - DefiniteLength::Absolute(length) => length.is_zero(), - DefiniteLength::Fraction(fraction) => *fraction == 0., - } - } -} - -impl IsZero for Length { - fn is_zero(&self) -> bool { - match self { - Length::Definite(length) => length.is_zero(), - Length::Auto => false, - } - } -} - -impl IsZero for Point { - fn is_zero(&self) -> bool { - self.x.is_zero() && self.y.is_zero() - } -} - -impl IsZero for Size -where - T: IsZero + Clone + Debug + Default + PartialEq, -{ - fn is_zero(&self) -> bool { - self.width.is_zero() || self.height.is_zero() - } -} - -impl IsZero for Bounds { - fn is_zero(&self) -> bool { - self.size.is_zero() - } -} - -impl IsZero for Corners -where - T: IsZero + Clone + Debug + Default + PartialEq, -{ - fn is_zero(&self) -> bool { - self.top_left.is_zero() - && self.top_right.is_zero() - && self.bottom_right.is_zero() - && self.bottom_left.is_zero() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_bounds_intersects() { - let bounds1 = Bounds { - origin: Point { x: 0.0, y: 0.0 }, - size: Size { - width: 5.0, - height: 5.0, - }, - }; - let bounds2 = Bounds { - origin: Point { x: 4.0, y: 4.0 }, - size: Size { - width: 5.0, - height: 5.0, - }, - }; - let bounds3 = Bounds { - origin: Point { x: 10.0, y: 10.0 }, - size: Size { - width: 5.0, - height: 5.0, - }, - }; - - // Test Case 1: Intersecting bounds - assert!(bounds1.intersects(&bounds2)); - - // Test Case 2: Non-Intersecting bounds - assert!(!bounds1.intersects(&bounds3)); - - // Test Case 3: Bounds intersecting with themselves - assert!(bounds1.intersects(&bounds1)); - } -} diff --git a/crates/gpui_pre/src/gestures.rs b/crates/gpui_pre/src/gestures.rs deleted file mode 100644 index ec61157..0000000 --- a/crates/gpui_pre/src/gestures.rs +++ /dev/null @@ -1,2211 +0,0 @@ -//! Touch gesture recognition vocabulary. -//! -//! GPUI recognizes gestures from raw [`TouchEvent`](crate::TouchEvent)s in a -//! single, portable arena in gpui core: recognizers compete for in-flight -//! touches, winners claim them, and losers are cancelled. Recognized gestures -//! are surfaced through *existing* semantic events wherever possible, a tap -//! becomes [`ClickEvent::Touch`](crate::ClickEvent), a pan becomes -//! [`ScrollWheelEvent`](crate::ScrollWheelEvent)s carrying a -//! [`TouchPhase`](crate::TouchPhase), and a pinch becomes -//! [`PinchEvent`](crate::PinchEvent)s — so components written against -//! `on_click` and scroll containers work untouched on mobile. - -use std::collections::VecDeque; -use std::mem; -use std::time::Duration; - -use scheduler::Instant; -use smallvec::SmallVec; - -use crate::{ - Axis, GestureEvent, InputEvent, IsZero, Modifiers, MouseButton, MouseDownEvent, MouseEvent, - MouseUpEvent, Pixels, PlatformInput, Point, ScrollDelta, ScrollWheelEvent, TouchEvent, TouchId, - TouchPhase, point, px, seal::Sealed, -}; - -const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28); - -fn dominant_axis(delta: Point) -> Axis { - if delta.x.abs() <= delta.y.abs() { - Axis::Vertical - } else { - Axis::Horizontal - } -} - -fn lock_delta_to_axis(delta: &mut Point, axis: Axis) { - match axis { - Axis::Vertical => delta.x = Pixels::ZERO, - Axis::Horizontal => delta.y = Pixels::ZERO, - } -} - -fn movements_oppose(left: Point, right: Point) -> bool { - f32::from(left.x) * f32::from(right.x) + f32::from(left.y) * f32::from(right.y) < 0. -} - -/// Tracks the dominant axis across the events in a scroll gesture. -#[derive(Clone, Copy, Debug, Default)] -pub struct OngoingScroll { - last_event: Option, - axis: Option, -} - -impl OngoingScroll { - /// Filters the given delta to the dominant axis of the current scroll gesture. - /// - /// Gestures are delimited by their touch phase when available, with a timeout - /// fallback for platforms that only emit [`TouchPhase::Moved`]. - pub fn filter(&mut self, delta: &mut Point, touch_phase: TouchPhase) { - self.filter_at(delta, touch_phase, Instant::now()) - } - - fn filter_at(&mut self, delta: &mut Point, touch_phase: TouchPhase, now: Instant) { - const UNLOCK_PERCENT: f32 = 1.9; - const UNLOCK_LOWER_BOUND: Pixels = px(6.); - - if matches!(touch_phase, TouchPhase::Ended | TouchPhase::Cancelled) { - self.last_event = None; - self.axis = None; - return; - } - - let x = delta.x.abs(); - let y = delta.y.abs(); - if x.is_zero() && y.is_zero() { - if touch_phase == TouchPhase::Started { - self.last_event = None; - self.axis = None; - } - return; - } - - let starts_new_gesture = touch_phase == TouchPhase::Started - || self - .last_event - .is_none_or(|last_event| now.duration_since(last_event) >= SCROLL_EVENT_SEPARATION); - let mut axis = self.axis; - if starts_new_gesture { - axis = Some(dominant_axis(*delta)); - } else if x.max(y) >= UNLOCK_LOWER_BOUND { - match axis { - Some(Axis::Vertical) if x > y && x >= y * UNLOCK_PERCENT => { - axis = None; - } - Some(Axis::Horizontal) if y > x && y >= x * UNLOCK_PERCENT => { - axis = None; - } - _ => {} - } - } - - self.last_event = Some(now); - self.axis = axis; - if let Some(axis) = axis { - lock_delta_to_axis(delta, axis); - } - } -} - -/// Feel constants consumed by gesture recognizers. Provided on a best-effort -/// basis, depending on each platform's support, defaulting to GPUI's own -/// (iOS flavored) values -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct GestureTuning { - /// Distance a touch may travel before it stops being a potential tap and - /// becomes a pan/drag. - pub touch_slop: Pixels, - /// Maximum interval between taps for them to accumulate a tap count. - pub multi_tap_interval: Duration, - /// Maximum distance between taps for them to accumulate a tap count. - pub multi_tap_slop: Pixels, - /// How long a touch must remain within [`Self::touch_slop`] to be - /// recognized as a long press. - pub long_press_duration: Duration, - /// How scroll momentum decelerates after a fling. - pub scroll_physics: ScrollPhysics, - /// Minimum release velocity, in pixels per second, required to start - /// scroll momentum. - pub min_fling_velocity: f32, -} - -impl Default for GestureTuning { - fn default() -> Self { - Self { - touch_slop: px(8.), - multi_tap_interval: Duration::from_millis(400), - multi_tap_slop: px(16.), - long_press_duration: Duration::from_millis(500), - scroll_physics: ScrollPhysics::ios(), - min_fling_velocity: 50., - } - } -} - -/// How free scrolling decelerates after a fling. -/// -/// This models deceleration only. Boundary behavior — bouncing, edge glow, -/// clamping — is the scroll container's policy: the container is the one that -/// knows its extents. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum ScrollPhysics { - /// Exponential velocity decay, the `UIScrollView` model: - /// `velocity(t) = v₀ · decay_per_msᵐˢ`. - Exponential { - /// Per-millisecond velocity decay factor. `UIScrollView`'s normal - /// deceleration rate is `0.998`. - decay_per_ms: f32, - }, - /// The friction spline of Android's `OverScroller`: fling duration and - /// distance follow a logarithmic deceleration law, and progress along - /// the fling follows a cubic-Bezier ease-out curve. Transcribed from - /// AOSP's `SplineOverScroller` (Apache-2.0). - FrictionSpline { - /// The scroll friction coefficient; - /// `ViewConfiguration.getScrollFriction()` is `0.015` on Android. - friction: f32, - /// Pixels per physical inch of the display, in the coordinate space - /// the fling runs in. Android folds display density into its - /// deceleration coefficient, so the same finger speed flings - /// further in pixels on a denser screen. - pixels_per_inch: f32, - }, -} - -impl ScrollPhysics { - /// iOS scroll feel: `UIScrollView`'s normal deceleration rate. - pub fn ios() -> Self { - Self::Exponential { - decay_per_ms: 0.998, - } - } - - /// Android scroll feel: `OverScroller` with stock friction, at Android's - /// nominal density of 160 density-independent pixels per inch — the - /// right pairing when fling distances are in logical pixels. Platforms - /// that fling in physical pixels, or know the display's true density in - /// their logical space, should construct - /// [`ScrollPhysics::FrictionSpline`] directly. - pub fn android() -> Self { - Self::FrictionSpline { - friction: 0.015, - pixels_per_inch: 160., - } - } - - /// How long a fling released at `speed` pixels per second coasts before - /// it stops. - fn fling_duration(self, speed: f32) -> Duration { - match self { - Self::Exponential { decay_per_ms } => { - if speed <= MOMENTUM_STOP_VELOCITY { - return Duration::ZERO; - } - let milliseconds = (MOMENTUM_STOP_VELOCITY / speed).ln() / decay_per_ms.ln(); - Duration::from_secs_f32(milliseconds / 1000.) - } - Self::FrictionSpline { - friction, - pixels_per_inch, - } => { - if speed <= 0. { - return Duration::ZERO; - } - let deceleration = friction_spline::deceleration(speed, friction, pixels_per_inch); - let seconds = (deceleration / (friction_spline::deceleration_rate() - 1.)).exp(); - Duration::from_secs_f64(seconds) - } - } - } - - /// Distance traveled `elapsed` into a fling released at `speed` pixels - /// per second, in pixels along the fling direction. Evaluated in closed - /// form so the trajectory is independent of tick timing. - fn fling_distance(self, speed: f32, elapsed: Duration) -> f32 { - let duration = self.fling_duration(speed); - if duration.is_zero() { - return 0.; - } - let elapsed = elapsed.min(duration); - match self { - Self::Exponential { decay_per_ms } => { - // ∫₀ᵗ v₀·kᵐˢ dms, with speed converted to pixels per - // millisecond. - let milliseconds = elapsed.as_secs_f32() * 1000.; - (speed / 1000.) * (decay_per_ms.powf(milliseconds) - 1.) / decay_per_ms.ln() - } - Self::FrictionSpline { - friction, - pixels_per_inch, - } => { - let deceleration = friction_spline::deceleration(speed, friction, pixels_per_inch); - let rate = friction_spline::deceleration_rate(); - let total_distance = friction as f64 - * friction_spline::physical_coefficient(pixels_per_inch) - * (rate / (rate - 1.) * deceleration).exp(); - let progress = elapsed.as_secs_f64() / duration.as_secs_f64(); - total_distance as f32 * friction_spline::distance_coefficient(progress as f32) - } - } - } -} - -/// The fling model of Android's `OverScroller.SplineOverScroller`, -/// transcribed from AOSP (Apache-2.0). `SPLINE_TIME`, which AOSP uses for -/// programmatic scroll animations rather than flings, is intentionally not -/// transcribed. -mod friction_spline { - use std::sync::LazyLock; - - const NB_SAMPLES: usize = 100; - const INFLEXION: f32 = 0.35; - const START_TENSION: f32 = 0.5; - const END_TENSION: f32 = 1.0; - const P1: f32 = START_TENSION * INFLEXION; - const P2: f32 = 1.0 - END_TENSION * (1.0 - INFLEXION); - - /// Android's `DECELERATION_RATE`: `ln(0.78) / ln(0.9)`. - pub(super) fn deceleration_rate() -> f64 { - 0.78f64.ln() / 0.9f64.ln() - } - - /// `SPLINE_POSITION` from AOSP's static initializer: fractional fling - /// distance sampled at 100 evenly spaced fractions of the fling - /// duration, from a cubic Bezier with control points shaped by - /// `INFLEXION` and the start/end tensions. - static SPLINE_POSITION: LazyLock<[f32; NB_SAMPLES + 1]> = LazyLock::new(|| { - let mut spline_position = [0f32; NB_SAMPLES + 1]; - let mut x_min = 0f32; - for (i, sample) in spline_position.iter_mut().take(NB_SAMPLES).enumerate() { - let alpha = i as f32 / NB_SAMPLES as f32; - let mut x_max = 1f32; - let (x, coefficient) = loop { - let x = x_min + (x_max - x_min) / 2.; - let coefficient = 3. * x * (1. - x); - let time = coefficient * ((1. - x) * P1 + x * P2) + x * x * x; - if (time - alpha).abs() < 1e-5 { - break (x, coefficient); - } - if time > alpha { - x_max = x; - } else { - x_min = x; - } - }; - *sample = coefficient * ((1. - x) * START_TENSION + x) + x * x * x; - } - spline_position[NB_SAMPLES] = 1.; - spline_position - }); - - /// `SensorManager.GRAVITY_EARTH · 39.37 in/m · ppi · 0.84`, AOSP's - /// `mPhysicalCoeff`: gravity expressed in pixels, times an empirical - /// "look and feel" tuning factor. - pub(super) fn physical_coefficient(pixels_per_inch: f32) -> f64 { - 9.80665 * 39.37 * pixels_per_inch as f64 * 0.84 - } - - /// AOSP's `getSplineDeceleration`. - pub(super) fn deceleration(speed: f32, friction: f32, pixels_per_inch: f32) -> f64 { - (INFLEXION as f64 * speed as f64 - / (friction as f64 * physical_coefficient(pixels_per_inch))) - .ln() - } - - /// Fraction of the total fling distance covered at fraction `time` of - /// the fling duration: table lookup plus linear interpolation, as in - /// `SplineOverScroller.update`. - pub(super) fn distance_coefficient(time: f32) -> f32 { - if time >= 1. { - return 1.; - } - let index = ((NB_SAMPLES as f32 * time) as usize).min(NB_SAMPLES - 1); - let time_lower = index as f32 / NB_SAMPLES as f32; - let time_upper = (index + 1) as f32 / NB_SAMPLES as f32; - let distance_lower = SPLINE_POSITION[index]; - let distance_upper = SPLINE_POSITION[index + 1]; - let velocity_coefficient = (distance_upper - distance_lower) / (time_upper - time_lower); - distance_lower + (time - time_lower) * velocity_coefficient - } - - #[cfg(test)] - pub(super) fn bezier_time_and_position(parameter: f32) -> (f32, f32) { - let coefficient = 3. * parameter * (1. - parameter); - let cubed = parameter * parameter * parameter; - ( - coefficient * ((1. - parameter) * P1 + parameter * P2) + cubed, - coefficient * ((1. - parameter) * START_TENSION + parameter) + cubed, - ) - } - - #[cfg(test)] - pub(super) fn spline_position_samples() -> &'static [f32; NB_SAMPLES + 1] { - &SPLINE_POSITION - } -} - -/// The set of gesture kinds that participate in recognition. -/// -/// Used by [`PlatformGestures::native_recognizers`] to declare which gestures -/// the platform recognizes natively rather than leaving to gpui core's -/// portable recognizers. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct GestureKinds { - /// Tap (and multi-tap), surfaced as [`ClickEvent::Touch`](crate::ClickEvent). - pub tap: bool, - /// Long press, surfaced as [`LongPressEvent`]. - pub long_press: bool, - /// Pan/scroll (including fling momentum), surfaced as - /// [`ScrollWheelEvent`](crate::ScrollWheelEvent)s. - pub pan: bool, - /// Pinch to zoom, surfaced as [`PinchEvent`](crate::PinchEvent)s. - pub pinch: bool, -} - -impl GestureKinds { - /// No gestures; gpui core's portable recognizers handle everything. - pub const NONE: Self = Self { - tap: false, - long_press: false, - pan: false, - pinch: false, - }; - - /// All gesture kinds. - pub const ALL: Self = Self { - tap: true, - long_press: true, - pan: true, - pinch: true, - }; -} - -/// A direct touch drag claimed by an element before touch input becomes a tap, -/// long press, or scrolling gesture. -#[derive(Clone, Debug)] -pub struct TouchDragEvent { - /// The phase of the touch drag. - pub phase: TouchPhase, - /// The position where the touch started. - pub start_position: Point, - /// The touch's current position. - pub position: Point, -} - -impl Sealed for TouchDragEvent {} -impl InputEvent for TouchDragEvent { - fn to_platform_input(self) -> PlatformInput { - PlatformInput::TouchDrag(self) - } -} -impl GestureEvent for TouchDragEvent {} -impl MouseEvent for TouchDragEvent {} - -/// A phased long-press gesture recognized from a touch. -#[derive(Clone, Debug)] -pub struct LongPressEvent { - /// The phase of the long press. - pub phase: TouchPhase, - /// The position where the touch started. - pub start_position: Point, - /// The touch's current position. - pub position: Point, -} - -impl Default for LongPressEvent { - fn default() -> Self { - Self { - phase: TouchPhase::Started, - start_position: Point::default(), - position: Point::default(), - } - } -} - -impl Sealed for LongPressEvent {} -impl InputEvent for LongPressEvent { - fn to_platform_input(self) -> PlatformInput { - PlatformInput::LongPress(self) - } -} -impl GestureEvent for LongPressEvent {} -impl MouseEvent for LongPressEvent {} - -/// Platform gesture recognition services. -/// -/// If your mobile platform supports native gesture recognition, use this -/// to share it with GPUI. -pub trait PlatformGestures { - /// Feel constants for the portable recognizers on this platform. - fn tuning(&self) -> GestureTuning { - GestureTuning::default() - } - - /// The gesture kinds this platform recognizes natively. - fn native_recognizers(&self) -> GestureKinds { - GestureKinds::NONE - } -} - -/// A no-op [`PlatformGestures`] implementation: no native recognizers and -/// default tuning. Suitable for desktop platforms and tests. -pub struct NullPlatformGestures; - -impl PlatformGestures for NullPlatformGestures {} - -/// Ceiling on recognized fling velocity, in pixels per second (matches -/// Flutter's `kMaxFlingVelocity`). -const MAX_FLING_VELOCITY: f32 = 8000.; - -/// Momentum below this speed, in pixels per second, is imperceptible. The -/// exponential model, which never mathematically stops, treats reaching this -/// speed as the end of the fling. (The friction spline has a finite duration -/// of its own.) -const MOMENTUM_STOP_VELOCITY: f32 = 10.; - -/// How far back the release-velocity estimate looks. Samples older than this -/// reflect an earlier part of the gesture, not the speed at release. -const VELOCITY_WINDOW: Duration = Duration::from_millis(100); - -/// A pause between samples longer than this means the finger stopped: -/// anything before the pause describes an earlier motion, not the release -/// (Flutter's `kAssumePointerMoveStoppedMilliseconds`). Touch hardware -/// reports movement every 8–16ms while the finger is in motion. -const VELOCITY_ASSUME_STOPPED_GAP: Duration = Duration::from_millis(40); - -const VELOCITY_MAX_SAMPLES: usize = 20; - -/// The portable recognizer behind raw touch input: it watches the -/// [`TouchEvent`] stream for one touch at a time and resolves it into either -/// a tap or a pan, following the competition model described in the module -/// docs. Pans continue into post-release momentum when the touch lifts at -/// speed; the window drives that phase through [`Self::tick_momentum`]. -/// -/// Taps are currently surfaced as synthesized mouse presses rather than -/// [`ClickEvent::Touch`](crate::ClickEvent), which keeps every existing -/// mouse-driven behavior (click listeners, caret placement, double-tap -/// selection) working before elements grow a direct tap-delivery path. -/// Pinch recognition is not implemented yet, and additional touches are ignored -/// while one is being recognized. -pub(crate) struct TouchGestureRecognizer { - tuning: GestureTuning, - state: TouchGestureState, - momentum: Option, - last_tap: Option, -} - -/// A semantic event recognized from raw touches, ready to dispatch through -/// the window's existing input paths. -#[derive(Debug)] -pub(crate) enum RecognizedTouchGesture { - /// One step of a pan (or of its post-release momentum), delivered to - /// scroll listeners at the pan's starting position. - Scroll(ScrollWheelEvent), - /// A recognized tap, delivered as a synthesized mouse press and release. - Tap { - down: MouseDownEvent, - up: MouseUpEvent, - }, - TouchDrag(TouchDragEvent), - LongPress(LongPressEvent), -} - -enum TouchGestureState { - Idle, - /// The touch is still within `touch_slop` of where it started: it can - /// still resolve into either a tap or a pan. - Pending { - touch: ActiveTouch, - deadline: Instant, - long_press_offered: bool, - touch_drag_offered: bool, - }, - /// The touch exceeded `touch_slop`: it is a pan until it ends, and its - /// movement flows out as scroll events. - Panning { - touch: ActiveTouch, - axis: Axis, - }, - LongPressing(ActiveTouch), - TouchDragging(ActiveTouch), -} - -struct ActiveTouch { - id: TouchId, - start_position: Point, - /// The latest raw position reported for this touch. - last_position: Point, - /// The position pan output has scrolled to so far. While panning this - /// may run ahead of the raw touch by the event's predicted position; - /// the release event targets the raw position again, so the total - /// scrolled distance always converges to the finger's actual travel. - emitted_position: Point, - /// Retained across stationary samples so prediction corrections cannot - /// reverse a pan when integer browser coordinates repeat. - last_movement: Point, - velocity_tracker: VelocityTracker, -} - -struct CompletedTap { - position: Point, - time: Instant, - count: usize, -} - -/// One fling in progress. The trajectory is a closed-form curve of elapsed -/// time — each tick evaluates it and emits the increment — so the fling is -/// exactly frame-rate independent: a stalled frame simply resumes further -/// along the same curve. -struct Momentum { - /// Where the pan started; synthesized scroll events keep hit-testing - /// there so momentum stays with the container the gesture began on. - position: Point, - /// Unit vector of the release velocity. - direction: Point, - axis: Axis, - /// Release speed in pixels per second. - speed: f32, - started_at: Instant, - duration: Duration, - /// Distance already emitted along `direction`, in pixels. - emitted_distance: f32, -} - -impl TouchGestureRecognizer { - pub(crate) fn new(tuning: GestureTuning) -> Self { - Self { - tuning, - state: TouchGestureState::Idle, - momentum: None, - last_tap: None, - } - } - - pub(crate) fn handle_event( - &mut self, - event: &TouchEvent, - ) -> SmallVec<[RecognizedTouchGesture; 2]> { - self.handle_event_at(event, Instant::now()) - } - - fn handle_event_at( - &mut self, - event: &TouchEvent, - now: Instant, - ) -> SmallVec<[RecognizedTouchGesture; 2]> { - let mut recognized = SmallVec::new(); - match event.phase { - TouchPhase::Started => { - let caught_fling = if let Some(momentum) = self.momentum.take() { - recognized.push(RecognizedTouchGesture::Scroll(scroll_event( - momentum.position, - Point::default(), - TouchPhase::Ended, - ))); - Some(momentum.axis) - } else { - None - }; - if matches!(self.state, TouchGestureState::Idle) { - let mut velocity_tracker = VelocityTracker::default(); - velocity_tracker.push(now, event.position); - let touch = ActiveTouch { - id: event.id, - start_position: event.position, - last_position: event.position, - emitted_position: event.position, - last_movement: Point::default(), - velocity_tracker, - }; - if let Some(axis) = caught_fling { - // A touch that catches a fling is a drag from the - // first pixel: waiting out the slop would freeze the - // content mid-scroll and then jump. It can also never - // be a tap; releasing it just leaves the content - // stopped, as on Android and iOS. - recognized.push(RecognizedTouchGesture::Scroll(scroll_event( - touch.start_position, - Point::default(), - TouchPhase::Started, - ))); - self.state = TouchGestureState::Panning { touch, axis }; - } else { - self.state = TouchGestureState::Pending { - touch, - deadline: now + self.tuning.long_press_duration, - long_press_offered: false, - touch_drag_offered: false, - }; - } - } - } - TouchPhase::Moved => match mem::replace(&mut self.state, TouchGestureState::Idle) { - TouchGestureState::Pending { - mut touch, - deadline, - long_press_offered, - touch_drag_offered, - } if touch.id == event.id => { - touch.velocity_tracker.push(now, event.position); - touch.last_position = event.position; - let accumulated = event.position - touch.start_position; - if accumulated.magnitude() > f64::from(self.tuning.touch_slop) { - // Carry the full movement so far into the first scroll - // step: the content catches up to the finger instead - // of losing the slop distance. - let mut target = event.predicted_position.unwrap_or(event.position); - let axis = dominant_axis(accumulated); - let mut delta = target - touch.start_position; - lock_delta_to_axis(&mut delta, axis); - touch.last_movement = accumulated; - lock_delta_to_axis(&mut touch.last_movement, axis); - if movements_oppose(delta, touch.last_movement) { - target = event.position; - delta = accumulated; - lock_delta_to_axis(&mut delta, axis); - } - touch.emitted_position = target; - recognized.push(RecognizedTouchGesture::Scroll(scroll_event( - touch.start_position, - delta, - TouchPhase::Started, - ))); - self.state = TouchGestureState::Panning { touch, axis }; - } else { - self.state = TouchGestureState::Pending { - touch, - deadline, - long_press_offered, - touch_drag_offered, - }; - } - } - TouchGestureState::Panning { mut touch, axis } if touch.id == event.id => { - let mut raw_delta = event.position - touch.last_position; - lock_delta_to_axis(&mut raw_delta, axis); - if raw_delta != Point::default() { - touch.last_movement = raw_delta; - } - touch.velocity_tracker.push(now, event.position); - touch.last_position = event.position; - let mut target = event.predicted_position.unwrap_or(event.position); - let mut delta = target - touch.emitted_position; - lock_delta_to_axis(&mut delta, axis); - // Prediction error must not reverse content while the raw - // touch still advances. Fall back to the raw position so a - // real finger reversal remains responsive. - if movements_oppose(delta, touch.last_movement) { - target = event.position; - delta = target - touch.emitted_position; - lock_delta_to_axis(&mut delta, axis); - if movements_oppose(delta, touch.last_movement) { - target = touch.emitted_position; - delta = Point::default(); - } - } - touch.emitted_position = target; - recognized.push(RecognizedTouchGesture::Scroll(scroll_event( - touch.start_position, - delta, - TouchPhase::Moved, - ))); - self.state = TouchGestureState::Panning { touch, axis }; - } - TouchGestureState::LongPressing(mut touch) if touch.id == event.id => { - touch.last_position = event.position; - recognized.push(RecognizedTouchGesture::LongPress(LongPressEvent { - phase: TouchPhase::Moved, - start_position: touch.start_position, - position: event.position, - })); - self.state = TouchGestureState::LongPressing(touch); - } - TouchGestureState::TouchDragging(mut touch) if touch.id == event.id => { - touch.last_position = event.position; - recognized.push(RecognizedTouchGesture::TouchDrag(TouchDragEvent { - phase: TouchPhase::Moved, - start_position: touch.start_position, - position: event.position, - })); - self.state = TouchGestureState::TouchDragging(touch); - } - other => self.state = other, - }, - TouchPhase::Ended => match mem::replace(&mut self.state, TouchGestureState::Idle) { - TouchGestureState::Pending { touch, .. } if touch.id == event.id => { - let tap_count = match &self.last_tap { - Some(tap) - if now.duration_since(tap.time) <= self.tuning.multi_tap_interval - && (event.position - tap.position).magnitude() - <= f64::from(self.tuning.multi_tap_slop) => - { - tap.count + 1 - } - _ => 1, - }; - self.last_tap = Some(CompletedTap { - position: event.position, - time: now, - count: tap_count, - }); - recognized.push(RecognizedTouchGesture::Tap { - down: MouseDownEvent { - button: MouseButton::Left, - position: event.position, - modifiers: Modifiers::default(), - click_count: tap_count, - first_mouse: false, - }, - up: MouseUpEvent { - button: MouseButton::Left, - position: event.position, - modifiers: Modifiers::default(), - click_count: tap_count, - }, - }); - } - TouchGestureState::Panning { touch, axis } if touch.id == event.id => { - // The release deliberately contributes no velocity - // sample: it usually repeats the last movement's position - // with a later timestamp, which would dilute the - // estimate. But a release long after the last movement - // means the finger had already stopped, so nothing - // flings. - let finger_stopped = - touch - .velocity_tracker - .latest_sample_time() - .is_none_or(|latest| { - now.duration_since(latest) > VELOCITY_ASSUME_STOPPED_GAP - }); - let mut velocity = if finger_stopped { - Point::default() - } else { - touch.velocity_tracker.velocity() - }; - match axis { - Axis::Vertical => velocity.x = 0., - Axis::Horizontal => velocity.y = 0., - } - let speed = (velocity.x.powi(2) + velocity.y.powi(2)).sqrt(); - let mut release_delta = event.position - touch.emitted_position; - lock_delta_to_axis(&mut release_delta, axis); - if speed >= self.tuning.min_fling_velocity { - let direction = point(velocity.x / speed, velocity.y / speed); - let speed = speed.min(MAX_FLING_VELOCITY); - let duration = self.tuning.scroll_physics.fling_duration(speed); - if !duration.is_zero() { - let total_distance = - self.tuning.scroll_physics.fling_distance(speed, duration); - // Prediction may have left the content ahead of - // the raw release position. Emitting that - // correction here would visibly snap the content - // backwards just as the fling launches, so fold - // it into the fling instead: start the curve - // already advanced by the overshoot, keeping the - // total travel exact while staying monotonic. - let overshoot = -(f32::from(release_delta.x) * direction.x - + f32::from(release_delta.y) * direction.y); - let emitted_distance = if overshoot > 0. && overshoot < total_distance { - release_delta += - point(px(direction.x * overshoot), px(direction.y * overshoot)); - overshoot - } else { - 0. - }; - self.momentum = Some(Momentum { - position: touch.start_position, - direction, - axis, - speed, - started_at: now, - duration, - emitted_distance, - }); - } - } - recognized.push(RecognizedTouchGesture::Scroll(scroll_event( - touch.start_position, - release_delta, - TouchPhase::Ended, - ))); - } - TouchGestureState::LongPressing(touch) if touch.id == event.id => { - recognized.push(RecognizedTouchGesture::LongPress(LongPressEvent { - phase: TouchPhase::Ended, - start_position: touch.start_position, - position: event.position, - })); - } - TouchGestureState::TouchDragging(touch) if touch.id == event.id => { - recognized.push(RecognizedTouchGesture::TouchDrag(TouchDragEvent { - phase: TouchPhase::Ended, - start_position: touch.start_position, - position: event.position, - })); - } - other => self.state = other, - }, - TouchPhase::Cancelled => match mem::replace(&mut self.state, TouchGestureState::Idle) { - TouchGestureState::Pending { touch, .. } if touch.id == event.id => {} - TouchGestureState::Panning { touch, .. } if touch.id == event.id => { - recognized.push(RecognizedTouchGesture::Scroll(scroll_event( - touch.start_position, - Point::default(), - TouchPhase::Cancelled, - ))); - } - TouchGestureState::LongPressing(touch) if touch.id == event.id => { - recognized.push(RecognizedTouchGesture::LongPress(LongPressEvent { - phase: TouchPhase::Cancelled, - start_position: touch.start_position, - position: event.position, - })); - } - TouchGestureState::TouchDragging(touch) if touch.id == event.id => { - recognized.push(RecognizedTouchGesture::TouchDrag(TouchDragEvent { - phase: TouchPhase::Cancelled, - start_position: touch.start_position, - position: event.position, - })); - } - other => self.state = other, - }, - } - recognized - } - - pub(crate) fn pending_long_press(&self) -> Option<(TouchId, Duration)> { - let TouchGestureState::Pending { - touch, - deadline, - long_press_offered: false, - .. - } = &self.state - else { - return None; - }; - Some((touch.id, deadline.saturating_duration_since(Instant::now()))) - } - - pub(crate) fn offer_long_press(&mut self, id: TouchId) -> Option { - let TouchGestureState::Pending { - touch, - long_press_offered, - .. - } = &mut self.state - else { - return None; - }; - if touch.id != id || *long_press_offered { - return None; - } - *long_press_offered = true; - Some(RecognizedTouchGesture::LongPress(LongPressEvent { - phase: TouchPhase::Started, - start_position: touch.start_position, - position: touch.last_position, - })) - } - - pub(crate) fn resolve_long_press(&mut self, claimed: bool) { - if !claimed { - return; - } - let state = mem::replace(&mut self.state, TouchGestureState::Idle); - self.state = match state { - TouchGestureState::Pending { - touch, - long_press_offered: true, - .. - } => TouchGestureState::LongPressing(touch), - other => other, - }; - } - - pub(crate) fn offer_touch_drag(&mut self, id: TouchId) -> Option { - let TouchGestureState::Pending { - touch, - touch_drag_offered, - .. - } = &mut self.state - else { - return None; - }; - if touch.id != id || *touch_drag_offered { - return None; - } - *touch_drag_offered = true; - Some(RecognizedTouchGesture::TouchDrag(TouchDragEvent { - phase: TouchPhase::Started, - start_position: touch.start_position, - position: touch.last_position, - })) - } - - pub(crate) fn resolve_touch_drag(&mut self, claimed: bool) { - if !claimed { - return; - } - let state = mem::replace(&mut self.state, TouchGestureState::Idle); - self.state = match state { - TouchGestureState::Pending { - touch, - touch_drag_offered: true, - .. - } => TouchGestureState::TouchDragging(touch), - other => other, - }; - } - - pub(crate) fn has_momentum(&self) -> bool { - self.momentum.is_some() - } - - /// Advances post-fling momentum by one frame, returning the scroll step - /// to dispatch, or `None` when no momentum is in progress. The final step - /// carries [`TouchPhase::Ended`] to close the synthetic scroll stream. - pub(crate) fn tick_momentum(&mut self) -> Option { - self.tick_momentum_at(Instant::now()) - } - - fn tick_momentum_at(&mut self, now: Instant) -> Option { - let momentum = self.momentum.as_mut()?; - let elapsed = now.duration_since(momentum.started_at); - let distance = self - .tuning - .scroll_physics - .fling_distance(momentum.speed, elapsed); - // Prediction overshoot can start momentum ahead of its curve. Hold - // that position until the curve catches up instead of stepping back. - let step = (distance - momentum.emitted_distance).max(0.); - momentum.emitted_distance = momentum.emitted_distance.max(distance); - let delta = point( - px(momentum.direction.x * step), - px(momentum.direction.y * step), - ); - let position = momentum.position; - if elapsed >= momentum.duration { - self.momentum = None; - Some(RecognizedTouchGesture::Scroll(scroll_event( - position, - delta, - TouchPhase::Ended, - ))) - } else { - Some(RecognizedTouchGesture::Scroll(scroll_event( - position, - delta, - TouchPhase::Moved, - ))) - } - } -} - -fn scroll_event( - position: Point, - delta: Point, - touch_phase: TouchPhase, -) -> ScrollWheelEvent { - ScrollWheelEvent { - position, - delta: ScrollDelta::Pixels(delta), - modifiers: Modifiers::default(), - touch_phase, - } -} - -/// Estimates the velocity a touch had at its newest sample. -#[derive(Default)] -struct VelocityTracker { - samples: VecDeque<(Instant, Point)>, -} - -impl VelocityTracker { - fn push(&mut self, time: Instant, position: Point) { - self.samples.push_back((time, position)); - while self.samples.len() > VELOCITY_MAX_SAMPLES { - self.samples.pop_front(); - } - } - - fn latest_sample_time(&self) -> Option { - self.samples.back().map(|(time, _)| *time) - } - - /// The velocity at the newest sample, in pixels per second. - /// - /// Fits a second-degree polynomial by least squares over the trailing - /// [`VELOCITY_WINDOW`] and takes its derivative at the newest sample, - /// like Flutter's `VelocityTracker` and Android's `lsq2` strategy. An - /// endpoint difference over the same window would report the window's - /// *average* speed, which for a flick — still accelerating at lift-off — - /// is roughly half the speed the finger actually had at release. - fn velocity(&self) -> Point { - let Some((newest_time, _)) = self.samples.back() else { - return Point::default(); - }; - let mut times_seconds: SmallVec<[f64; VELOCITY_MAX_SAMPLES]> = SmallVec::new(); - let mut horizontal: SmallVec<[f64; VELOCITY_MAX_SAMPLES]> = SmallVec::new(); - let mut vertical: SmallVec<[f64; VELOCITY_MAX_SAMPLES]> = SmallVec::new(); - let mut previous_time = *newest_time; - for (time, position) in self.samples.iter().rev() { - let age = newest_time.duration_since(*time); - if age > VELOCITY_WINDOW - || previous_time.duration_since(*time) > VELOCITY_ASSUME_STOPPED_GAP - { - break; - } - previous_time = *time; - times_seconds.push(-age.as_secs_f64()); - horizontal.push(f64::from(f32::from(position.x))); - vertical.push(f64::from(f32::from(position.y))); - } - - let endpoint_estimate = |values: &[f64]| -> f32 { - let elapsed = -times_seconds.last().copied().unwrap_or(0.); - if elapsed <= f64::EPSILON { - return 0.; - } - ((values.first().copied().unwrap_or(0.) - values.last().copied().unwrap_or(0.)) - / elapsed) as f32 - }; - if times_seconds.len() < 3 { - return point(endpoint_estimate(&horizontal), endpoint_estimate(&vertical)); - } - point( - quadratic_velocity_at_newest(×_seconds, &horizontal).map_or_else( - || endpoint_estimate(&horizontal), - |velocity| velocity as f32, - ), - quadratic_velocity_at_newest(×_seconds, &vertical) - .map_or_else(|| endpoint_estimate(&vertical), |velocity| velocity as f32), - ) - } -} - -/// Least-squares fit of `value = a0 + a1·t + a2·t²` returning `a1`: the -/// fitted curve's velocity at `t = 0`, which callers place at the newest -/// sample. `None` when the samples are too degenerate to fit (all -/// simultaneous, for example). -fn quadratic_velocity_at_newest(times: &[f64], values: &[f64]) -> Option { - let count = times.len() as f64; - let (mut sum_t1, mut sum_t2, mut sum_t3, mut sum_t4) = (0., 0., 0., 0.); - let (mut sum_v, mut sum_vt, mut sum_vt2) = (0., 0., 0.); - for (&time, &value) in times.iter().zip(values) { - let time_squared = time * time; - sum_t1 += time; - sum_t2 += time_squared; - sum_t3 += time_squared * time; - sum_t4 += time_squared * time_squared; - sum_v += value; - sum_vt += value * time; - sum_vt2 += value * time_squared; - } - // Cramer's rule on the 3×3 normal equations, solved for the linear - // coefficient only. - let determinant = count * (sum_t2 * sum_t4 - sum_t3 * sum_t3) - - sum_t1 * (sum_t1 * sum_t4 - sum_t3 * sum_t2) - + sum_t2 * (sum_t1 * sum_t3 - sum_t2 * sum_t2); - if determinant.abs() < 1e-12 { - return None; - } - let linear_determinant = count * (sum_vt * sum_t4 - sum_t3 * sum_vt2) - - sum_v * (sum_t1 * sum_t4 - sum_t3 * sum_t2) - + sum_t2 * (sum_t1 * sum_vt2 - sum_vt * sum_t2); - Some(linear_determinant / determinant) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::point; - - #[test] - fn ongoing_scroll_locks_to_dominant_axis() { - let now = Instant::now(); - let mut ongoing_scroll = OngoingScroll::default(); - let mut horizontal_delta = point(px(10.), px(2.)); - ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now); - assert_eq!(ongoing_scroll.axis, Some(Axis::Horizontal)); - assert_eq!(horizontal_delta, point(px(10.), px(0.))); - - let mut continued_delta = point(px(3.), px(2.)); - ongoing_scroll.filter_at( - &mut continued_delta, - TouchPhase::Moved, - now + Duration::from_millis(1), - ); - assert_eq!(ongoing_scroll.axis, Some(Axis::Horizontal)); - assert_eq!(continued_delta, point(px(3.), px(0.))); - } - - #[test] - fn ongoing_scroll_unlocks_when_direction_changes() { - let now = Instant::now(); - let mut ongoing_scroll = OngoingScroll::default(); - let mut horizontal_delta = point(px(10.), px(2.)); - ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now); - - let mut vertical_delta = point(px(2.), px(10.)); - ongoing_scroll.filter_at( - &mut vertical_delta, - TouchPhase::Moved, - now + Duration::from_millis(1), - ); - assert_eq!(ongoing_scroll.axis, None); - assert_eq!(vertical_delta, point(px(2.), px(10.))); - } - - #[test] - fn ongoing_scroll_starts_new_gesture_at_timeout_boundary() { - let now = Instant::now(); - let mut ongoing_scroll = OngoingScroll::default(); - let mut horizontal_delta = point(px(10.), px(2.)); - ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Moved, now); - - let mut vertical_delta = point(px(2.), px(10.)); - ongoing_scroll.filter_at( - &mut vertical_delta, - TouchPhase::Moved, - now + SCROLL_EVENT_SEPARATION, - ); - assert_eq!(ongoing_scroll.axis, Some(Axis::Vertical)); - assert_eq!(vertical_delta, point(px(0.), px(10.))); - } - - #[test] - fn ongoing_scroll_ignores_zero_delta_and_resets_when_ended() { - let now = Instant::now(); - let mut ongoing_scroll = OngoingScroll::default(); - let mut horizontal_delta = point(px(10.), px(2.)); - ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now); - - let mut zero_delta = Point::default(); - ongoing_scroll.filter_at( - &mut zero_delta, - TouchPhase::Ended, - now + Duration::from_millis(1), - ); - assert_eq!(ongoing_scroll.axis, None); - - let mut vertical_delta = point(px(2.), px(3.)); - ongoing_scroll.filter_at( - &mut vertical_delta, - TouchPhase::Moved, - now + Duration::from_millis(2), - ); - assert_eq!(ongoing_scroll.axis, Some(Axis::Vertical)); - assert_eq!(vertical_delta, point(px(0.), px(3.))); - } - - #[test] - fn ongoing_scroll_ignores_zero_delta_movement() { - let now = Instant::now(); - let mut ongoing_scroll = OngoingScroll::default(); - let mut horizontal_delta = point(px(10.), px(2.)); - ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now); - - let mut zero_delta = Point::default(); - ongoing_scroll.filter_at( - &mut zero_delta, - TouchPhase::Moved, - now + SCROLL_EVENT_SEPARATION, - ); - - let mut vertical_delta = point(px(2.), px(10.)); - ongoing_scroll.filter_at( - &mut vertical_delta, - TouchPhase::Moved, - now + SCROLL_EVENT_SEPARATION, - ); - assert_eq!(ongoing_scroll.axis, Some(Axis::Vertical)); - assert_eq!(vertical_delta, point(px(0.), px(10.))); - } - - #[test] - fn ongoing_scroll_supports_moved_only_platforms() { - let now = Instant::now(); - let mut ongoing_scroll = OngoingScroll::default(); - let mut horizontal_delta = point(px(10.), px(2.)); - ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Moved, now); - assert_eq!(ongoing_scroll.axis, Some(Axis::Horizontal)); - assert_eq!(horizontal_delta, point(px(10.), px(0.))); - } - - #[test] - fn touch_within_slop_resolves_to_tap() { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let now = Instant::now(); - let touch = TouchId(1); - - let recognized = - recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 10., 10.), now); - assert!(recognized.is_empty()); - let recognized = recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Moved, 12., 11.), - now + Duration::from_millis(20), - ); - assert!(recognized.is_empty()); - - let recognized = recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Ended, 12., 11.), - now + Duration::from_millis(60), - ); - let [RecognizedTouchGesture::Tap { down, up }] = recognized.as_slice() else { - panic!("expected tap, got {recognized:?}"); - }; - assert_eq!(down.click_count, 1); - assert_eq!(down.position, point(px(12.), px(11.))); - assert_eq!(up.click_count, 1); - } - - #[test] - fn consecutive_taps_accumulate_tap_count() { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let now = Instant::now(); - - recognizer.handle_event_at(&touch_event(TouchId(1), TouchPhase::Started, 10., 10.), now); - recognizer.handle_event_at( - &touch_event(TouchId(1), TouchPhase::Ended, 10., 10.), - now + Duration::from_millis(40), - ); - - let second_down = now + Duration::from_millis(200); - recognizer.handle_event_at( - &touch_event(TouchId(2), TouchPhase::Started, 14., 10.), - second_down, - ); - let recognized = recognizer.handle_event_at( - &touch_event(TouchId(2), TouchPhase::Ended, 14., 10.), - second_down + Duration::from_millis(40), - ); - let [RecognizedTouchGesture::Tap { down, .. }] = recognized.as_slice() else { - panic!("expected tap, got {recognized:?}"); - }; - assert_eq!(down.click_count, 2); - - let late_down = second_down + Duration::from_secs(2); - recognizer.handle_event_at( - &touch_event(TouchId(3), TouchPhase::Started, 14., 10.), - late_down, - ); - let recognized = recognizer.handle_event_at( - &touch_event(TouchId(3), TouchPhase::Ended, 14., 10.), - late_down + Duration::from_millis(40), - ); - let [RecognizedTouchGesture::Tap { down, .. }] = recognized.as_slice() else { - panic!("expected tap, got {recognized:?}"); - }; - assert_eq!(down.click_count, 1); - } - - #[test] - fn touch_beyond_slop_resolves_to_pan() { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let now = Instant::now(); - let touch = TouchId(1); - - recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 100.), now); - - let recognized = recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Moved, 100., 120.), - now + Duration::from_millis(16), - ); - let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { - panic!("expected scroll, got {recognized:?}"); - }; - assert_eq!(scroll.touch_phase, TouchPhase::Started); - assert_eq!(scroll.position, point(px(100.), px(100.))); - assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(20.))); - - let recognized = recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Moved, 100., 135.), - now + Duration::from_millis(32), - ); - let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { - panic!("expected scroll, got {recognized:?}"); - }; - assert_eq!(scroll.touch_phase, TouchPhase::Moved); - assert_eq!(scroll.position, point(px(100.), px(100.))); - assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(15.))); - - let recognized = recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Ended, 100., 135.), - now + Duration::from_millis(48), - ); - let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { - panic!("expected scroll, got {recognized:?}"); - }; - assert_eq!(scroll.touch_phase, TouchPhase::Ended); - } - - #[test] - fn touch_pan_stays_locked_to_its_initial_dominant_axis() { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let now = Instant::now(); - let touch = TouchId(1); - - recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 100.), now); - - let recognized = recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Moved, 104., 120.), - now + Duration::from_millis(16), - ); - let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { - panic!("expected scroll, got {recognized:?}"); - }; - assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(20.))); - - let recognized = recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Moved, 134., 125.), - now + Duration::from_millis(32), - ); - let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { - panic!("expected scroll, got {recognized:?}"); - }; - assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(5.))); - } - - #[test] - fn touch_pan_locks_to_horizontal_axis() { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let now = Instant::now(); - let touch = TouchId(1); - - recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 100.), now); - - let recognized = recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Moved, 120., 104.), - now + Duration::from_millis(16), - ); - let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { - panic!("expected scroll, got {recognized:?}"); - }; - assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(20.), px(0.))); - } - - #[test] - fn predicted_positions_lead_the_pan_but_totals_converge_on_release() { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let now = Instant::now(); - let touch = TouchId(1); - - recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 100.), now); - - // The first pan step scrolls to the predicted position, not the raw one. - let mut moved = touch_event(touch, TouchPhase::Moved, 100., 120.); - moved.predicted_position = Some(point(px(106.), px(128.))); - let recognized = recognizer.handle_event_at(&moved, now + Duration::from_millis(16)); - let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { - panic!("expected scroll, got {recognized:?}"); - }; - assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(28.))); - - // The next step is measured from where the previous prediction left - // the content, so an overshoot is paid back here. - let mut moved = touch_event(touch, TouchPhase::Moved, 100., 130.); - moved.predicted_position = Some(point(px(104.), px(134.))); - let recognized = recognizer.handle_event_at(&moved, now + Duration::from_millis(32)); - let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { - panic!("expected scroll, got {recognized:?}"); - }; - assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(6.))); - - // A release without a fling (the finger stopped long before lifting) - // targets the raw position: the total scrolled distance equals the - // finger's actual travel despite the predictions. - let recognized = recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Ended, 100., 130.), - now + Duration::from_millis(120), - ); - let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { - panic!("expected scroll, got {recognized:?}"); - }; - assert_eq!(scroll.touch_phase, TouchPhase::Ended); - assert!(!recognizer.has_momentum()); - assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(-4.))); - } - - #[test] - fn predicted_positions_do_not_emit_false_reversals() { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let now = Instant::now(); - let touch = TouchId(1); - - recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 100.), now); - - let mut moved = touch_event(touch, TouchPhase::Moved, 100., 120.); - moved.predicted_position = Some(point(px(100.), px(130.))); - let recognized = recognizer.handle_event_at(&moved, now + Duration::from_millis(16)); - let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { - panic!("expected scroll, got {recognized:?}"); - }; - assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(30.))); - - let mut moved = touch_event(touch, TouchPhase::Moved, 100., 125.); - moved.predicted_position = Some(point(px(100.), px(127.))); - let recognized = recognizer.handle_event_at(&moved, now + Duration::from_millis(32)); - let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { - panic!("expected scroll, got {recognized:?}"); - }; - assert_eq!( - scroll.delta.pixel_delta(px(16.)), - Point::::default() - ); - - let mut moved = touch_event(touch, TouchPhase::Moved, 100., 125.); - moved.predicted_position = Some(point(px(100.), px(126.))); - let recognized = recognizer.handle_event_at(&moved, now + Duration::from_millis(40)); - let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { - panic!("expected scroll, got {recognized:?}"); - }; - assert_eq!( - scroll.delta.pixel_delta(px(16.)), - Point::::default() - ); - - let mut moved = touch_event(touch, TouchPhase::Moved, 100., 132.); - moved.predicted_position = Some(point(px(100.), px(136.))); - let recognized = recognizer.handle_event_at(&moved, now + Duration::from_millis(48)); - let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { - panic!("expected scroll, got {recognized:?}"); - }; - assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(6.))); - - let mut moved = touch_event(touch, TouchPhase::Moved, 100., 124.); - moved.predicted_position = Some(point(px(100.), px(140.))); - let recognized = recognizer.handle_event_at(&moved, now + Duration::from_millis(64)); - let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { - panic!("expected scroll, got {recognized:?}"); - }; - assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(-12.))); - } - - #[test] - fn predicted_overshoot_folds_into_the_fling_without_scrolling_backwards() { - let now = Instant::now(); - let mut total_with_prediction = 0f32; - let mut total_without_prediction = 0f32; - for use_prediction in [true, false] { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let mut total = 0f32; - let mut drain = |recognized: &[RecognizedTouchGesture], upward_only: bool| { - for gesture in recognized { - let RecognizedTouchGesture::Scroll(scroll) = gesture else { - panic!("expected scroll, got {gesture:?}"); - }; - let delta = scroll.delta.pixel_delta(px(16.)).y; - if upward_only { - assert!( - delta <= px(0.), - "content moved backwards by {delta:?} during an upward gesture" - ); - } - total += f32::from(delta); - } - }; - - recognizer.handle_event_at( - &touch_event(TouchId(1), TouchPhase::Started, 100., 500.), - now, - ); - for step in 1..=5u64 { - let raw_y = 500. - step as f32 * 40.; - let mut moved = touch_event(TouchId(1), TouchPhase::Moved, 100., raw_y); - if use_prediction { - moved.predicted_position = Some(point(px(100.), px(raw_y - 25.))); - } - let recognized = - recognizer.handle_event_at(&moved, now + Duration::from_millis(step * 16)); - drain(&recognized, use_prediction); - } - // The release leaves the emitted position 25px ahead of the raw - // one; with prediction the correction must not scroll backwards. - let recognized = recognizer.handle_event_at( - &touch_event(TouchId(1), TouchPhase::Ended, 100., 300.), - now + Duration::from_millis(90), - ); - drain(&recognized, use_prediction); - assert!(recognizer.has_momentum()); - let mut tick = now + Duration::from_millis(91); - while recognizer.has_momentum() { - if let Some(gesture) = recognizer.tick_momentum_at(tick) { - drain(&[gesture], use_prediction); - } - tick += Duration::from_millis(16); - } - - if use_prediction { - total_with_prediction = total; - } else { - total_without_prediction = total; - } - } - // Folding the overshoot into the fling redistributes the travel but - // must not change where the content comes to rest. - assert!( - (total_with_prediction - total_without_prediction).abs() < 0.01, - "totals diverged: {total_with_prediction} vs {total_without_prediction}" - ); - } - - #[test] - fn fast_release_starts_momentum_that_decays_to_a_stop() { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let now = Instant::now(); - let touch = TouchId(1); - - recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 300.), now); - for step in 1..=5 { - recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Moved, 100., 300. - step as f32 * 20.), - now + Duration::from_millis(step * 16), - ); - } - recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Ended, 100., 200.), - now + Duration::from_millis(6 * 16), - ); - assert!(recognizer.has_momentum()); - - let tick = now + Duration::from_millis(6 * 16 + 16); - let recognized = recognizer.tick_momentum_at(tick); - let Some(RecognizedTouchGesture::Scroll(scroll)) = recognized else { - panic!("expected momentum scroll, got {recognized:?}"); - }; - assert_eq!(scroll.touch_phase, TouchPhase::Moved); - assert_eq!(scroll.position, point(px(100.), px(300.))); - let delta = scroll.delta.pixel_delta(px(16.)); - assert!( - delta.y < px(0.), - "momentum should continue upward, got {delta:?}" - ); - // The least-squares fit may leave float residue on the motionless axis. - assert!( - delta.x.abs() < px(0.001), - "expected no x motion, got {delta:?}" - ); - - let mut last_phase = TouchPhase::Moved; - let mut ticks = 0; - let mut time = tick; - while recognizer.has_momentum() { - time += Duration::from_millis(16); - ticks += 1; - assert!(ticks < 1000, "momentum never stopped"); - if let Some(RecognizedTouchGesture::Scroll(scroll)) = recognizer.tick_momentum_at(time) - { - last_phase = scroll.touch_phase; - } - } - assert_eq!(last_phase, TouchPhase::Ended); - assert!(recognizer.tick_momentum_at(time).is_none()); - } - - #[test] - fn diagonal_release_flings_only_on_locked_axis() { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let now = Instant::now(); - let touch = TouchId(1); - - recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 300.), now); - for step in 1..=5 { - let recognized = recognizer.handle_event_at( - &touch_event( - touch, - TouchPhase::Moved, - 100. + step as f32 * 3., - 300. - step as f32 * 20., - ), - now + Duration::from_millis(step * 16), - ); - let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { - panic!("expected scroll, got {recognized:?}"); - }; - assert_eq!(scroll.delta.pixel_delta(px(16.)).x, px(0.)); - } - recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Ended, 115., 200.), - now + Duration::from_millis(6 * 16), - ); - assert!(recognizer.has_momentum()); - - let mut time = now + Duration::from_millis(6 * 16); - while recognizer.has_momentum() { - time += Duration::from_millis(16); - if let Some(RecognizedTouchGesture::Scroll(scroll)) = recognizer.tick_momentum_at(time) - { - let delta = scroll.delta.pixel_delta(px(16.)); - assert_eq!(delta.x, px(0.)); - assert!(delta.y <= px(0.)); - } - } - } - - #[test] - fn slow_release_does_not_start_momentum() { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let now = Instant::now(); - let touch = TouchId(1); - - recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 300.), now); - recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Moved, 100., 280.), - now + Duration::from_millis(16), - ); - recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Moved, 100., 279.), - now + Duration::from_millis(500), - ); - recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Ended, 100., 279.), - now + Duration::from_millis(600), - ); - assert!(!recognizer.has_momentum()); - } - - #[test] - fn new_touch_interrupts_momentum() { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let now = Instant::now(); - - recognizer.handle_event_at( - &touch_event(TouchId(1), TouchPhase::Started, 100., 300.), - now, - ); - for step in 1..=3 { - recognizer.handle_event_at( - &touch_event( - TouchId(1), - TouchPhase::Moved, - 100., - 300. - step as f32 * 33., - ), - now + Duration::from_millis(step * 16), - ); - } - recognizer.handle_event_at( - &touch_event(TouchId(1), TouchPhase::Ended, 100., 200.), - now + Duration::from_millis(64), - ); - assert!(recognizer.has_momentum()); - - let recognized = recognizer.handle_event_at( - &touch_event(TouchId(2), TouchPhase::Started, 100., 200.), - now + Duration::from_millis(200), - ); - assert!(!recognizer.has_momentum()); - let [ - RecognizedTouchGesture::Scroll(closing), - RecognizedTouchGesture::Scroll(opening), - ] = recognized.as_slice() - else { - panic!("expected closing and opening scrolls, got {recognized:?}"); - }; - assert_eq!(closing.touch_phase, TouchPhase::Ended); - assert!(closing.delta.pixel_delta(px(16.)).is_zero()); - assert_eq!(opening.touch_phase, TouchPhase::Started); - assert!(opening.delta.pixel_delta(px(16.)).is_zero()); - } - - #[test] - fn catching_a_fling_pans_immediately_and_never_taps() { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let now = Instant::now(); - - recognizer.handle_event_at( - &touch_event(TouchId(1), TouchPhase::Started, 100., 300.), - now, - ); - for step in 1..=3 { - recognizer.handle_event_at( - &touch_event( - TouchId(1), - TouchPhase::Moved, - 100., - 300. - step as f32 * 33., - ), - now + Duration::from_millis(step * 16), - ); - } - recognizer.handle_event_at( - &touch_event(TouchId(1), TouchPhase::Ended, 100., 200.), - now + Duration::from_millis(64), - ); - assert!(recognizer.has_momentum()); - - recognizer.handle_event_at( - &touch_event(TouchId(2), TouchPhase::Started, 100., 200.), - now + Duration::from_millis(200), - ); - - // A movement well within the slop scrolls immediately. - let recognized = recognizer.handle_event_at( - &touch_event(TouchId(2), TouchPhase::Moved, 100., 197.), - now + Duration::from_millis(216), - ); - let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { - panic!("expected scroll, got {recognized:?}"); - }; - assert_eq!(scroll.touch_phase, TouchPhase::Moved); - assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(-3.))); - - // Releasing the catch is not a tap. - let recognized = recognizer.handle_event_at( - &touch_event(TouchId(2), TouchPhase::Ended, 100., 197.), - now + Duration::from_millis(232), - ); - let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { - panic!("expected scroll, got {recognized:?}"); - }; - assert_eq!(scroll.touch_phase, TouchPhase::Ended); - } - - #[test] - fn cancelled_pan_emits_cancelled_scroll_and_no_tap() { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let now = Instant::now(); - let touch = TouchId(1); - - recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 100.), now); - recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Moved, 100., 150.), - now + Duration::from_millis(16), - ); - let recognized = recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Cancelled, 100., 150.), - now + Duration::from_millis(32), - ); - let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { - panic!("expected cancelled scroll, got {recognized:?}"); - }; - assert_eq!(scroll.touch_phase, TouchPhase::Cancelled); - assert!(!recognizer.has_momentum()); - - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 100.), now); - let recognized = recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Cancelled, 100., 102.), - now + Duration::from_millis(16), - ); - assert!(recognized.is_empty(), "cancelled tap must not click"); - } - - #[test] - fn concurrent_touches_are_ignored_while_one_is_active() { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let now = Instant::now(); - - recognizer.handle_event_at( - &touch_event(TouchId(1), TouchPhase::Started, 100., 100.), - now, - ); - let recognized = recognizer.handle_event_at( - &touch_event(TouchId(2), TouchPhase::Started, 200., 200.), - now + Duration::from_millis(8), - ); - assert!(recognized.is_empty()); - let recognized = recognizer.handle_event_at( - &touch_event(TouchId(2), TouchPhase::Moved, 200., 300.), - now + Duration::from_millis(16), - ); - assert!(recognized.is_empty()); - let recognized = recognizer.handle_event_at( - &touch_event(TouchId(2), TouchPhase::Ended, 200., 300.), - now + Duration::from_millis(24), - ); - assert!(recognized.is_empty()); - - // The first touch still resolves normally. - let recognized = recognizer.handle_event_at( - &touch_event(TouchId(1), TouchPhase::Moved, 100., 150.), - now + Duration::from_millis(32), - ); - let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { - panic!("expected scroll, got {recognized:?}"); - }; - assert_eq!(scroll.touch_phase, TouchPhase::Started); - } - - #[test] - fn spline_position_table_matches_the_bezier_curve() { - let samples = friction_spline::spline_position_samples(); - // AOSP's initializer solves sample 0 numerically like every other - // sample, so it lands within solver tolerance of zero, not at zero. - assert!(samples[0].abs() < 1e-4); - assert_eq!(samples[100], 1.); - for window in samples.windows(2) { - assert!(window[0] < window[1], "table must be strictly increasing"); - } - // Each table entry must lie on the defining parametric Bezier: for - // sample i there must be a curve parameter whose time component is - // i/100 and whose position component is the stored value. - for (i, &stored_position) in samples.iter().enumerate().take(100) { - let alpha = i as f32 / 100.; - let (mut lower, mut upper) = (0f32, 1f32); - for _ in 0..50 { - let middle = (lower + upper) / 2.; - let (time, _) = friction_spline::bezier_time_and_position(middle); - if time > alpha { - upper = middle; - } else { - lower = middle; - } - } - let (time, position) = friction_spline::bezier_time_and_position((lower + upper) / 2.); - assert!( - (time - alpha).abs() < 1e-4, - "sample {i}: time {time} != {alpha}" - ); - assert!( - (position - stored_position).abs() < 1e-3, - "sample {i}: position {position} != stored {stored_position}" - ); - } - } - - #[test] - fn fling_curves_are_sane_for_both_physics() { - for physics in [ScrollPhysics::ios(), ScrollPhysics::android()] { - let slow = physics.fling_duration(500.); - let fast = physics.fling_duration(4000.); - assert!(slow > Duration::ZERO, "{physics:?}"); - assert!(fast > slow, "faster flings must coast longer: {physics:?}"); - - let halfway = physics.fling_distance(4000., fast / 2); - let total = physics.fling_distance(4000., fast); - assert!(halfway > 0. && halfway < total, "{physics:?}"); - assert!( - physics.fling_distance(4000., fast * 2) == total, - "distance must not grow past the fling duration: {physics:?}" - ); - assert!( - physics.fling_distance(4000., fast) > physics.fling_distance(500., slow), - "faster flings must travel further: {physics:?}" - ); - } - } - - #[test] - fn momentum_is_frame_rate_independent() { - // The same fling ticked at 60Hz and as one huge stalled frame must - // cover identical ground. - let total_distance_with_tick_length = |tick: Duration| -> f32 { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning { - scroll_physics: ScrollPhysics::android(), - ..GestureTuning::default() - }); - let now = Instant::now(); - recognizer.handle_event_at( - &touch_event(TouchId(1), TouchPhase::Started, 100., 500.), - now, - ); - for step in 1..=3 { - recognizer.handle_event_at( - &touch_event( - TouchId(1), - TouchPhase::Moved, - 100., - 500. - step as f32 * 40., - ), - now + Duration::from_millis(step * 16), - ); - } - recognizer.handle_event_at( - &touch_event(TouchId(1), TouchPhase::Ended, 100., 380.), - now + Duration::from_millis(64), - ); - assert!(recognizer.has_momentum()); - - let mut total = 0f32; - let mut time = now + Duration::from_millis(64); - let mut guard = 0; - while recognizer.has_momentum() { - time += tick; - guard += 1; - assert!(guard < 10_000, "momentum never stopped"); - if let Some(RecognizedTouchGesture::Scroll(scroll)) = - recognizer.tick_momentum_at(time) - { - total += f32::from(scroll.delta.pixel_delta(px(16.)).y); - } - } - total - }; - - let smooth = total_distance_with_tick_length(Duration::from_millis(16)); - let stalled = total_distance_with_tick_length(Duration::from_secs(10)); - assert!( - (smooth - stalled).abs() < 0.01, - "expected identical fling distance, got {smooth} vs {stalled}" - ); - } - - #[test] - fn flick_velocity_reflects_release_speed_not_window_average() { - // A uniformly accelerating flick: position grows quadratically, so - // the speed at the newest sample (2·k·t) is twice the window - // average (k·t). The estimator must report the former. - let mut velocity_tracker = VelocityTracker::default(); - let start = Instant::now(); - for step in 0..=6 { - let t = step as f32 * 0.016; - velocity_tracker.push( - start + Duration::from_millis(step * 16), - point(px(0.), px(1000. * t * t)), - ); - } - let velocity = velocity_tracker.velocity(); - let release_speed = 2. * 1000. * 0.096; - assert!( - (velocity.y - release_speed).abs() < 1., - "expected ≈{release_speed} px/s at release, got {} px/s", - velocity.y - ); - assert_eq!(velocity.x, 0.); - } - - #[test] - fn samples_before_a_pause_do_not_contribute_velocity() { - // Fast motion, then a hold longer than the stopped-finger gap, then - // a slow nudge: only the motion after the pause describes the - // release. - let mut velocity_tracker = VelocityTracker::default(); - let start = Instant::now(); - velocity_tracker.push(start, point(px(0.), px(0.))); - velocity_tracker.push(start + Duration::from_millis(16), point(px(0.), px(50.))); - velocity_tracker.push(start + Duration::from_millis(80), point(px(0.), px(52.))); - velocity_tracker.push(start + Duration::from_millis(96), point(px(0.), px(54.))); - let velocity = velocity_tracker.velocity(); - assert!( - velocity.y < 200., - "pre-pause motion leaked into the estimate: {} px/s", - velocity.y - ); - } - - #[test] - fn claimed_touch_drag_emits_phased_stream_without_pan_or_tap() { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let touch = TouchId(1); - let now = Instant::now(); - recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 10., 20.), now); - let Some(RecognizedTouchGesture::TouchDrag(started)) = recognizer.offer_touch_drag(touch) - else { - panic!("expected touch drag"); - }; - assert_eq!(started.phase, TouchPhase::Started); - assert_eq!(started.start_position, point(px(10.), px(20.))); - recognizer.resolve_touch_drag(true); - - let moved = recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Moved, 40., 50.), - now + Duration::from_millis(10), - ); - let [RecognizedTouchGesture::TouchDrag(moved)] = moved.as_slice() else { - panic!("expected moved touch drag, got {moved:?}"); - }; - assert_eq!(moved.phase, TouchPhase::Moved); - assert_eq!(moved.position, point(px(40.), px(50.))); - - let ended = recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Ended, 45., 55.), - now + Duration::from_millis(20), - ); - let [RecognizedTouchGesture::TouchDrag(ended)] = ended.as_slice() else { - panic!("expected ended touch drag, got {ended:?}"); - }; - assert_eq!(ended.phase, TouchPhase::Ended); - assert_eq!(ended.position, point(px(45.), px(55.))); - } - - #[test] - fn unclaimed_touch_drag_remains_a_pan_candidate() { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let touch = TouchId(1); - let now = Instant::now(); - recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 0., 0.), now); - assert!(recognizer.offer_touch_drag(touch).is_some()); - recognizer.resolve_touch_drag(false); - - let moved = recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Moved, 20., 0.), - now + Duration::from_millis(10), - ); - assert!(matches!( - moved.as_slice(), - [RecognizedTouchGesture::Scroll(ScrollWheelEvent { - touch_phase: TouchPhase::Started, - .. - })] - )); - } - - #[test] - fn claimed_long_press_emits_phased_stream_without_tap() { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let touch = TouchId(1); - let now = Instant::now(); - recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 10., 20.), now); - let Some(RecognizedTouchGesture::LongPress(started)) = recognizer.offer_long_press(touch) - else { - panic!("expected long press"); - }; - assert_eq!(started.phase, TouchPhase::Started); - assert_eq!(started.start_position, point(px(10.), px(20.))); - recognizer.resolve_long_press(true); - - let moved = recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Moved, 12., 21.), - now + Duration::from_millis(510), - ); - let [RecognizedTouchGesture::LongPress(moved)] = moved.as_slice() else { - panic!("expected moved long press, got {moved:?}"); - }; - assert_eq!(moved.phase, TouchPhase::Moved); - - let ended = recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Ended, 12., 21.), - now + Duration::from_millis(520), - ); - let [RecognizedTouchGesture::LongPress(ended)] = ended.as_slice() else { - panic!("expected ended long press, got {ended:?}"); - }; - assert_eq!(ended.phase, TouchPhase::Ended); - } - - #[test] - fn unclaimed_long_press_remains_a_tap_candidate() { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let touch = TouchId(1); - let now = Instant::now(); - recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 10., 20.), now); - assert!(recognizer.offer_long_press(touch).is_some()); - recognizer.resolve_long_press(false); - - let ended = recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Ended, 10., 20.), - now + Duration::from_millis(510), - ); - assert!(matches!( - ended.as_slice(), - [RecognizedTouchGesture::Tap { .. }] - )); - } - - #[test] - fn unclaimed_long_press_can_still_become_a_pan() { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let touch = TouchId(1); - let now = Instant::now(); - recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 0., 0.), now); - assert!(recognizer.offer_long_press(touch).is_some()); - recognizer.resolve_long_press(false); - - let moved = recognizer.handle_event_at( - &touch_event(touch, TouchPhase::Moved, 20., 0.), - now + Duration::from_millis(510), - ); - assert!(matches!( - moved.as_slice(), - [RecognizedTouchGesture::Scroll(ScrollWheelEvent { - touch_phase: TouchPhase::Started, - .. - })] - )); - } - - #[test] - fn long_press_offer_is_one_shot_and_specific_to_pending_touch() { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let touch = TouchId(1); - recognizer.handle_event(&touch_event(touch, TouchPhase::Started, 0., 0.)); - - assert!( - recognizer - .handle_event(&touch_event(TouchId(2), TouchPhase::Moved, 20., 0.)) - .is_empty() - ); - assert!(recognizer.offer_long_press(TouchId(2)).is_none()); - assert!(recognizer.offer_long_press(touch).is_some()); - assert!(recognizer.offer_long_press(touch).is_none()); - } - - #[test] - fn long_press_cannot_be_offered_after_pending_touch_resolves() { - for phase in [TouchPhase::Ended, TouchPhase::Cancelled, TouchPhase::Moved] { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let touch = TouchId(1); - let now = Instant::now(); - recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 0., 0.), now); - let position = if phase == TouchPhase::Moved { 20. } else { 0. }; - recognizer.handle_event_at( - &touch_event(touch, phase, position, 0.), - now + Duration::from_millis(10), - ); - assert!(recognizer.offer_long_press(touch).is_none()); - } - } - - #[test] - fn claimed_long_press_emits_cancelled_for_its_touch_only() { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let touch = TouchId(1); - recognizer.handle_event(&touch_event(touch, TouchPhase::Started, 4., 5.)); - assert!(recognizer.offer_long_press(touch).is_some()); - recognizer.resolve_long_press(true); - - assert!( - recognizer - .handle_event(&touch_event(TouchId(2), TouchPhase::Cancelled, 9., 9.)) - .is_empty() - ); - let cancelled = recognizer.handle_event(&touch_event(touch, TouchPhase::Cancelled, 6., 7.)); - let [RecognizedTouchGesture::LongPress(cancelled)] = cancelled.as_slice() else { - panic!("expected cancelled long press, got {cancelled:?}"); - }; - assert_eq!(cancelled.phase, TouchPhase::Cancelled); - assert_eq!(cancelled.start_position, point(px(4.), px(5.))); - assert_eq!(cancelled.position, point(px(6.), px(7.))); - } - - #[test] - fn unrelated_touch_cannot_end_or_cancel_pending_touch() { - for phase in [TouchPhase::Ended, TouchPhase::Cancelled] { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let touch = TouchId(1); - recognizer.handle_event(&touch_event(touch, TouchPhase::Started, 4., 5.)); - - assert!( - recognizer - .handle_event(&touch_event(TouchId(2), phase, 9., 9.)) - .is_empty() - ); - assert!(recognizer.offer_long_press(touch).is_some()); - } - } - - #[test] - fn completed_touch_id_cannot_claim_replacement_touch() { - let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); - let completed_touch = TouchId(1); - let replacement_touch = TouchId(2); - recognizer.handle_event(&touch_event(completed_touch, TouchPhase::Started, 0., 0.)); - recognizer.handle_event(&touch_event(completed_touch, TouchPhase::Cancelled, 0., 0.)); - recognizer.handle_event(&touch_event(replacement_touch, TouchPhase::Started, 5., 5.)); - - assert!(recognizer.offer_long_press(completed_touch).is_none()); - assert!(recognizer.offer_long_press(replacement_touch).is_some()); - } - - fn touch_event(id: TouchId, phase: TouchPhase, x: f32, y: f32) -> TouchEvent { - TouchEvent { - id, - phase, - position: point(px(x), px(y)), - predicted_position: None, - force: None, - } - } -} diff --git a/crates/gpui_pre/src/global.rs b/crates/gpui_pre/src/global.rs deleted file mode 100644 index a16934c..0000000 --- a/crates/gpui_pre/src/global.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::{App, BorrowAppContext}; - -/// A marker trait for types that can be stored in GPUI's global state. -/// -/// This trait exists to provide type-safe access to globals by ensuring only -/// types that implement [`Global`] can be used with the accessor methods. For -/// example, trying to access a global with a type that does not implement -/// [`Global`] will result in a compile-time error. -/// -/// Implement this on types you want to store in the context as a global. -/// -/// ## Restricting Access to Globals -/// -/// In some situations you may need to store some global state, but want to -/// restrict access to reading it or writing to it. -/// -/// In these cases, Rust's visibility system can be used to restrict access to -/// a global value. For example, you can create a private struct that implements -/// [`Global`] and holds the global state. Then create a newtype struct that wraps -/// the global type and create custom accessor methods to expose the desired subset -/// of operations. -pub trait Global: 'static { - // This trait is intentionally left empty, by virtue of being a marker trait. - // - // Use additional traits with blanket implementations to attach functionality - // to types that implement `Global`. -} - -/// A trait for reading a global value from the context. -pub trait ReadGlobal { - /// Returns the global instance of the implementing type. - /// - /// Panics if a global for that type has not been assigned. - fn global(cx: &App) -> &Self; -} - -impl ReadGlobal for T { - fn global(cx: &App) -> &Self { - cx.global::() - } -} - -/// A trait for updating a global value in the context. -pub trait UpdateGlobal { - /// Updates the global instance of the implementing type using the provided closure. - /// - /// This method provides the closure with mutable access to the context and the global simultaneously. - fn update_global(cx: &mut C, update: F) -> R - where - C: BorrowAppContext, - F: FnOnce(&mut Self, &mut C) -> R; - - /// Set the global instance of the implementing type. - fn set_global(cx: &mut C, global: Self) - where - C: BorrowAppContext; -} - -impl UpdateGlobal for T { - #[track_caller] - fn update_global(cx: &mut C, update: F) -> R - where - C: BorrowAppContext, - F: FnOnce(&mut Self, &mut C) -> R, - { - cx.update_global(update) - } - - fn set_global(cx: &mut C, global: Self) - where - C: BorrowAppContext, - { - cx.set_global(global) - } -} diff --git a/crates/gpui_pre/src/gpui.rs b/crates/gpui_pre/src/gpui.rs deleted file mode 100644 index cb97c3c..0000000 --- a/crates/gpui_pre/src/gpui.rs +++ /dev/null @@ -1,354 +0,0 @@ -#![doc = include_str!("../README.md")] -#![warn(missing_docs)] -#![allow(clippy::type_complexity)] // Not useful, GPUI makes heavy use of callbacks -#![allow(clippy::collapsible_else_if)] // False positives in platform specific code -#![allow(unused_mut)] // False positives in platform specific code - -extern crate self as gpui; -#[macro_use] -mod action; -mod app; - -mod arena; -mod asset_cache; -mod assets; -mod bounds_tree; -mod clip; -mod color; -/// The default colors used by GPUI. -pub mod colors; -#[cfg(feature = "profiler")] -mod debug_overlay; -mod element; -mod elements; -mod executor; -mod platform_scheduler; -pub(crate) use platform_scheduler::PlatformScheduler; -mod geometry; -mod gestures; -mod global; -mod input; -mod inspector; -mod interactive; -mod key_dispatch; -mod keymap; -mod path_builder; -mod platform; -pub mod prelude; -/// Profiling utilities for task, frame, and thread performance tracking. -pub mod profiler; -#[cfg(any( - test, - target_os = "windows", - target_os = "linux", - target_family = "wasm", - feature = "test-support", - feature = "bench-support" -))] -#[expect(missing_docs)] -pub mod queue; -mod scene; -mod shared_uri; -mod spring; -mod style; -mod styled; -mod subscription; -mod svg_renderer; -mod tab_stop; -mod taffy; -#[cfg(any(test, feature = "test-support"))] -pub mod test; -mod text_system; -mod util; -mod view; -mod window; - -#[cfg(any(test, feature = "test-support"))] -pub use proptest; - -#[cfg(doc)] -pub mod _accessibility; -#[cfg(doc)] -pub mod _ownership_and_data_flow; - -/// Do not touch, here be dragons for use by gpui_macros and such. -#[doc(hidden)] -pub mod private { - pub use anyhow; - pub use inventory; - pub use schemars; - pub use serde; - pub use serde_json; -} - -mod seal { - /// A mechanism for restricting implementations of a trait to only those in GPUI. - /// See: - pub trait Sealed {} -} - -pub use accesskit; -pub use accesskit::Action as AccessibleAction; -pub use accesskit::{Orientation, Role, Toggled}; -pub use action::*; -pub use anyhow::Result; -pub use app::*; -pub(crate) use arena::*; -pub use asset_cache::*; -pub use assets::*; -pub use clip::*; -pub use color::*; -pub use ctor::ctor; -#[cfg(feature = "profiler")] -pub use debug_overlay::*; -pub use element::*; -pub use elements::*; -pub use executor::*; -pub use geometry::*; -pub use gestures::*; -pub use global::*; -pub use gpui_macros::{ - bench, property_test, register_action, test, AppContext, IntoElement, Render, VisualContext, -}; -pub use spring::*; - -/// Defines a Criterion benchmark group for benchmarks annotated with [`gpui::bench`]. -/// -/// This mirrors `criterion::criterion_group!` so GPUI benchmark files can keep the -/// same shape as ordinary Criterion benchmarks. -/// -/// [`gpui::bench`]: crate::bench -#[macro_export] -macro_rules! bench_group { - ($($tokens:tt)*) => { - criterion::criterion_group!($($tokens)*); - }; -} - -/// Defines the entry point for GPUI Criterion benchmark groups. -/// -/// This mirrors `criterion::criterion_main!` so GPUI benchmark files can keep the -/// same shape as ordinary Criterion benchmarks. -#[macro_export] -macro_rules! bench_main { - ($($tokens:tt)*) => { - criterion::criterion_main!($($tokens)*); - }; -} -pub use gpui_shared_string::*; -pub use gpui_util::arc_cow::ArcCow; -pub use http_client; -pub use input::*; -pub use inspector::*; -pub use interactive::*; -use key_dispatch::*; -pub use keymap::*; -pub use path_builder::*; -pub use platform::*; -pub use profiler::*; -#[cfg(any(target_os = "windows", target_os = "linux", target_family = "wasm"))] -pub use queue::{PriorityQueueReceiver, PriorityQueueSender}; -pub use refineable::*; -pub use scene::*; -pub use shared_uri::*; -use std::{any::Any, future::Future}; -pub use style::*; -pub use styled::*; -pub use subscription::*; -pub use svg_renderer::*; -pub(crate) use tab_stop::*; -use taffy::TaffyLayoutEngine; -pub use taffy::{AvailableSpace, LayoutId}; -#[cfg(any(test, feature = "test-support"))] -pub use test::*; -pub use text_system::*; -pub use util::{FutureExt, Timeout}; -pub use view::*; -pub use window::*; - -#[cfg(not(target_family = "wasm"))] -pub use pollster::block_on; - -/// The context trait, allows the different contexts in GPUI to be used -/// interchangeably for certain operations. -pub trait AppContext { - /// Create a new entity in the app context. - #[expect( - clippy::wrong_self_convention, - reason = "`App::new` is an ubiquitous function for creating entities" - )] - fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity; - - /// Reserve a slot for a entity to be inserted later. - /// The returned [Reservation] allows you to obtain the [EntityId] for the future entity. - fn reserve_entity(&mut self) -> Reservation; - - /// Insert a new entity in the app context based on a [Reservation] previously obtained from [`reserve_entity`]. - /// - /// [`reserve_entity`]: Self::reserve_entity - fn insert_entity( - &mut self, - reservation: Reservation, - build_entity: impl FnOnce(&mut Context) -> T, - ) -> Entity; - - /// Update a entity in the app context. - fn update_entity( - &mut self, - handle: &Entity, - update: impl FnOnce(&mut T, &mut Context) -> R, - ) -> R - where - T: 'static; - - /// Update a entity in the app context. - fn as_mut<'a, T>(&'a mut self, handle: &Entity) -> GpuiBorrow<'a, T> - where - T: 'static; - - /// Read a entity from the app context. - fn read_entity(&self, handle: &Entity, read: impl FnOnce(&T, &App) -> R) -> R - where - T: 'static; - - /// Update a window for the given handle. - fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result - where - F: FnOnce(AnyView, &mut Window, &mut App) -> T; - - /// Run `f` against the entity's *current* window — the most recently - /// rendered window that referenced the entity. Returns `None` if the - /// entity has no current window or that window is unavailable. See - /// [`App::with_window`] for the underlying lookup. - fn with_window( - &mut self, - entity_id: EntityId, - f: impl FnOnce(&mut Window, &mut App) -> R, - ) -> Option; - - /// Read a window off of the application context. - fn read_window( - &self, - window: &WindowHandle, - read: impl FnOnce(Entity, &App) -> R, - ) -> Result - where - T: 'static; - - /// Spawn a future on a background thread - fn background_spawn(&self, future: impl Future + Send + 'static) -> Task - where - R: Send + 'static; - - /// Read a global from this app context - fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R - where - G: Global; -} - -/// Returned by [Context::reserve_entity] to later be passed to [Context::insert_entity]. -/// Allows you to obtain the [EntityId] for a entity before it is created. -pub struct Reservation(pub(crate) Slot); - -impl Reservation { - /// Returns the [EntityId] that will be associated with the entity once it is inserted. - pub fn entity_id(&self) -> EntityId { - self.0.entity_id() - } -} - -/// This trait is used for the different visual contexts in GPUI that -/// require a window to be present. -pub trait VisualContext: AppContext { - /// The result type for window operations. - type Result; - - /// Returns the handle of the window associated with this context. - fn window_handle(&self) -> AnyWindowHandle; - - /// Update a view with the given callback - fn update_window_entity( - &mut self, - entity: &Entity, - update: impl FnOnce(&mut T, &mut Window, &mut Context) -> R, - ) -> Self::Result; - - /// Create a new entity, with access to `Window`. - fn new_window_entity( - &mut self, - build_entity: impl FnOnce(&mut Window, &mut Context) -> T, - ) -> Self::Result>; - - /// Replace the root view of a window with a new view. - fn replace_root_view( - &mut self, - build_view: impl FnOnce(&mut Window, &mut Context) -> V, - ) -> Self::Result> - where - V: 'static + Render; - - /// Focus a entity in the window, if it implements the [`Focusable`] trait. - fn focus(&mut self, entity: &Entity) -> Self::Result<()> - where - V: Focusable; -} - -/// A trait for tying together the types of a GPUI entity and the events it can -/// emit. -pub trait EventEmitter: 'static {} - -/// A helper trait for auto-implementing certain methods on contexts that -/// can be used interchangeably. -pub trait BorrowAppContext { - /// Set a global value on the context. - fn set_global(&mut self, global: T); - /// Updates the global state of the given type. - fn update_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R - where - G: Global; - /// Updates the global state of the given type, creating a default if it didn't exist before. - fn update_default_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R - where - G: Global + Default; -} - -impl BorrowAppContext for C -where - C: std::borrow::BorrowMut, -{ - fn set_global(&mut self, global: G) { - self.borrow_mut().set_global(global) - } - - #[track_caller] - fn update_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R - where - G: Global, - { - let mut global = self.borrow_mut().lease_global::(); - let result = f(&mut global, self); - self.borrow_mut().end_global_lease(global); - result - } - - fn update_default_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R - where - G: Global + Default, - { - self.borrow_mut().default_global::(); - self.update_global(f) - } -} - -/// Information about the GPU GPUI is running on. -#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone)] -pub struct GpuSpecs { - /// Whether the GPU is really a fake (like `llvmpipe`) running on the CPU. - pub is_software_emulated: bool, - /// The name of the device, as reported by Vulkan. - pub device_name: String, - /// The name of the driver, as reported by Vulkan. - pub driver_name: String, - /// Further information about the driver, as reported by Vulkan. - pub driver_info: String, -} diff --git a/crates/gpui_pre/src/input.rs b/crates/gpui_pre/src/input.rs deleted file mode 100644 index 4167aa0..0000000 --- a/crates/gpui_pre/src/input.rs +++ /dev/null @@ -1,485 +0,0 @@ -use crate::{ - App, Bounds, ClipboardItem, Context, Entity, InputHandler, Pixels, TextInputConfiguration, - UTF16Selection, Window, -}; -use std::ops::Range; - -/// Implement this trait to allow views to handle textual input when implementing an editor, field, etc. -/// -/// Once your view implements this trait, you can use it to construct an [`ElementInputHandler`]. -/// This input handler can then be assigned during paint by calling [`Window::handle_input`]. -/// -/// See [`InputHandler`] for details on how to implement each method. -pub trait EntityInputHandler: 'static + Sized { - /// See [`InputHandler::text_for_range`] for details - fn text_for_range( - &mut self, - range: Range, - adjusted_range: &mut Option>, - window: &mut Window, - cx: &mut Context, - ) -> Option; - - /// See [`InputHandler::selected_text_range`] for details - fn selected_text_range( - &mut self, - ignore_disabled_input: bool, - window: &mut Window, - cx: &mut Context, - ) -> Option; - - /// See [`InputHandler::marked_text_range`] for details - fn marked_text_range( - &self, - window: &mut Window, - cx: &mut Context, - ) -> Option>; - - /// See [`InputHandler::unmark_text`] for details - fn unmark_text(&mut self, window: &mut Window, cx: &mut Context); - - /// See [`InputHandler::paste`] for details - fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut Context) { - if let Some(text) = item.text() { - self.replace_text_in_range(None, &text, window, cx); - } - } - - /// See [`InputHandler::replace_text_in_range`] for details - fn replace_text_in_range( - &mut self, - range: Option>, - text: &str, - window: &mut Window, - cx: &mut Context, - ); - - /// See [`InputHandler::replace_and_mark_text_in_range`] for details - fn replace_and_mark_text_in_range( - &mut self, - range: Option>, - new_text: &str, - new_selected_range: Option>, - window: &mut Window, - cx: &mut Context, - ); - - /// See [`InputHandler::bounds_for_range`] for details - fn bounds_for_range( - &mut self, - range_utf16: Range, - element_bounds: Bounds, - window: &mut Window, - cx: &mut Context, - ) -> Option>; - - /// See [`InputHandler::character_index_for_point`] for details - fn character_index_for_point( - &mut self, - point: crate::Point, - window: &mut Window, - cx: &mut Context, - ) -> Option; - - /// See [`InputHandler::set_selected_text_range`] for details - fn set_selected_text_range( - &mut self, - _range_utf16: Range, - _window: &mut Window, - _cx: &mut Context, - ) { - } - - /// See [`InputHandler::text_length_utf16`] for details - fn text_length_utf16( - &mut self, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - None - } - - /// See [`InputHandler::accepts_text_input`] for details - fn accepts_text_input(&self, _window: &mut Window, _cx: &mut Context) -> bool { - true - } - - /// See [`InputHandler::text_input_configuration`] for details - fn text_input_configuration( - &mut self, - _window: &mut Window, - _cx: &mut Context, - ) -> TextInputConfiguration { - TextInputConfiguration::default() - } - - /// See [`InputHandler::text_input_editable_range`] for details - fn text_input_editable_range( - &mut self, - _window: &mut Window, - _cx: &mut Context, - ) -> Option> { - None - } -} - -/// The canonical implementation of [`crate::PlatformInputHandler`]. Call [`Window::handle_input`] -/// with an instance during your element's paint. -pub struct ElementInputHandler { - view: Entity, - element_bounds: Bounds, -} - -impl ElementInputHandler { - /// Used in [`Element::paint`][element_paint] with the element's bounds, a `Window`, and a `App` context. - /// - /// [element_paint]: crate::Element::paint - pub fn new(element_bounds: Bounds, view: Entity) -> Self { - ElementInputHandler { - view, - element_bounds, - } - } -} - -impl InputHandler for ElementInputHandler { - fn selected_text_range( - &mut self, - ignore_disabled_input: bool, - window: &mut Window, - cx: &mut App, - ) -> Option { - self.view.update(cx, |view, cx| { - view.selected_text_range(ignore_disabled_input, window, cx) - }) - } - - fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option> { - self.view - .update(cx, |view, cx| view.marked_text_range(window, cx)) - } - - fn text_for_range( - &mut self, - range_utf16: Range, - adjusted_range: &mut Option>, - window: &mut Window, - cx: &mut App, - ) -> Option { - self.view.update(cx, |view, cx| { - view.text_for_range(range_utf16, adjusted_range, window, cx) - }) - } - - fn replace_text_in_range( - &mut self, - replacement_range: Option>, - text: &str, - window: &mut Window, - cx: &mut App, - ) { - self.view.update(cx, |view, cx| { - view.replace_text_in_range(replacement_range, text, window, cx) - }); - } - - fn replace_and_mark_text_in_range( - &mut self, - range_utf16: Option>, - new_text: &str, - new_selected_range: Option>, - window: &mut Window, - cx: &mut App, - ) { - self.view.update(cx, |view, cx| { - view.replace_and_mark_text_in_range( - range_utf16, - new_text, - new_selected_range, - window, - cx, - ) - }); - } - - fn unmark_text(&mut self, window: &mut Window, cx: &mut App) { - self.view - .update(cx, |view, cx| view.unmark_text(window, cx)); - } - - fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut App) { - self.view - .update(cx, |view, cx| view.paste(item, window, cx)); - } - - fn bounds_for_range( - &mut self, - range_utf16: Range, - window: &mut Window, - cx: &mut App, - ) -> Option> { - self.view.update(cx, |view, cx| { - view.bounds_for_range(range_utf16, self.element_bounds, window, cx) - }) - } - - fn character_index_for_point( - &mut self, - point: crate::Point, - window: &mut Window, - cx: &mut App, - ) -> Option { - self.view.update(cx, |view, cx| { - view.character_index_for_point(point, window, cx) - }) - } - - fn set_selected_text_range( - &mut self, - range_utf16: Range, - window: &mut Window, - cx: &mut App, - ) { - self.view.update(cx, |view, cx| { - view.set_selected_text_range(range_utf16, window, cx) - }) - } - - fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option> { - Some(self.element_bounds) - } - - fn text_length_utf16(&mut self, window: &mut Window, cx: &mut App) -> Option { - self.view - .update(cx, |view, cx| view.text_length_utf16(window, cx)) - } - - fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool { - self.view - .update(cx, |view, cx| view.accepts_text_input(window, cx)) - } - - fn prefers_ime_for_printable_keys(&mut self, window: &mut Window, cx: &mut App) -> bool { - self.view - .update(cx, |view, cx| view.accepts_text_input(window, cx)) - } - - fn text_input_configuration( - &mut self, - window: &mut Window, - cx: &mut App, - ) -> TextInputConfiguration { - self.view - .update(cx, |view, cx| view.text_input_configuration(window, cx)) - } - - fn text_input_editable_range( - &mut self, - window: &mut Window, - cx: &mut App, - ) -> Option> { - self.view - .update(cx, |view, cx| view.text_input_editable_range(window, cx)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - AnyWindowHandle, AppContext as _, FocusHandle, InteractiveElement as _, IntoElement, - ParentElement as _, Render, Styled as _, TestAppContext, TextInputAction, - TextInputStateChange, canvas, div, - }; - - #[gpui::test] - fn text_input_configuration_and_focus_state_are_forwarded_on_change(cx: &mut TestAppContext) { - let custom = TextInputConfiguration { - autocorrect: true, - input_action: TextInputAction::Send, - ..Default::default() - }; - let window = cx.add_window({ - let custom = custom.clone(); - move |_, cx| ConfigurationTestView { - focus_handle: cx.focus_handle(), - configuration: custom, - } - }); - let view = window.root(cx).unwrap(); - let test_window = cx.test_window(window.into()); - let window = AnyWindowHandle::from(window); - let draw = |cx: &mut TestAppContext| { - cx.update_window(window, |_, window, cx| window.draw(cx).clear(cx)) - .unwrap(); - }; - - // Nothing is focused, so the platform learns the default configuration. - draw(cx); - assert_eq!( - test_window.text_input_configurations(), - vec![TextInputConfiguration::default()] - ); - assert!(test_window.text_input_state_changes().is_empty()); - - // Focusing the view routes its configuration to the platform. - cx.update_window(window, |_, window, cx| { - let focus_handle = view.read(cx).focus_handle.clone(); - window.focus(&focus_handle, cx); - }) - .unwrap(); - draw(cx); - assert_eq!( - test_window.text_input_configurations(), - vec![TextInputConfiguration::default(), custom.clone()] - ); - assert_eq!( - test_window.text_input_state_changes(), - vec![TextInputStateChange::FocusGained] - ); - - // Redrawing without a change forwards nothing. - draw(cx); - assert_eq!(test_window.text_input_configurations().len(), 2); - assert_eq!(test_window.text_input_state_changes().len(), 1); - - // Changing the configuration forwards the new value. - let updated = TextInputConfiguration { - suggestions: true, - ..custom - }; - view.update(cx, { - let updated = updated.clone(); - |view, cx| { - view.configuration = updated; - cx.notify(); - } - }); - draw(cx); - assert_eq!( - test_window.text_input_configurations().last(), - Some(&updated) - ); - assert_eq!(test_window.text_input_configurations().len(), 3); - assert_eq!(test_window.text_input_state_changes().len(), 1); - - // Losing focus reverts the platform to the default configuration. - cx.update_window(window, |_, window, cx| window.blur(cx)) - .unwrap(); - draw(cx); - assert_eq!( - test_window.text_input_configurations().last(), - Some(&TextInputConfiguration::default()) - ); - assert_eq!(test_window.text_input_configurations().len(), 4); - assert_eq!( - test_window.text_input_state_changes(), - vec![ - TextInputStateChange::FocusGained, - TextInputStateChange::FocusLost - ] - ); - } - - struct ConfigurationTestView { - focus_handle: FocusHandle, - configuration: TextInputConfiguration, - } - - impl Render for ConfigurationTestView { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let view = cx.entity(); - let focus_handle = self.focus_handle.clone(); - div().size_full().track_focus(&self.focus_handle).child( - canvas( - |_, _, _| {}, - move |bounds, _, window, cx| { - window.handle_input( - &focus_handle, - ElementInputHandler::new(bounds, view), - cx, - ); - }, - ) - .size_full(), - ) - } - } - - impl EntityInputHandler for ConfigurationTestView { - fn text_for_range( - &mut self, - _range: std::ops::Range, - _adjusted_range: &mut Option>, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - None - } - - fn selected_text_range( - &mut self, - _ignore_disabled_input: bool, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - None - } - - fn marked_text_range( - &self, - _window: &mut Window, - _cx: &mut Context, - ) -> Option> { - None - } - - fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context) {} - - fn replace_text_in_range( - &mut self, - _range: Option>, - _text: &str, - _window: &mut Window, - _cx: &mut Context, - ) { - } - - fn replace_and_mark_text_in_range( - &mut self, - _range: Option>, - _new_text: &str, - _new_selected_range: Option>, - _window: &mut Window, - _cx: &mut Context, - ) { - } - - fn bounds_for_range( - &mut self, - _range_utf16: std::ops::Range, - _element_bounds: Bounds, - _window: &mut Window, - _cx: &mut Context, - ) -> Option> { - None - } - - fn character_index_for_point( - &mut self, - _point: crate::Point, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - None - } - - fn text_input_configuration( - &mut self, - _window: &mut Window, - _cx: &mut Context, - ) -> TextInputConfiguration { - self.configuration.clone() - } - } -} diff --git a/crates/gpui_pre/src/inspector.rs b/crates/gpui_pre/src/inspector.rs deleted file mode 100644 index 12995f0..0000000 --- a/crates/gpui_pre/src/inspector.rs +++ /dev/null @@ -1,254 +0,0 @@ -/// A unique identifier for an element that can be inspected. -#[derive(Debug, Eq, PartialEq, Hash, Clone)] -pub struct InspectorElementId { - /// Stable part of the ID. - #[cfg(any(feature = "inspector", debug_assertions))] - pub path: std::rc::Rc, - /// Disambiguates elements that have the same path. - #[cfg(any(feature = "inspector", debug_assertions))] - pub instance_id: usize, -} - -impl Into for &InspectorElementId { - fn into(self) -> InspectorElementId { - self.clone() - } -} - -#[cfg(any(feature = "inspector", debug_assertions))] -pub use conditional::*; - -#[cfg(any(feature = "inspector", debug_assertions))] -mod conditional { - use super::*; - use crate::{AnyElement, App, Context, Empty, IntoElement, Render, Window}; - use collections::{FxHashMap, TypeIdHashMap}; - use std::any::{Any, TypeId}; - - /// `GlobalElementId` qualified by source location of element construction. - #[derive(Debug, Eq, PartialEq, Hash)] - pub struct InspectorElementPath { - /// The path to the nearest ancestor element that has an `ElementId`. - #[cfg(any(feature = "inspector", debug_assertions))] - pub global_id: crate::GlobalElementId, - /// Source location where this element was constructed. - #[cfg(any(feature = "inspector", debug_assertions))] - pub source_location: &'static std::panic::Location<'static>, - } - - impl Clone for InspectorElementPath { - fn clone(&self) -> Self { - Self { - global_id: self.global_id.clone(), - source_location: self.source_location, - } - } - } - - impl Into for &InspectorElementPath { - fn into(self) -> InspectorElementPath { - self.clone() - } - } - - /// Function set on `App` to render the inspector UI. - pub type InspectorRenderer = - Box) -> AnyElement>; - - /// Manages inspector state - which element is currently selected and whether the inspector is - /// in picking mode. - pub struct Inspector { - active_element: Option, - pub(crate) pick_depth: Option, - } - - struct InspectedElement { - id: InspectorElementId, - states: TypeIdHashMap>, - } - - impl InspectedElement { - fn new(id: InspectorElementId) -> Self { - InspectedElement { - id, - states: Default::default(), - } - } - } - - impl Inspector { - pub(crate) fn new() -> Self { - Self { - active_element: None, - pick_depth: Some(0.0), - } - } - - pub(crate) fn select(&mut self, id: InspectorElementId, window: &mut Window) { - self.set_active_element_id(id, window); - self.pick_depth = None; - } - - pub(crate) fn hover(&mut self, id: InspectorElementId, window: &mut Window) { - if self.is_picking() { - let changed = self.set_active_element_id(id, window); - if changed { - self.pick_depth = Some(0.0); - } - } - } - - pub(crate) fn set_active_element_id( - &mut self, - id: InspectorElementId, - window: &mut Window, - ) -> bool { - let changed = Some(&id) != self.active_element_id(); - if changed { - self.active_element = Some(InspectedElement::new(id)); - window.refresh(); - } - changed - } - - /// ID of the currently hovered or selected element. - pub fn active_element_id(&self) -> Option<&InspectorElementId> { - self.active_element.as_ref().map(|e| &e.id) - } - - pub(crate) fn with_active_element_state( - &mut self, - window: &mut Window, - f: impl FnOnce(&mut Option, &mut Window) -> R, - ) -> R { - let Some(active_element) = &mut self.active_element else { - return f(&mut None, window); - }; - - let type_id = TypeId::of::(); - let mut inspector_state = active_element - .states - .remove(&type_id) - .map(|state| *state.downcast().unwrap()); - - let result = f(&mut inspector_state, window); - - if let Some(inspector_state) = inspector_state { - active_element - .states - .insert(type_id, Box::new(inspector_state)); - } - - result - } - - /// Starts element picking mode, allowing the user to select elements by clicking. - pub fn start_picking(&mut self) { - self.pick_depth = Some(0.0); - } - - /// Returns whether the inspector is currently in picking mode. - pub fn is_picking(&self) -> bool { - self.pick_depth.is_some() - } - - /// Renders elements for all registered inspector states of the active inspector element. - pub fn render_inspector_states( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Vec { - let mut elements = Vec::new(); - if let Some(active_element) = self.active_element.take() { - for (type_id, state) in &active_element.states { - if let Some(render_inspector) = cx - .inspector_element_registry - .renderers_by_type_id - .remove(type_id) - { - let mut element = (render_inspector)( - active_element.id.clone(), - state.as_ref(), - window, - cx, - ); - elements.push(element); - cx.inspector_element_registry - .renderers_by_type_id - .insert(*type_id, render_inspector); - } - } - - self.active_element = Some(active_element); - } - - elements - } - } - - impl Render for Inspector { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - if let Some(inspector_renderer) = cx.inspector_renderer.take() { - let result = inspector_renderer(self, window, cx); - cx.inspector_renderer = Some(inspector_renderer); - result - } else { - Empty.into_any_element() - } - } - } - - #[derive(Default)] - pub(crate) struct InspectorElementRegistry { - renderers_by_type_id: FxHashMap< - TypeId, - Box AnyElement>, - >, - } - - impl InspectorElementRegistry { - pub fn register( - &mut self, - f: impl 'static + Fn(InspectorElementId, &T, &mut Window, &mut App) -> R, - ) { - self.renderers_by_type_id.insert( - TypeId::of::(), - Box::new(move |id, value, window, cx| { - let value = value.downcast_ref().unwrap(); - f(id, value, window, cx).into_any_element() - }), - ); - } - } -} - -/// Provides definitions used by `#[derive_inspector_reflection]`. -#[cfg(any(feature = "inspector", debug_assertions))] -pub mod inspector_reflection { - use std::any::Any; - - /// Reification of a function that has the signature `fn some_fn(T) -> T`. Provides the name, - /// documentation, and ability to invoke the function. - #[derive(Clone, Copy)] - pub struct FunctionReflection { - /// The name of the function - pub name: &'static str, - /// The method - pub function: fn(Box) -> Box, - /// Documentation for the function - pub documentation: Option<&'static str>, - /// `PhantomData` for the type of the argument and result - pub _type: std::marker::PhantomData, - } - - impl FunctionReflection { - /// Invoke this method on a value and return the result. - pub fn invoke(&self, value: T) -> T { - let boxed = Box::new(value) as Box; - let result = (self.function)(boxed); - *result - .downcast::() - .expect("Type mismatch in reflection invoke") - } - } -} diff --git a/crates/gpui_pre/src/interactive.rs b/crates/gpui_pre/src/interactive.rs deleted file mode 100644 index 913cf1b..0000000 --- a/crates/gpui_pre/src/interactive.rs +++ /dev/null @@ -1,971 +0,0 @@ -use crate::{ - Bounds, Capslock, Context, Empty, IntoElement, Keystroke, LongPressEvent, Modifiers, Pixels, - Point, Render, TouchDragEvent, Window, point, seal::Sealed, -}; -use smallvec::SmallVec; -use std::{any::Any, fmt::Debug, ops::Deref, path::PathBuf}; - -/// An event from a platform input source. -pub trait InputEvent: Sealed + 'static { - /// Convert this event into the platform input enum. - fn to_platform_input(self) -> PlatformInput; -} - -/// A key event from the platform. -pub trait KeyEvent: InputEvent {} - -/// A mouse event from the platform. -pub trait MouseEvent: InputEvent {} - -/// A gesture event from the platform. -pub trait GestureEvent: InputEvent {} - -/// The key down event equivalent for the platform. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct KeyDownEvent { - /// The keystroke that was generated. - pub keystroke: Keystroke, - - /// Whether the key is currently held down. - pub is_held: bool, - - /// Whether to prefer character input over keybindings for this keystroke. - /// In some cases, like AltGr on Windows, modifiers are significant for character input. - pub prefer_character_input: bool, -} - -impl Sealed for KeyDownEvent {} -impl InputEvent for KeyDownEvent { - fn to_platform_input(self) -> PlatformInput { - PlatformInput::KeyDown(self) - } -} -impl KeyEvent for KeyDownEvent {} - -/// The key up event equivalent for the platform. -#[derive(Clone, Debug)] -pub struct KeyUpEvent { - /// The keystroke that was released. - pub keystroke: Keystroke, -} - -impl Sealed for KeyUpEvent {} -impl InputEvent for KeyUpEvent { - fn to_platform_input(self) -> PlatformInput { - PlatformInput::KeyUp(self) - } -} -impl KeyEvent for KeyUpEvent {} - -/// The modifiers changed event equivalent for the platform. -#[derive(Clone, Debug, Default)] -pub struct ModifiersChangedEvent { - /// The new state of the modifier keys - pub modifiers: Modifiers, - /// The new state of the capslock key - pub capslock: Capslock, -} - -impl Sealed for ModifiersChangedEvent {} -impl InputEvent for ModifiersChangedEvent { - fn to_platform_input(self) -> PlatformInput { - PlatformInput::ModifiersChanged(self) - } -} -impl KeyEvent for ModifiersChangedEvent {} - -impl Deref for ModifiersChangedEvent { - type Target = Modifiers; - - fn deref(&self) -> &Self::Target { - &self.modifiers - } -} - -/// The phase of a touch motion event. -/// Based on the winit enum of the same name. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum TouchPhase { - /// The touch started. - Started, - /// The touch event is moving. - #[default] - Moved, - /// The touch phase has ended - Ended, - /// The touch was cancelled: the system took it and it will not end - /// normally. Consumers must fully unwind any in-progress interaction, - /// treating the touch as if it never committed. - Cancelled, -} - -/// Identifies one touch (finger or stylus contact) for its lifetime, from -/// [`TouchPhase::Started`] through [`TouchPhase::Ended`] or -/// [`TouchPhase::Cancelled`]. -/// -/// The value is opaque and assigned by the platform. A platform window must -/// not reuse an identifier for a later touch. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct TouchId(pub u64); - -/// A raw touch event from the platform. -/// -/// -/// Dispatch contract (core implementation pending): a touch is hit-tested -/// once, at [`TouchPhase::Started`], occlusion-aware; all subsequent events -/// for the same [`TouchId`] are delivered to the elements under the starting -/// position, even after the touch moves outside them. -#[derive(Clone, Debug, Default)] -pub struct TouchEvent { - /// Which touch this event belongs to. - pub id: TouchId, - /// The phase of the touch. - pub phase: TouchPhase, - /// The position of the touch in window coordinates. - pub position: Point, - /// Where the platform predicts the touch will be roughly one frame from - /// now, in the same coordinate space as `position`, when the platform - /// offers a prediction for a [`TouchPhase::Moved`] event. - /// - /// Best-effort latency compensation only: it may influence how far a - /// recognized pan scrolls within a frame, but never hit testing, gesture - /// classification, or velocity estimation, and any error it introduces - /// must be corrected by later events for the same touch. - pub predicted_position: Option>, - /// Normalized touch force in `0.0..=1.0`, if the hardware reports it. - pub force: Option, -} - -impl Sealed for TouchEvent {} -impl InputEvent for TouchEvent { - fn to_platform_input(self) -> PlatformInput { - PlatformInput::Touch(self) - } -} - -/// A mouse down event from the platform -#[derive(Clone, Debug, Default)] -pub struct MouseDownEvent { - /// Which mouse button was pressed. - pub button: MouseButton, - - /// The position of the mouse on the window. - pub position: Point, - - /// The modifiers that were held down when the mouse was pressed. - pub modifiers: Modifiers, - - /// The number of times the button has been clicked. - pub click_count: usize, - - /// Whether this is the first, focusing click. - pub first_mouse: bool, -} - -impl Sealed for MouseDownEvent {} -impl InputEvent for MouseDownEvent { - fn to_platform_input(self) -> PlatformInput { - PlatformInput::MouseDown(self) - } -} -impl MouseEvent for MouseDownEvent {} - -impl MouseDownEvent { - /// Returns true if this mouse up event should focus the element. - pub fn is_focusing(&self) -> bool { - match self.button { - MouseButton::Left => true, - _ => false, - } - } -} - -/// A mouse up event from the platform -#[derive(Clone, Debug, Default)] -pub struct MouseUpEvent { - /// Which mouse button was released. - pub button: MouseButton, - - /// The position of the mouse on the window. - pub position: Point, - - /// The modifiers that were held down when the mouse was released. - pub modifiers: Modifiers, - - /// The number of times the button has been clicked. - pub click_count: usize, -} - -impl Sealed for MouseUpEvent {} -impl InputEvent for MouseUpEvent { - fn to_platform_input(self) -> PlatformInput { - PlatformInput::MouseUp(self) - } -} - -impl MouseEvent for MouseUpEvent {} - -impl MouseUpEvent { - /// Returns true if this mouse up event should focus the element. - pub fn is_focusing(&self) -> bool { - match self.button { - MouseButton::Left => true, - _ => false, - } - } -} - -/// A click event, generated when a mouse button is pressed and released. -#[derive(Clone, Debug, Default)] -pub struct MouseClickEvent { - /// The mouse event when the button was pressed. - pub down: MouseDownEvent, - - /// The mouse event when the button was released. - pub up: MouseUpEvent, -} - -/// The stage of a pressure click event. -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub enum PressureStage { - /// No pressure. - #[default] - Zero, - /// Normal click pressure. - Normal, - /// High pressure, enough to trigger a force click. - Force, -} - -/// A mouse pressure event from the platform. Generated when a force-sensitive trackpad is pressed hard. -/// Currently only implemented for macOS trackpads. -#[derive(Debug, Clone, Default)] -pub struct MousePressureEvent { - /// Pressure of the current stage as a float between 0 and 1 - pub pressure: f32, - /// The pressure stage of the event. - pub stage: PressureStage, - /// The position of the mouse on the window. - pub position: Point, - /// The modifiers that were held down when the mouse pressure changed. - pub modifiers: Modifiers, -} - -impl Sealed for MousePressureEvent {} -impl InputEvent for MousePressureEvent { - fn to_platform_input(self) -> PlatformInput { - PlatformInput::MousePressure(self) - } -} -impl MouseEvent for MousePressureEvent {} - -/// A click event that was generated by a keyboard button being pressed and released. -#[derive(Clone, Debug, Default)] -pub struct KeyboardClickEvent { - /// The keyboard button that was pressed to trigger the click. - pub button: KeyboardButton, - - /// The bounds of the element that was clicked. - pub bounds: Bounds, -} - -/// A click event that was generated by a recognized tap gesture on a touch -/// screen. -#[derive(Clone, Debug, Default)] -pub struct TouchClickEvent { - /// The position of the tap in window coordinates. - pub position: Point, - /// The number of consecutive taps at this location (double tap = 2), - /// analogous to the mouse `click_count`. - pub tap_count: usize, - /// Whether this was a long press rather than a tap. Long presses are - /// touch's secondary activation: they are delivered to aux-click - /// listeners alongside right clicks, not to primary click listeners. - pub long_press: bool, -} - -/// A click event, generated when a mouse button or keyboard button is pressed and released, -/// or when a tap gesture is recognized on a touch screen. -#[derive(Clone, Debug)] -pub enum ClickEvent { - /// A click event trigger by a mouse button being pressed and released. - Mouse(MouseClickEvent), - /// A click event trigger by a keyboard button being pressed and released. - Keyboard(KeyboardClickEvent), - /// A click event triggered by a recognized tap gesture on a touch screen. - Touch(TouchClickEvent), -} - -impl Default for ClickEvent { - fn default() -> Self { - ClickEvent::Keyboard(KeyboardClickEvent::default()) - } -} - -impl ClickEvent { - /// Returns the modifiers that were held during the click event - /// - /// `Keyboard`: The keyboard click events never have modifiers. - /// `Mouse`: Modifiers that were held during the mouse key up event. - pub fn modifiers(&self) -> Modifiers { - match self { - // Click events are only generated from keyboard events _without any modifiers_, so we know the modifiers are always Default - ClickEvent::Keyboard(_) => Modifiers::default(), - // Click events on the web only reflect the modifiers for the keyup event, - // tested via observing the behavior of the `ClickEvent.shiftKey` field in Chrome 138 - // under various combinations of modifiers and keyUp / keyDown events. - ClickEvent::Mouse(event) => event.up.modifiers, - // Touch screens have no modifier keys. - ClickEvent::Touch(_) => Modifiers::default(), - } - } - - /// Returns the position of the click event - /// - /// `Keyboard`: The bottom left corner of the clicked hitbox - /// `Mouse`: The position of the mouse when the button was released. - /// `Touch`: The position of the tap. - pub fn position(&self) -> Point { - match self { - ClickEvent::Keyboard(event) => event.bounds.bottom_left(), - ClickEvent::Mouse(event) => event.up.position, - ClickEvent::Touch(event) => event.position, - } - } - - /// Returns the mouse position of the click event - /// - /// `Keyboard`: None - /// `Mouse`: The position of the mouse when the button was released. - /// `Touch`: None, touches are not mouse input and there is no cursor. - pub fn mouse_position(&self) -> Option> { - match self { - ClickEvent::Keyboard(_) => None, - ClickEvent::Mouse(event) => Some(event.up.position), - ClickEvent::Touch(_) => None, - } - } - - /// Returns if this was a right click - /// - /// `Keyboard`: false - /// `Mouse`: Whether the right button was pressed and released - pub fn is_right_click(&self) -> bool { - match self { - ClickEvent::Keyboard(_) => false, - ClickEvent::Mouse(event) => { - event.down.button == MouseButton::Right && event.up.button == MouseButton::Right - } - ClickEvent::Touch(_) => false, - } - } - - /// Returns if this was a middle click - /// - /// `Keyboard`: false - /// `Mouse`: Whether the middle button was pressed and released - pub fn is_middle_click(&self) -> bool { - match self { - ClickEvent::Keyboard(_) => false, - ClickEvent::Mouse(event) => { - event.down.button == MouseButton::Middle && event.up.button == MouseButton::Middle - } - ClickEvent::Touch(_) => false, - } - } - - /// Returns whether the click is a secondary activation, i.e. a context - /// menu trigger: a right click from a mouse (macOS ctrl-clicks arrive - /// already converted to right clicks by the platform layer), or a long - /// press on a touch screen. - pub fn is_secondary(&self) -> bool { - match self { - ClickEvent::Keyboard(_) => false, - ClickEvent::Mouse(event) => { - event.down.button == MouseButton::Right && event.up.button == MouseButton::Right - } - ClickEvent::Touch(event) => event.long_press, - } - } - - /// Returns whether the click was a standard click - /// - /// `Keyboard`: Always true - /// `Mouse`: Left button pressed and released - /// `Touch`: A tap, but not a long press - pub fn standard_click(&self) -> bool { - match self { - ClickEvent::Keyboard(_) => true, - ClickEvent::Mouse(event) => { - event.down.button == MouseButton::Left && event.up.button == MouseButton::Left - } - ClickEvent::Touch(event) => !event.long_press, - } - } - - /// Returns whether the click focused the element - /// - /// `Keyboard`: false, keyboard clicks only work if an element is already focused - /// `Mouse`: Whether this was the first focusing click - /// `Touch`: false, mobile windows are already active when tappable - pub fn first_focus(&self) -> bool { - match self { - ClickEvent::Keyboard(_) => false, - ClickEvent::Mouse(event) => event.down.first_mouse, - ClickEvent::Touch(_) => false, - } - } - - /// Returns the click count of the click event - /// - /// `Keyboard`: Always 1 - /// `Mouse`: Count of clicks from MouseUpEvent - /// `Touch`: Count of consecutive taps - pub fn click_count(&self) -> usize { - match self { - ClickEvent::Keyboard(_) => 1, - ClickEvent::Mouse(event) => event.up.click_count, - ClickEvent::Touch(event) => event.tap_count, - } - } - - /// Returns whether the click event is generated by a keyboard event - pub fn is_keyboard(&self) -> bool { - match self { - ClickEvent::Mouse(_) | ClickEvent::Touch(_) => false, - ClickEvent::Keyboard(_) => true, - } - } -} - -/// An enum representing the keyboard button that was pressed for a click event. -#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, Default)] -pub enum KeyboardButton { - /// Enter key was clicked - #[default] - Enter, - /// Space key was clicked - Space, -} - -/// An enum representing the mouse button that was pressed. -#[derive(Hash, Default, PartialEq, Eq, Copy, Clone, Debug)] -pub enum MouseButton { - /// The left mouse button. - #[default] - Left, - - /// The right mouse button. - Right, - - /// The middle mouse button. - Middle, - - /// A navigation button, such as back or forward. - Navigate(NavigationDirection), -} - -impl MouseButton { - /// Get all the mouse buttons in a list. - pub fn all() -> Vec { - vec![ - MouseButton::Left, - MouseButton::Right, - MouseButton::Middle, - MouseButton::Navigate(NavigationDirection::Back), - MouseButton::Navigate(NavigationDirection::Forward), - ] - } -} - -/// A navigation direction, such as back or forward. -#[derive(Hash, Default, PartialEq, Eq, Copy, Clone, Debug)] -pub enum NavigationDirection { - /// The back button. - #[default] - Back, - - /// The forward button. - Forward, -} - -/// A mouse move event from the platform. -#[derive(Clone, Debug, Default)] -pub struct MouseMoveEvent { - /// The position of the mouse on the window. - pub position: Point, - - /// The mouse button that was pressed, if any. - pub pressed_button: Option, - - /// The modifiers that were held down when the mouse was moved. - pub modifiers: Modifiers, -} - -impl Sealed for MouseMoveEvent {} -impl InputEvent for MouseMoveEvent { - fn to_platform_input(self) -> PlatformInput { - PlatformInput::MouseMove(self) - } -} -impl MouseEvent for MouseMoveEvent {} - -impl MouseMoveEvent { - /// Returns true if the left mouse button is currently held down. - pub fn dragging(&self) -> bool { - self.pressed_button == Some(MouseButton::Left) - } -} - -/// A mouse wheel event from the platform. -#[derive(Clone, Debug, Default)] -pub struct ScrollWheelEvent { - /// The position of the mouse on the window. - pub position: Point, - - /// The change in scroll wheel position for this event. - pub delta: ScrollDelta, - - /// The modifiers that were held down when the mouse was moved. - pub modifiers: Modifiers, - - /// The phase of the touch event. - pub touch_phase: TouchPhase, -} - -impl Sealed for ScrollWheelEvent {} -impl InputEvent for ScrollWheelEvent { - fn to_platform_input(self) -> PlatformInput { - PlatformInput::ScrollWheel(self) - } -} -impl MouseEvent for ScrollWheelEvent {} - -impl Deref for ScrollWheelEvent { - type Target = Modifiers; - - fn deref(&self) -> &Self::Target { - &self.modifiers - } -} - -/// The scroll delta for a scroll wheel event. -#[derive(Clone, Copy, Debug)] -pub enum ScrollDelta { - /// An exact scroll delta in pixels. - Pixels(Point), - /// An inexact scroll delta in lines. - Lines(Point), -} - -impl Default for ScrollDelta { - fn default() -> Self { - Self::Lines(Default::default()) - } -} - -/// A pinch gesture event from the platform, generated when the user performs -/// a pinch-to-zoom gesture (typically on a trackpad). -/// -#[derive(Clone, Debug, Default)] -pub struct PinchEvent { - /// The position of the pinch center on the window. - pub position: Point, - - /// The zoom delta for this event. - /// Positive values indicate zooming in, negative values indicate zooming out. - /// For example, 0.1 represents a 10% zoom increase. - pub delta: f32, - - /// The modifiers that were held down during the pinch gesture. - pub modifiers: Modifiers, - - /// The phase of the pinch gesture. - pub phase: TouchPhase, -} - -impl Sealed for PinchEvent {} -impl InputEvent for PinchEvent { - fn to_platform_input(self) -> PlatformInput { - PlatformInput::Pinch(self) - } -} -impl GestureEvent for PinchEvent {} -impl MouseEvent for PinchEvent {} - -impl Deref for PinchEvent { - type Target = Modifiers; - - fn deref(&self) -> &Self::Target { - &self.modifiers - } -} - -impl ScrollDelta { - /// Returns true if this is a precise scroll delta in pixels. - pub fn precise(&self) -> bool { - match self { - ScrollDelta::Pixels(_) => true, - ScrollDelta::Lines(_) => false, - } - } - - /// Converts this scroll event into exact pixels. - pub fn pixel_delta(&self, line_height: Pixels) -> Point { - match self { - ScrollDelta::Pixels(delta) => *delta, - ScrollDelta::Lines(delta) => point(line_height * delta.x, line_height * delta.y), - } - } - - /// Combines two scroll deltas into one. - /// If the signs of the deltas are the same (both positive or both negative), - /// the deltas are added together. If the signs are opposite, the second delta - /// (other) is used, effectively overriding the first delta. - pub fn coalesce(self, other: ScrollDelta) -> ScrollDelta { - match (self, other) { - (ScrollDelta::Pixels(a), ScrollDelta::Pixels(b)) => { - let x = if a.x.signum() == b.x.signum() { - a.x + b.x - } else { - b.x - }; - - let y = if a.y.signum() == b.y.signum() { - a.y + b.y - } else { - b.y - }; - - ScrollDelta::Pixels(point(x, y)) - } - - (ScrollDelta::Lines(a), ScrollDelta::Lines(b)) => { - let x = if a.x.signum() == b.x.signum() { - a.x + b.x - } else { - b.x - }; - - let y = if a.y.signum() == b.y.signum() { - a.y + b.y - } else { - b.y - }; - - ScrollDelta::Lines(point(x, y)) - } - - _ => other, - } - } -} - -/// A mouse exit event from the platform, generated when the mouse leaves the window. -#[derive(Clone, Debug, Default)] -pub struct MouseExitEvent { - /// The position of the mouse relative to the window. - pub position: Point, - /// The mouse button that was pressed, if any. - pub pressed_button: Option, - /// The modifiers that were held down when the mouse was moved. - pub modifiers: Modifiers, -} - -impl Sealed for MouseExitEvent {} -impl InputEvent for MouseExitEvent { - fn to_platform_input(self) -> PlatformInput { - PlatformInput::MouseExited(self) - } -} - -impl MouseEvent for MouseExitEvent {} - -impl Deref for MouseExitEvent { - type Target = Modifiers; - - fn deref(&self) -> &Self::Target { - &self.modifiers - } -} - -/// A collection of paths from the platform, such as from a file drop. -#[derive(Debug, Clone, Default, Eq, PartialEq)] -pub struct ExternalPaths(pub SmallVec<[PathBuf; 2]>); - -impl ExternalPaths { - /// Convert this collection of paths into a slice. - pub fn paths(&self) -> &[PathBuf] { - &self.0 - } -} - -/// Data offered to the platform when an internal drag leaves the window and is -/// promoted to a native drag session. -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum ExternalDragPayload { - /// Real on-disk paths, handed to the platform as an outbound file drag. - Files(FileDragPaths), -} - -/// Paths handed to the platform for a native file drag. Directory metadata is -/// provided by the caller to avoid querying it when the platform drag starts. -#[derive(Debug, Clone, Default, Eq, PartialEq)] -pub struct FileDragPaths(SmallVec<[(PathBuf, bool); 2]>); - -impl FileDragPaths { - /// Creates a native file-drag payload from paths paired with whether each path is a directory. - pub fn new(entries: impl IntoIterator) -> Self { - Self(entries.into_iter().collect()) - } - - /// The dragged paths, each paired with whether it is a directory. - pub fn entries(&self) -> &[(PathBuf, bool)] { - &self.0 - } -} - -impl Render for ExternalPaths { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - // the platform will render icons for the dragged files - Empty - } -} - -/// A file drop event from the platform, generated when files are dragged and dropped onto the window. -#[derive(Debug, Clone)] -pub enum FileDropEvent { - /// The files have entered the window. - Entered { - /// The position of the mouse relative to the window. - position: Point, - /// The paths of the files that are being dragged. - paths: ExternalPaths, - }, - /// The files are being dragged over the window - Pending { - /// The position of the mouse relative to the window. - position: Point, - }, - /// The files have been dropped onto the window. - Submit { - /// The position of the mouse relative to the window. - position: Point, - }, - /// The user has stopped dragging the files over the window. - Exited, - /// The platform-owned drag session has ended. - Ended, -} - -impl Sealed for FileDropEvent {} -impl InputEvent for FileDropEvent { - fn to_platform_input(self) -> PlatformInput { - PlatformInput::FileDrop(self) - } -} -impl MouseEvent for FileDropEvent {} - -/// An enum corresponding to all kinds of platform input events. -#[derive(Clone, Debug)] -pub enum PlatformInput { - /// A key was pressed. - KeyDown(KeyDownEvent), - /// A key was released. - KeyUp(KeyUpEvent), - /// The keyboard modifiers were changed. - ModifiersChanged(ModifiersChangedEvent), - /// The mouse was pressed. - MouseDown(MouseDownEvent), - /// The mouse was released. - MouseUp(MouseUpEvent), - /// Mouse pressure. - MousePressure(MousePressureEvent), - /// The mouse was moved. - MouseMove(MouseMoveEvent), - /// The mouse exited the window. - MouseExited(MouseExitEvent), - /// The scroll wheel was used. - ScrollWheel(ScrollWheelEvent), - /// A pinch gesture was performed. - Pinch(PinchEvent), - /// A long-press gesture recognized from touch input. - LongPress(LongPressEvent), - /// A direct touch drag claimed by an element. - TouchDrag(TouchDragEvent), - /// Files were dragged and dropped onto the window. - FileDrop(FileDropEvent), - /// A raw touch event on a touch screen. - Touch(TouchEvent), -} - -impl PlatformInput { - pub(crate) fn mouse_event(&self) -> Option<&dyn Any> { - match self { - PlatformInput::KeyDown { .. } => None, - PlatformInput::KeyUp { .. } => None, - PlatformInput::ModifiersChanged { .. } => None, - PlatformInput::MouseDown(event) => Some(event), - PlatformInput::MouseUp(event) => Some(event), - PlatformInput::MouseMove(event) => Some(event), - PlatformInput::MousePressure(event) => Some(event), - PlatformInput::MouseExited(event) => Some(event), - PlatformInput::ScrollWheel(event) => Some(event), - PlatformInput::Pinch(event) => Some(event), - PlatformInput::LongPress(event) => Some(event), - PlatformInput::TouchDrag(event) => Some(event), - PlatformInput::FileDrop(event) => Some(event), - PlatformInput::Touch(_) => None, - } - } - - pub(crate) fn keyboard_event(&self) -> Option<&dyn Any> { - match self { - PlatformInput::KeyDown(event) => Some(event), - PlatformInput::KeyUp(event) => Some(event), - PlatformInput::ModifiersChanged(event) => Some(event), - PlatformInput::MouseDown(_) => None, - PlatformInput::MouseUp(_) => None, - PlatformInput::MouseMove(_) => None, - PlatformInput::MousePressure(_) => None, - PlatformInput::MouseExited(_) => None, - PlatformInput::ScrollWheel(_) => None, - PlatformInput::Pinch(_) => None, - PlatformInput::LongPress(_) => None, - PlatformInput::TouchDrag(_) => None, - PlatformInput::FileDrop(_) => None, - PlatformInput::Touch(_) => None, - } - } - - /// A short static name for this input's variant, for diagnostics and - /// telemetry. - pub fn kind_name(&self) -> &'static str { - match self { - PlatformInput::KeyDown(_) => "key_down", - PlatformInput::KeyUp(_) => "key_up", - PlatformInput::ModifiersChanged(_) => "modifiers_changed", - PlatformInput::MouseDown(_) => "mouse_down", - PlatformInput::MouseUp(_) => "mouse_up", - PlatformInput::MousePressure(_) => "mouse_pressure", - PlatformInput::MouseMove(_) => "mouse_move", - PlatformInput::MouseExited(_) => "mouse_exited", - PlatformInput::ScrollWheel(_) => "scroll_wheel", - PlatformInput::Pinch(_) => "pinch", - PlatformInput::LongPress(_) => "long_press", - PlatformInput::TouchDrag(_) => "touch_drag", - PlatformInput::FileDrop(_) => "file_drop", - PlatformInput::Touch(_) => "touch", - } - } - - /// Returns the touch event contained in this input, if any. - pub fn touch_event(&self) -> Option<&TouchEvent> { - match self { - PlatformInput::Touch(event) => Some(event), - _ => None, - } - } -} - -#[cfg(test)] -mod test { - - use crate::{ - self as gpui, AppContext as _, Context, FocusHandle, InteractiveElement, IntoElement, - KeyBinding, Keystroke, Modifiers, ParentElement, Render, TestAppContext, Window, div, - }; - - struct TestView { - saw_key_down: bool, - saw_action: bool, - focus_handle: FocusHandle, - } - - actions!(test_only, [TestAction]); - - impl Render for TestView { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - div().id("testview").child( - div() - .key_context("parent") - .on_key_down(cx.listener(|this, _, _, cx| { - cx.stop_propagation(); - this.saw_key_down = true - })) - .on_action(cx.listener(|this: &mut TestView, _: &TestAction, _, _| { - this.saw_action = true - })) - .child( - div() - .key_context("nested") - .track_focus(&self.focus_handle) - .into_element(), - ), - ) - } - } - - #[gpui::test] - fn test_on_events(cx: &mut TestAppContext) { - let window = cx.update(|cx| { - cx.open_window(Default::default(), |_, cx| { - cx.new(|cx| TestView { - saw_key_down: false, - saw_action: false, - focus_handle: cx.focus_handle(), - }) - }) - .unwrap() - }); - - cx.update(|cx| { - cx.bind_keys(vec![KeyBinding::new("ctrl-g", TestAction, Some("parent"))]); - }); - - window - .update(cx, |test_view, window, cx| { - window.focus(&test_view.focus_handle, cx) - }) - .unwrap(); - - cx.dispatch_keystroke(*window, Keystroke::parse("a").unwrap()); - cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap()); - - window - .update(cx, |test_view, _, _| { - assert!(test_view.saw_key_down || test_view.saw_action); - assert!(test_view.saw_key_down); - assert!(test_view.saw_action); - }) - .unwrap(); - } - - #[gpui::test] - fn test_multi_modifier_gesture_does_not_dispatch_standalone_modifier_binding( - cx: &mut TestAppContext, - ) { - let (test_view, cx) = cx.add_window_view(|_, cx| TestView { - saw_key_down: false, - saw_action: false, - focus_handle: cx.focus_handle(), - }); - - cx.update(|_, cx| { - cx.bind_keys(vec![KeyBinding::new("shift", TestAction, None)]); - }); - test_view.update_in(cx, |test_view, window, cx| { - window.focus(&test_view.focus_handle, cx); - }); - - cx.simulate_modifiers_change(Modifiers::alt()); - cx.simulate_modifiers_change(Modifiers::alt() | Modifiers::shift()); - cx.simulate_modifiers_change(Modifiers::shift()); - cx.simulate_modifiers_change(Modifiers::none()); - assert!(!test_view.read_with(cx, |test_view, _| test_view.saw_action)); - - cx.simulate_modifiers_change(Modifiers::shift()); - cx.simulate_modifiers_change(Modifiers::none()); - assert!(test_view.read_with(cx, |test_view, _| test_view.saw_action)); - } -} diff --git a/crates/gpui_pre/src/key_dispatch.rs b/crates/gpui_pre/src/key_dispatch.rs deleted file mode 100644 index f18bd0e..0000000 --- a/crates/gpui_pre/src/key_dispatch.rs +++ /dev/null @@ -1,1691 +0,0 @@ -//! KeyDispatch is where GPUI deals with binding actions to key events. -//! -//! The key pieces to making a key binding work are to define an action, -//! implement a method that takes that action as a type parameter, -//! and then to register the action during render on a focused node -//! with a keymap context: -//! -//! ```ignore -//! actions!(editor,[Undo, Redo]); -//! -//! impl Editor { -//! fn undo(&mut self, _: &Undo, _window: &mut Window, _cx: &mut Context) { ... } -//! fn redo(&mut self, _: &Redo, _window: &mut Window, _cx: &mut Context) { ... } -//! } -//! -//! impl Render for Editor { -//! fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { -//! div() -//! .track_focus(&self.focus_handle(cx)) -//! .key_context("Editor") -//! .on_action(cx.listener(Editor::undo)) -//! .on_action(cx.listener(Editor::redo)) -//! ... -//! } -//! } -//!``` -//! -//! The keybindings themselves are managed independently by calling cx.bind_keys(). -//! (Though mostly when developing Zed itself, you just need to add a new line to -//! assets/keymaps/default-{platform}.json). -//! -//! ```ignore -//! cx.bind_keys([ -//! KeyBinding::new("cmd-z", Editor::undo, Some("Editor")), -//! KeyBinding::new("cmd-shift-z", Editor::redo, Some("Editor")), -//! ]) -//! ``` -//! -//! With all of this in place, GPUI will ensure that if you have an Editor that contains -//! the focus, hitting cmd-z will Undo. -//! -//! In real apps, it is a little more complicated than this, because typically you have -//! several nested views that each register keyboard handlers. In this case action matching -//! bubbles up from the bottom. For example in Zed, the Workspace is the top-level view, which contains Pane's, which contain Editors. If there are conflicting keybindings defined -//! then the Editor's bindings take precedence over the Pane's bindings, which take precedence over the Workspace. -//! -//! In GPUI, keybindings are not limited to just single keystrokes, you can define -//! sequences by separating the keys with a space: -//! -//! KeyBinding::new("cmd-k left", pane::SplitLeft, Some("Pane")) - -use crate::{ - Action, ActionRegistry, App, DispatchPhase, EntityId, FocusId, KeyBinding, KeyContext, Keymap, - Keystroke, ModifiersChangedEvent, Window, -}; -use collections::FxHashMap; -use smallvec::SmallVec; -use std::{ - any::{Any, TypeId}, - cell::RefCell, - mem, - ops::Range, - rc::Rc, -}; - -/// ID of a node within `DispatchTree`. Note that these are **not** stable between frames, and so a -/// `DispatchNodeId` should only be used with the `DispatchTree` that provided it. -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] -pub(crate) struct DispatchNodeId(usize); - -pub(crate) struct DispatchTree { - node_stack: Vec, - pub(crate) context_stack: Vec, - view_stack: Vec, - nodes: Vec, - focusable_node_ids: FxHashMap, - view_node_ids: FxHashMap, - keymap: Rc>, - action_registry: Rc, -} - -#[derive(Default)] -pub(crate) struct DispatchNode { - pub key_listeners: Vec, - pub action_listeners: Vec, - pub modifiers_changed_listeners: Vec, - pub context: Option, - pub focus_id: Option, - view_id: Option, - parent: Option, -} - -pub(crate) struct ReusedSubtree { - old_range: Range, - new_range: Range, - contains_focus: bool, -} - -impl ReusedSubtree { - pub fn refresh_node_id(&self, node_id: DispatchNodeId) -> DispatchNodeId { - debug_assert!( - self.old_range.contains(&node_id.0), - "node {} was not part of the reused subtree {:?}", - node_id.0, - self.old_range - ); - DispatchNodeId((node_id.0 - self.old_range.start) + self.new_range.start) - } - - pub fn contains_focus(&self) -> bool { - self.contains_focus - } -} - -#[derive(Default, Debug)] -pub(crate) struct Replay { - pub(crate) keystroke: Keystroke, - pub(crate) bindings: SmallVec<[KeyBinding; 1]>, -} - -#[derive(Default, Debug)] -pub(crate) struct DispatchResult { - pub(crate) pending: SmallVec<[Keystroke; 1]>, - pub(crate) pending_has_binding: bool, - pub(crate) bindings: SmallVec<[KeyBinding; 1]>, - pub(crate) to_replay: SmallVec<[Replay; 1]>, - pub(crate) context_stack: Vec, -} - -type KeyListener = Rc; -type ModifiersChangedListener = Rc; - -#[derive(Clone)] -pub(crate) struct DispatchActionListener { - pub(crate) action_type: TypeId, - pub(crate) listener: Rc, -} - -impl DispatchTree { - pub fn new(keymap: Rc>, action_registry: Rc) -> Self { - Self { - node_stack: Vec::new(), - context_stack: Vec::new(), - view_stack: Vec::new(), - nodes: Vec::new(), - focusable_node_ids: FxHashMap::default(), - view_node_ids: FxHashMap::default(), - keymap, - action_registry, - } - } - - pub fn clear(&mut self) { - self.node_stack.clear(); - self.context_stack.clear(); - self.view_stack.clear(); - self.nodes.clear(); - self.focusable_node_ids.clear(); - self.view_node_ids.clear(); - } - - pub fn len(&self) -> usize { - self.nodes.len() - } - - pub fn push_node(&mut self) -> DispatchNodeId { - let parent = self.node_stack.last().copied(); - let node_id = DispatchNodeId(self.nodes.len()); - - self.nodes.push(DispatchNode { - parent, - ..Default::default() - }); - self.node_stack.push(node_id); - node_id - } - - pub fn set_active_node(&mut self, node_id: DispatchNodeId) { - let next_node_parent = self.nodes[node_id.0].parent; - while self.node_stack.last().copied() != next_node_parent && !self.node_stack.is_empty() { - self.pop_node(); - } - - if self.node_stack.last().copied() == next_node_parent { - self.node_stack.push(node_id); - let active_node = &self.nodes[node_id.0]; - if let Some(view_id) = active_node.view_id { - self.view_stack.push(view_id) - } - if let Some(context) = active_node.context.clone() { - self.context_stack.push(context); - } - } else { - debug_assert_eq!(self.node_stack.len(), 0); - - let mut current_node_id = Some(node_id); - while let Some(node_id) = current_node_id { - let node = &self.nodes[node_id.0]; - if let Some(context) = node.context.clone() { - self.context_stack.push(context); - } - if let Some(view_id) = node.view_id { - self.view_stack.push(view_id); - } - self.node_stack.push(node_id); - current_node_id = node.parent; - } - - self.context_stack.reverse(); - self.view_stack.reverse(); - self.node_stack.reverse(); - } - } - - pub fn set_key_context(&mut self, context: KeyContext) { - self.active_node().context = Some(context.clone()); - self.context_stack.push(context); - } - - pub fn set_focus_id(&mut self, focus_id: FocusId) { - let node_id = *self.node_stack.last().unwrap(); - self.nodes[node_id.0].focus_id = Some(focus_id); - self.focusable_node_ids.insert(focus_id, node_id); - } - - pub fn set_view_id(&mut self, view_id: EntityId) { - if self.view_stack.last().copied() != Some(view_id) { - let node_id = *self.node_stack.last().unwrap(); - self.nodes[node_id.0].view_id = Some(view_id); - self.view_node_ids.insert(view_id, node_id); - self.view_stack.push(view_id); - } - } - - pub fn pop_node(&mut self) { - let node = &self.nodes[self.active_node_id().unwrap().0]; - if node.context.is_some() { - self.context_stack.pop(); - } - if node.view_id.is_some() { - self.view_stack.pop(); - } - self.node_stack.pop(); - } - - fn move_node(&mut self, source: &mut DispatchNode) { - self.push_node(); - if let Some(context) = source.context.clone() { - self.set_key_context(context); - } - if let Some(focus_id) = source.focus_id { - self.set_focus_id(focus_id); - } - if let Some(view_id) = source.view_id { - self.set_view_id(view_id); - } - - let target = self.active_node(); - target.key_listeners = mem::take(&mut source.key_listeners); - target.action_listeners = mem::take(&mut source.action_listeners); - target.modifiers_changed_listeners = mem::take(&mut source.modifiers_changed_listeners); - } - - pub fn reuse_subtree( - &mut self, - old_range: Range, - source: &mut Self, - focus: Option, - ) -> ReusedSubtree { - let new_range = self.nodes.len()..self.nodes.len() + old_range.len(); - - let mut contains_focus = false; - let mut source_stack = vec![]; - for (source_node_id, source_node) in source - .nodes - .iter_mut() - .enumerate() - .skip(old_range.start) - .take(old_range.len()) - { - let source_node_id = DispatchNodeId(source_node_id); - while let Some(source_ancestor) = source_stack.last() { - if source_node.parent == Some(*source_ancestor) { - break; - } else { - source_stack.pop(); - self.pop_node(); - } - } - - source_stack.push(source_node_id); - if source_node.focus_id.is_some() && source_node.focus_id == focus { - contains_focus = true; - } - self.move_node(source_node); - } - - while !source_stack.is_empty() { - source_stack.pop(); - self.pop_node(); - } - - ReusedSubtree { - old_range, - new_range, - contains_focus, - } - } - - pub fn truncate(&mut self, index: usize) { - for node in &self.nodes[index..] { - if let Some(focus_id) = node.focus_id { - self.focusable_node_ids.remove(&focus_id); - } - - if let Some(view_id) = node.view_id { - self.view_node_ids.remove(&view_id); - } - } - self.nodes.truncate(index); - } - - pub fn on_key_event(&mut self, listener: KeyListener) { - self.active_node().key_listeners.push(listener); - } - - pub fn on_modifiers_changed(&mut self, listener: ModifiersChangedListener) { - self.active_node() - .modifiers_changed_listeners - .push(listener); - } - - pub fn on_action( - &mut self, - action_type: TypeId, - listener: Rc, - ) { - self.active_node() - .action_listeners - .push(DispatchActionListener { - action_type, - listener, - }); - } - - pub fn focus_contains(&self, parent: FocusId, child: FocusId) -> bool { - if parent == child { - return true; - } - - if let Some(parent_node_id) = self.focusable_node_ids.get(&parent) { - let mut current_node_id = self.focusable_node_ids.get(&child).copied(); - while let Some(node_id) = current_node_id { - if node_id == *parent_node_id { - return true; - } - current_node_id = self.nodes[node_id.0].parent; - } - } - false - } - - pub fn available_actions(&self, target: DispatchNodeId) -> Vec> { - let mut actions = Vec::>::new(); - for node_id in self.dispatch_path(target) { - let node = &self.nodes[node_id.0]; - for DispatchActionListener { action_type, .. } in &node.action_listeners { - if let Err(ix) = actions.binary_search_by_key(action_type, |a| a.as_any().type_id()) - { - // Intentionally silence these errors without logging. - // If an action cannot be built by default, it's not available. - let action = self.action_registry.build_action_type(action_type).ok(); - if let Some(action) = action { - actions.insert(ix, action); - } - } - } - } - actions - } - - pub fn is_action_available(&self, action: &dyn Action, target: DispatchNodeId) -> bool { - for node_id in self.dispatch_path(target) { - let node = &self.nodes[node_id.0]; - if node - .action_listeners - .iter() - .any(|listener| listener.action_type == action.as_any().type_id()) - { - return true; - } - } - false - } - - /// Returns key bindings that invoke an action on the currently focused element. Bindings are - /// returned in the order they were added. For display, the last binding should take precedence. - /// - /// Bindings are only included if they are the highest precedence match for their keystrokes, so - /// shadowed bindings are not included. - pub fn bindings_for_action( - &self, - action: &dyn Action, - context_stack: &[KeyContext], - ) -> Vec { - // Ideally this would return a `DoubleEndedIterator` to avoid `highest_precedence_*` - // methods, but this can't be done very cleanly since keymap must be borrowed. - let keymap = self.keymap.borrow(); - keymap - .bindings_for_action(action) - .filter(|binding| { - Self::binding_matches_predicate_and_not_shadowed(&keymap, binding, context_stack) - }) - .cloned() - .collect() - } - - /// Returns the highest precedence binding for the given action and context stack. This is the - /// same as the last result of `bindings_for_action`, but more efficient than getting all bindings. - pub fn highest_precedence_binding_for_action( - &self, - action: &dyn Action, - context_stack: &[KeyContext], - ) -> Option { - let keymap = self.keymap.borrow(); - keymap - .bindings_for_action(action) - .rev() - .find(|binding| { - Self::binding_matches_predicate_and_not_shadowed(&keymap, binding, context_stack) - }) - .cloned() - } - - fn binding_matches_predicate_and_not_shadowed( - keymap: &Keymap, - binding: &KeyBinding, - context_stack: &[KeyContext], - ) -> bool { - let (bindings, _) = keymap.bindings_for_input(&binding.keystrokes, context_stack); - if let Some(found) = bindings.iter().next() { - found.action.partial_eq(binding.action.as_ref()) - } else { - false - } - } - - fn bindings_for_input( - &self, - input: &[Keystroke], - dispatch_path: &SmallVec<[DispatchNodeId; 32]>, - ) -> (SmallVec<[KeyBinding; 1]>, bool, Vec) { - let context_stack: Vec = dispatch_path - .iter() - .filter_map(|node_id| self.node(*node_id).context.clone()) - .collect(); - - let (bindings, partial) = self - .keymap - .borrow() - .bindings_for_input(input, &context_stack); - (bindings, partial, context_stack) - } - - /// Find the bindings that can follow the current input sequence. - pub fn possible_next_bindings_for_input( - &self, - input: &[Keystroke], - context_stack: &[KeyContext], - ) -> Vec { - self.keymap - .borrow() - .possible_next_bindings_for_input(input, context_stack) - } - - /// dispatch_key processes the keystroke - /// input should be set to the value of `pending` from the previous call to dispatch_key. - /// This returns three instructions to the input handler: - /// - bindings: any bindings to execute before processing this keystroke - /// - pending: the new set of pending keystrokes to store - /// - to_replay: any keystroke that had been pushed to pending, but are no-longer matched, - /// these should be replayed first. - pub fn dispatch_key( - &mut self, - mut input: SmallVec<[Keystroke; 1]>, - keystroke: Keystroke, - dispatch_path: &SmallVec<[DispatchNodeId; 32]>, - ) -> DispatchResult { - input.push(keystroke.clone()); - let (bindings, pending, context_stack) = self.bindings_for_input(&input, dispatch_path); - - if pending { - return DispatchResult { - pending: input, - pending_has_binding: !bindings.is_empty(), - context_stack, - ..Default::default() - }; - } else if !bindings.is_empty() { - return DispatchResult { - bindings, - context_stack, - ..Default::default() - }; - } else if input.len() == 1 { - return DispatchResult { - context_stack, - ..Default::default() - }; - } - input.pop(); - - let (suffix, mut to_replay) = self.replay_prefix(input, dispatch_path); - - let mut result = self.dispatch_key(suffix, keystroke, dispatch_path); - to_replay.extend(result.to_replay); - result.to_replay = to_replay; - result - } - - /// If the user types a matching prefix of a binding and then waits for a timeout - /// flush_dispatch() converts any previously pending input to replay events. - pub fn flush_dispatch( - &mut self, - input: SmallVec<[Keystroke; 1]>, - dispatch_path: &SmallVec<[DispatchNodeId; 32]>, - ) -> SmallVec<[Replay; 1]> { - let (suffix, mut to_replay) = self.replay_prefix(input, dispatch_path); - - if !suffix.is_empty() { - to_replay.extend(self.flush_dispatch(suffix, dispatch_path)) - } - - to_replay - } - - /// Converts the longest prefix of input to a replay event and returns the rest. - fn replay_prefix( - &self, - mut input: SmallVec<[Keystroke; 1]>, - dispatch_path: &SmallVec<[DispatchNodeId; 32]>, - ) -> (SmallVec<[Keystroke; 1]>, SmallVec<[Replay; 1]>) { - let mut to_replay: SmallVec<[Replay; 1]> = Default::default(); - for last in (0..input.len()).rev() { - let (bindings, _, _) = self.bindings_for_input(&input[0..=last], dispatch_path); - if !bindings.is_empty() { - to_replay.push(Replay { - keystroke: input.drain(0..=last).next_back().unwrap(), - bindings, - }); - break; - } - } - if to_replay.is_empty() { - to_replay.push(Replay { - keystroke: input.remove(0), - ..Default::default() - }); - } - (input, to_replay) - } - - pub fn dispatch_path(&self, target: DispatchNodeId) -> SmallVec<[DispatchNodeId; 32]> { - let mut dispatch_path: SmallVec<[DispatchNodeId; 32]> = SmallVec::new(); - let mut current_node_id = Some(target); - while let Some(node_id) = current_node_id { - dispatch_path.push(node_id); - current_node_id = self.nodes.get(node_id.0).and_then(|node| node.parent); - } - dispatch_path.reverse(); // Reverse the path so it goes from the root to the focused node. - dispatch_path - } - - pub fn focus_path(&self, focus_id: FocusId) -> SmallVec<[FocusId; 8]> { - let mut focus_path: SmallVec<[FocusId; 8]> = SmallVec::new(); - let mut current_node_id = self.focusable_node_ids.get(&focus_id).copied(); - while let Some(node_id) = current_node_id { - let node = self.node(node_id); - if let Some(focus_id) = node.focus_id { - focus_path.push(focus_id); - } - current_node_id = node.parent; - } - focus_path.reverse(); // Reverse the path so it goes from the root to the focused node. - focus_path - } - - pub fn view_path_reversed(&self, view_id: EntityId) -> impl Iterator { - let mut current_node_id = self.view_node_ids.get(&view_id).copied(); - - std::iter::successors( - current_node_id.map(|node_id| self.node(node_id)), - |node_id| Some(self.node(node_id.parent?)), - ) - .filter_map(|node| node.view_id) - } - - pub fn node(&self, node_id: DispatchNodeId) -> &DispatchNode { - &self.nodes[node_id.0] - } - - fn active_node(&mut self) -> &mut DispatchNode { - let active_node_id = self.active_node_id().unwrap(); - &mut self.nodes[active_node_id.0] - } - - pub fn focusable_node_id(&self, target: FocusId) -> Option { - self.focusable_node_ids.get(&target).copied() - } - - pub fn root_node_id(&self) -> DispatchNodeId { - debug_assert!(!self.nodes.is_empty()); - DispatchNodeId(0) - } - - pub fn active_node_id(&self) -> Option { - self.node_stack.last().copied() - } -} - -#[cfg(test)] -mod tests { - use crate::{ - self as gpui, AppContext, DispatchResult, Element, ElementId, GlobalElementId, - InspectorElementId, Keystroke, LayoutId, Style, - }; - use core::panic; - use smallvec::SmallVec; - use std::{ - cell::{Cell, RefCell}, - ops::Range, - rc::Rc, - }; - - use crate::{ - ActionRegistry, App, Bounds, Context, DispatchPhase, DispatchTree, FocusHandle, - InputHandler, IntoElement, KeyBinding, KeyContext, Keymap, Pixels, PlatformWindow, Point, - Render, Subscription, TestAppContext, UTF16Selection, Unbind, VisualContext, - VisualTestContext, Window, - }; - - actions!(dispatch_test, [TestAction, SecondaryTestAction]); - - fn test_dispatch_tree(bindings: Vec) -> DispatchTree { - let registry = ActionRegistry::default(); - - DispatchTree::new( - Rc::new(RefCell::new(Keymap::new(bindings))), - Rc::new(registry), - ) - } - - struct PendingInputTestView { - focus_handle: FocusHandle, - action_count: Rc>, - secondary_action_count: Rc>, - } - - #[derive(Clone)] - struct PendingTextInputTestView { - focus_handle: FocusHandle, - text: Rc>, - action_count: Rc>, - } - - impl PendingTextInputTestView { - fn new(cx: &mut Context) -> Self { - Self { - focus_handle: cx.focus_handle(), - text: Rc::default(), - action_count: Rc::default(), - } - } - } - - impl Element for PendingTextInputTestView { - type RequestLayoutState = (); - type PrepaintState = (); - - fn id(&self) -> Option { - Some("pending-text-input-test".into()) - } - - fn source_location(&self) -> Option<&'static panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _: Option<&GlobalElementId>, - _: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - (window.request_layout(Style::default(), [], cx), ()) - } - - fn prepaint( - &mut self, - _: Option<&GlobalElementId>, - _: Option<&InspectorElementId>, - _: Bounds, - _: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState { - window.set_focus_handle(&self.focus_handle, cx); - } - - fn paint( - &mut self, - _: Option<&GlobalElementId>, - _: Option<&InspectorElementId>, - _: Bounds, - _: &mut Self::RequestLayoutState, - _: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - let mut key_context = KeyContext::default(); - key_context.add("Terminal"); - window.set_key_context(key_context); - window.handle_input(&self.focus_handle, self.clone(), cx); - let action_count = self.action_count.clone(); - window.on_action( - std::any::TypeId::of::(), - move |_, phase, _, _| { - if phase == DispatchPhase::Bubble { - action_count.set(action_count.get() + 1); - } - }, - ); - } - } - - impl IntoElement for PendingTextInputTestView { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } - } - - impl InputHandler for PendingTextInputTestView { - fn selected_text_range( - &mut self, - _: bool, - _: &mut Window, - _: &mut App, - ) -> Option { - None - } - - fn marked_text_range(&mut self, _: &mut Window, _: &mut App) -> Option> { - None - } - - fn text_for_range( - &mut self, - _: Range, - _: &mut Option>, - _: &mut Window, - _: &mut App, - ) -> Option { - None - } - - fn replace_text_in_range( - &mut self, - replacement_range: Option>, - text: &str, - _: &mut Window, - _: &mut App, - ) { - if replacement_range.is_some() { - unimplemented!() - } - self.text.borrow_mut().push_str(text) - } - - fn replace_and_mark_text_in_range( - &mut self, - replacement_range: Option>, - new_text: &str, - _: Option>, - _: &mut Window, - _: &mut App, - ) { - if replacement_range.is_some() { - unimplemented!() - } - self.text.borrow_mut().push_str(new_text) - } - - fn unmark_text(&mut self, _: &mut Window, _: &mut App) {} - - fn prefers_ime_for_printable_keys(&mut self, _: &mut Window, _: &mut App) -> bool { - true - } - - fn bounds_for_range( - &mut self, - _: Range, - _: &mut Window, - _: &mut App, - ) -> Option> { - None - } - - fn character_index_for_point( - &mut self, - _: Point, - _: &mut Window, - _: &mut App, - ) -> Option { - None - } - } - - impl Render for PendingTextInputTestView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - self.clone() - } - } - - struct PendingInputTimeoutPauseOwner; - - impl Render for PendingInputTestView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - use crate::{InteractiveElement as _, Styled as _}; - let action_count = self.action_count.clone(); - let secondary_action_count = self.secondary_action_count.clone(); - crate::div() - .key_context("Terminal") - .track_focus(&self.focus_handle) - .size_full() - .on_action(move |_: &TestAction, _, _| { - action_count.set(action_count.get() + 1); - }) - .on_action(move |_: &SecondaryTestAction, _, _| { - secondary_action_count.set(secondary_action_count.get() + 1); - }) - } - } - - fn setup_pending_input_test( - cx: &mut TestAppContext, - bindings: impl IntoIterator, - ) -> (&mut VisualTestContext, Rc>, Rc>) { - cx.update(|cx| cx.bind_keys(bindings)); - - let action_count = Rc::new(Cell::new(0)); - let secondary_action_count = Rc::new(Cell::new(0)); - let (view, cx) = cx.add_window_view(|_, cx| PendingInputTestView { - focus_handle: cx.focus_handle(), - action_count: action_count.clone(), - secondary_action_count: secondary_action_count.clone(), - }); - let focus_handle = cx.update(|_, cx| view.read(cx).focus_handle.clone()); - cx.update(|window, cx| { - window.focus(&focus_handle, cx); - window.activate_window(); - }); - - (cx, action_count, secondary_action_count) - } - - fn setup_pending_input_timeout_test( - cx: &mut TestAppContext, - ) -> (&mut VisualTestContext, Rc>, Rc>) { - setup_pending_input_test( - cx, - [ - KeyBinding::new("ctrl-b", TestAction, Some("Terminal")), - KeyBinding::new("ctrl-b h", SecondaryTestAction, Some("Terminal")), - KeyBinding::new("ctrl-b h j", TestAction, Some("Terminal")), - ], - ) - } - - fn query_prefers_ime_for_printable_keys(cx: &mut VisualTestContext) -> Option { - let mut platform_window = cx.test_window(cx.window_handle()); - let mut input_handler = platform_window.take_input_handler()?; - let prefers_ime = input_handler.query_prefers_ime_for_printable_keys(); - platform_window.set_input_handler(input_handler); - Some(prefers_ime) - } - - fn simulate_pending_binding(cx: &mut VisualTestContext) { - cx.simulate_modifiers_change(crate::Modifiers::control()); - cx.simulate_keystrokes("ctrl-b"); - cx.simulate_modifiers_change(crate::Modifiers::default()); - } - - #[test] - fn test_keybinding_for_action_bounds() { - let tree = test_dispatch_tree(vec![KeyBinding::new( - "cmd-n", - TestAction, - Some("ProjectPanel"), - )]); - - let contexts = vec![ - KeyContext::parse("Workspace").unwrap(), - KeyContext::parse("ProjectPanel").unwrap(), - ]; - - let keybinding = tree.bindings_for_action(&TestAction, &contexts); - - assert!(keybinding[0].action.partial_eq(&TestAction)) - } - - #[test] - fn test_bindings_for_action_hides_targeted_unbind_in_active_context() { - let tree = test_dispatch_tree(vec![ - KeyBinding::new("tab", TestAction, Some("Editor")), - KeyBinding::new( - "tab", - Unbind("dispatch_test::TestAction".into()), - Some("Editor && edit_prediction"), - ), - KeyBinding::new( - "tab", - SecondaryTestAction, - Some("Editor && showing_completions"), - ), - ]); - - let contexts = vec![ - KeyContext::parse("Workspace").unwrap(), - KeyContext::parse("Editor showing_completions edit_prediction").unwrap(), - ]; - - let bindings = tree.bindings_for_action(&TestAction, &contexts); - assert!(bindings.is_empty()); - - let highest = tree.highest_precedence_binding_for_action(&TestAction, &contexts); - assert!(highest.is_none()); - - let fallback_bindings = tree.bindings_for_action(&SecondaryTestAction, &contexts); - assert_eq!(fallback_bindings.len(), 1); - assert!(fallback_bindings[0].action.partial_eq(&SecondaryTestAction)); - } - - #[test] - fn test_bindings_for_action_keeps_targeted_binding_outside_unbind_context() { - let tree = test_dispatch_tree(vec![ - KeyBinding::new("tab", TestAction, Some("Editor")), - KeyBinding::new( - "tab", - Unbind("dispatch_test::TestAction".into()), - Some("Editor && edit_prediction"), - ), - KeyBinding::new( - "tab", - SecondaryTestAction, - Some("Editor && showing_completions"), - ), - ]); - - let contexts = vec![ - KeyContext::parse("Workspace").unwrap(), - KeyContext::parse("Editor").unwrap(), - ]; - - let bindings = tree.bindings_for_action(&TestAction, &contexts); - assert_eq!(bindings.len(), 1); - assert!(bindings[0].action.partial_eq(&TestAction)); - - let highest = tree.highest_precedence_binding_for_action(&TestAction, &contexts); - assert!(highest.is_some_and(|binding| binding.action.partial_eq(&TestAction))); - } - - /// Models the picker preview footer scenario: a picker action is bound in - /// `Picker > Editor`, but a base keymap binds the same chord to an editor - /// action in `Editor`. `Picker > Editor` and `Editor` resolve at the same - /// context depth, so at equal depth precedence is decided purely by load - /// order (later wins). Because base keymaps load after the default keymap, - /// the picker binding is shadowed unless it is (re)bound by an overlay that - /// loads after the base keymap - which is exactly what - /// `keymaps/specific-overrides*.json` does. - #[test] - fn test_overlay_after_base_restores_shadowed_picker_binding() { - // SecondaryTestAction stands in for the editor/base action (e.g. - // editor::AddSelectionBelow), TestAction for the picker action. - let contexts = vec![ - KeyContext::parse("Picker").unwrap(), - KeyContext::parse("Editor").unwrap(), - ]; - - // Default keymap (picker binding) followed by a base keymap that binds - // the same chord to an editor action: the base binding wins and the - // picker action is shadowed, so its footer tooltip renders no shortcut. - let shadowed = test_dispatch_tree(vec![ - KeyBinding::new("ctrl-alt-down", TestAction, Some("Picker > Editor")), - KeyBinding::new("ctrl-alt-down", SecondaryTestAction, Some("Editor")), - ]); - let highest = shadowed.highest_precedence_binding_for_action(&TestAction, &contexts); - assert!( - highest.is_none(), - "picker binding should be shadowed by the base editor binding" - ); - - // Re-binding the picker action in an overlay loaded after the base keymap - // restores it as the resolved binding. - let fixed = test_dispatch_tree(vec![ - KeyBinding::new("ctrl-alt-down", TestAction, Some("Picker > Editor")), - KeyBinding::new("ctrl-alt-down", SecondaryTestAction, Some("Editor")), - // overlay loaded last: - KeyBinding::new("ctrl-alt-down", TestAction, Some("Picker > Editor")), - ]); - let highest = fixed.highest_precedence_binding_for_action(&TestAction, &contexts); - assert!( - highest.is_some_and(|binding| binding.action.partial_eq(&TestAction)), - "overlay loaded after base should restore the picker binding" - ); - - // Conversely, putting the override in the default keymap (i.e. before the - // base keymap) does NOT help: the later base binding still wins at equal - // depth. This is why the overlay must be loaded after the base keymap. - let override_before_base = test_dispatch_tree(vec![ - KeyBinding::new("ctrl-alt-down", TestAction, Some("Picker > Editor")), - KeyBinding::new("ctrl-alt-down", TestAction, Some("Picker > Editor")), - KeyBinding::new("ctrl-alt-down", SecondaryTestAction, Some("Editor")), - ]); - let highest = - override_before_base.highest_precedence_binding_for_action(&TestAction, &contexts); - assert!( - highest.is_none(), - "an override loaded before the base binding cannot win the equal-depth tie" - ); - } - - #[test] - fn test_pending_has_binding_state() { - let bindings = vec![ - KeyBinding::new("ctrl-b h", TestAction, None), - KeyBinding::new("space", TestAction, Some("ContextA")), - KeyBinding::new("space f g", TestAction, Some("ContextB")), - ]; - let mut tree = test_dispatch_tree(bindings); - - type DispatchPath = SmallVec<[super::DispatchNodeId; 32]>; - fn dispatch( - tree: &mut DispatchTree, - pending: SmallVec<[Keystroke; 1]>, - key: &str, - path: &DispatchPath, - ) -> DispatchResult { - tree.dispatch_key(pending, Keystroke::parse(key).unwrap(), path) - } - - let dispatch_path: DispatchPath = SmallVec::new(); - let result = dispatch(&mut tree, SmallVec::new(), "ctrl-b", &dispatch_path); - assert_eq!(result.pending.len(), 1); - assert!(!result.pending_has_binding); - - let result = dispatch(&mut tree, result.pending, "h", &dispatch_path); - assert_eq!(result.pending.len(), 0); - assert_eq!(result.bindings.len(), 1); - assert!(!result.pending_has_binding); - - let node_id = tree.push_node(); - tree.set_key_context(KeyContext::parse("ContextB").unwrap()); - tree.pop_node(); - - let dispatch_path = tree.dispatch_path(node_id); - let result = dispatch(&mut tree, SmallVec::new(), "space", &dispatch_path); - - assert_eq!(result.pending.len(), 1); - assert!(!result.pending_has_binding); - } - - #[crate::test] - fn test_pending_input_observers_notified_on_focus_change_and_blur(cx: &mut TestAppContext) { - #[derive(Clone)] - struct CustomElement { - focus_handle: FocusHandle, - text: Rc>, - } - - impl CustomElement { - fn new(cx: &mut Context) -> Self { - Self { - focus_handle: cx.focus_handle(), - text: Rc::default(), - } - } - } - - impl Element for CustomElement { - type RequestLayoutState = (); - - type PrepaintState = (); - - fn id(&self) -> Option { - Some("custom".into()) - } - - fn source_location(&self) -> Option<&'static panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _: Option<&GlobalElementId>, - _: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - (window.request_layout(Style::default(), [], cx), ()) - } - - fn prepaint( - &mut self, - _: Option<&GlobalElementId>, - _: Option<&InspectorElementId>, - _: Bounds, - _: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState { - window.set_focus_handle(&self.focus_handle, cx); - } - - fn paint( - &mut self, - _: Option<&GlobalElementId>, - _: Option<&InspectorElementId>, - _: Bounds, - _: &mut Self::RequestLayoutState, - _: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - let mut key_context = KeyContext::default(); - key_context.add("Terminal"); - window.set_key_context(key_context); - window.handle_input(&self.focus_handle, self.clone(), cx); - window.on_action(std::any::TypeId::of::(), |_, _, _, _| {}); - } - } - - impl IntoElement for CustomElement { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } - } - - impl InputHandler for CustomElement { - fn selected_text_range( - &mut self, - _: bool, - _: &mut Window, - _: &mut App, - ) -> Option { - None - } - - fn marked_text_range(&mut self, _: &mut Window, _: &mut App) -> Option> { - None - } - - fn text_for_range( - &mut self, - _: Range, - _: &mut Option>, - _: &mut Window, - _: &mut App, - ) -> Option { - None - } - - fn replace_text_in_range( - &mut self, - replacement_range: Option>, - text: &str, - _: &mut Window, - _: &mut App, - ) { - if replacement_range.is_some() { - unimplemented!() - } - self.text.borrow_mut().push_str(text) - } - - fn replace_and_mark_text_in_range( - &mut self, - replacement_range: Option>, - new_text: &str, - _: Option>, - _: &mut Window, - _: &mut App, - ) { - if replacement_range.is_some() { - unimplemented!() - } - self.text.borrow_mut().push_str(new_text) - } - - fn unmark_text(&mut self, _: &mut Window, _: &mut App) {} - - fn bounds_for_range( - &mut self, - _: Range, - _: &mut Window, - _: &mut App, - ) -> Option> { - None - } - - fn character_index_for_point( - &mut self, - _: Point, - _: &mut Window, - _: &mut App, - ) -> Option { - None - } - } - - impl Render for CustomElement { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - self.clone() - } - } - - cx.update(|cx| { - cx.bind_keys([KeyBinding::new("ctrl-b", TestAction, Some("Terminal"))]); - cx.bind_keys([KeyBinding::new("ctrl-b h", TestAction, Some("Terminal"))]); - cx.bind_keys([KeyBinding::new("ctrl-d", TestAction, None)]); - cx.bind_keys([KeyBinding::new("ctrl-d h", TestAction, None)]); - }); - - let (test, cx) = cx.add_window_view(|_, cx| CustomElement::new(cx)); - let focus_handle = test.update(cx, |test, _| test.focus_handle.clone()); - - let pending_input_changed_count = Rc::new(RefCell::new(0usize)); - let pending_input_changed_count_for_observer = pending_input_changed_count.clone(); - - struct PendingInputObserver { - _subscription: Subscription, - } - - let _observer = cx.update(|window, cx| { - cx.new(|cx| PendingInputObserver { - _subscription: cx.observe_pending_input(window, move |_, _, _| { - *pending_input_changed_count_for_observer.borrow_mut() += 1; - }), - }) - }); - - cx.update(|window, cx| { - window.focus(&focus_handle, cx); - window.activate_window(); - }); - - cx.simulate_keystrokes("ctrl-b"); - - let count_after_pending = Rc::new(RefCell::new(0usize)); - let count_after_pending_for_assertion = count_after_pending.clone(); - - cx.update(|window, cx| { - assert!(window.has_pending_keystrokes()); - *count_after_pending.borrow_mut() = *pending_input_changed_count.borrow(); - assert!(*count_after_pending.borrow() > 0); - - window.focus(&cx.focus_handle(), cx); - - assert!(!window.has_pending_keystrokes()); - }); - - // Focus-triggered pending-input notifications are deferred to the end of the current - // effect cycle, so the observer callback should run after the focus update completes. - cx.update(|_, _| { - let count_after_focus_change = *pending_input_changed_count.borrow(); - assert!(count_after_focus_change > *count_after_pending_for_assertion.borrow()); - }); - - cx.update(|window, cx| window.focus(&focus_handle, cx)); - cx.simulate_keystrokes("ctrl-b"); - let count_before_blur = *pending_input_changed_count.borrow(); - - cx.update(|window, cx| { - assert!(window.has_pending_keystrokes()); - window.blur(cx); - assert!(!window.has_pending_keystrokes()); - assert!(window.pending_input_is_none()); - }); - - cx.update(|_, _| { - assert!(*pending_input_changed_count.borrow() > count_before_blur); - }); - - cx.update(|window, cx| window.disable_focus(cx)); - cx.simulate_keystrokes("ctrl-d"); - - cx.update(|window, cx| { - assert!(window.has_pending_keystrokes()); - window.blur(cx); - assert!(window.pending_input_is_none()); - }); - } - - #[crate::test] - fn test_printable_pending_input_replays_on_timeout(cx: &mut TestAppContext) { - cx.update(|cx| { - cx.bind_keys([KeyBinding::new("j k", TestAction, Some("Terminal"))]); - }); - let (test, cx) = cx.add_window_view(|_, cx| PendingTextInputTestView::new(cx)); - let focus_handle = test.update(cx, |test, _| test.focus_handle.clone()); - cx.update(|window, cx| { - window.focus(&focus_handle, cx); - window.activate_window(); - }); - - cx.simulate_keystrokes("j"); - - cx.update(|window, _| { - let pending_input = window.pending_input().expect("pending input"); - assert_eq!(pending_input.keystrokes().len(), 1); - assert!(pending_input.timeout().is_some()); - }); - test.update(cx, |test, _| { - assert_eq!(test.action_count.get(), 0); - assert_eq!(test.text.borrow().as_str(), ""); - }); - - cx.executor().advance_clock(crate::PENDING_INPUT_TIMEOUT); - cx.run_until_parked(); - - cx.update(|window, _| assert!(!window.has_pending_keystrokes())); - test.update(cx, |test, _| { - assert_eq!(test.action_count.get(), 0); - assert_eq!(test.text.borrow().as_str(), "j"); - }); - } - - #[crate::test] - fn test_pending_input_timeout_dispatches_shorter_binding(cx: &mut TestAppContext) { - let (cx, action_count, secondary_action_count) = setup_pending_input_timeout_test(cx); - simulate_pending_binding(cx); - cx.update(|window, _| { - assert_eq!( - window - .pending_input() - .map(|pending_input| pending_input.keystrokes().len()), - Some(1) - ); - assert_eq!( - window - .pending_input() - .and_then(|pending_input| pending_input.timeout()) - .map(|timeout| timeout.duration()), - Some(crate::PENDING_INPUT_TIMEOUT) - ); - }); - assert_eq!(action_count.get(), 0); - - // Emulate a countdown indicator re-rendering the window while waiting for the timeout. - for _ in 0..10 { - cx.executor() - .advance_clock(crate::PENDING_INPUT_TIMEOUT / 10); - cx.update(|window, _| window.refresh()); - cx.run_until_parked(); - } - - cx.update(|window, _| assert!(!window.has_pending_keystrokes())); - assert_eq!(action_count.get(), 1); - assert_eq!(secondary_action_count.get(), 0); - } - - #[crate::test] - fn test_running_pending_input_timeout_resets_when_binding_advances(cx: &mut TestAppContext) { - let (cx, action_count, secondary_action_count) = setup_pending_input_timeout_test(cx); - simulate_pending_binding(cx); - - cx.executor() - .advance_clock(crate::PENDING_INPUT_TIMEOUT * 4 / 5); - cx.run_until_parked(); - cx.simulate_keystrokes("h"); - cx.run_until_parked(); - - cx.update(|window, cx| { - let pending_input = window.pending_input().expect("pending input"); - let timeout = pending_input.timeout().expect("pending input timeout"); - assert_eq!(pending_input.keystrokes().len(), 2); - assert!(!timeout.is_paused()); - assert_eq!(timeout.remaining(cx), crate::PENDING_INPUT_TIMEOUT); - }); - - cx.executor() - .advance_clock(crate::PENDING_INPUT_TIMEOUT / 5); - cx.run_until_parked(); - cx.update(|window, _| assert!(window.has_pending_keystrokes())); - assert_eq!(action_count.get(), 0); - assert_eq!(secondary_action_count.get(), 0); - - cx.executor() - .advance_clock(crate::PENDING_INPUT_TIMEOUT * 4 / 5); - cx.run_until_parked(); - cx.update(|window, _| assert!(!window.has_pending_keystrokes())); - assert_eq!(action_count.get(), 0); - assert_eq!(secondary_action_count.get(), 1); - } - - #[crate::test] - fn test_pending_input_timeout_starts_when_binding_becomes_ambiguous(cx: &mut TestAppContext) { - let (cx, action_count, secondary_action_count) = setup_pending_input_test( - cx, - [ - KeyBinding::new("ctrl-b h", SecondaryTestAction, Some("Terminal")), - KeyBinding::new("ctrl-b h j", TestAction, Some("Terminal")), - ], - ); - simulate_pending_binding(cx); - - cx.update(|window, _| { - let pending_input = window.pending_input().expect("pending input"); - assert_eq!(pending_input.keystrokes().len(), 1); - assert!(pending_input.timeout().is_none()); - }); - - cx.simulate_keystrokes("h"); - cx.run_until_parked(); - cx.update(|window, cx| { - let pending_input = window.pending_input().expect("pending input"); - let timeout = pending_input.timeout().expect("pending input timeout"); - assert_eq!(pending_input.keystrokes().len(), 2); - assert_eq!(timeout.remaining(cx), crate::PENDING_INPUT_TIMEOUT); - }); - - cx.executor().advance_clock(crate::PENDING_INPUT_TIMEOUT); - cx.run_until_parked(); - cx.update(|window, _| assert!(!window.has_pending_keystrokes())); - assert_eq!(action_count.get(), 0); - assert_eq!(secondary_action_count.get(), 1); - } - - #[crate::test] - fn test_invalid_continuation_while_timeout_paused_replays_pending_input( - cx: &mut TestAppContext, - ) { - let (cx, action_count, secondary_action_count) = setup_pending_input_test( - cx, - [ - KeyBinding::new("ctrl-b", TestAction, Some("Terminal")), - KeyBinding::new("ctrl-b h", SecondaryTestAction, Some("Terminal")), - KeyBinding::new("x", SecondaryTestAction, Some("Terminal")), - ], - ); - simulate_pending_binding(cx); - let pause_owner = cx.update(|_, cx| cx.new(|_| PendingInputTimeoutPauseOwner)); - cx.update(|window, cx| { - assert!(window.set_pending_input_timeout_paused(&pause_owner, true, cx)); - }); - cx.run_until_parked(); - - cx.simulate_keystrokes("x"); - cx.run_until_parked(); - - cx.update(|window, _| assert!(!window.has_pending_keystrokes())); - assert_eq!(action_count.get(), 1); - assert_eq!(secondary_action_count.get(), 1); - - drop(pause_owner); - cx.update(|_, _| {}); - cx.run_until_parked(); - cx.executor().advance_clock(crate::PENDING_INPUT_TIMEOUT); - cx.run_until_parked(); - - cx.update(|window, _| assert!(window.pending_input_is_none())); - assert_eq!(action_count.get(), 1); - assert_eq!(secondary_action_count.get(), 1); - } - - #[crate::test] - fn test_pending_input_timeout_pauses_and_resumes(cx: &mut TestAppContext) { - let (cx, action_count, secondary_action_count) = setup_pending_input_timeout_test(cx); - simulate_pending_binding(cx); - - cx.executor() - .advance_clock(crate::PENDING_INPUT_TIMEOUT * 7 / 10); - cx.run_until_parked(); - - let pause_owner = cx.update(|_, cx| cx.new(|_| PendingInputTimeoutPauseOwner)); - let other_owner = cx.update(|_, cx| cx.new(|_| PendingInputTimeoutPauseOwner)); - cx.update(|window, cx| { - assert!(window.set_pending_input_timeout_paused(&pause_owner, true, cx)); - assert!(!window.set_pending_input_timeout_paused(&pause_owner, true, cx)); - assert!(!window.set_pending_input_timeout_paused(&other_owner, false, cx)); - }); - cx.run_until_parked(); - cx.update(|window, cx| { - let timeout = window - .pending_input() - .and_then(|pending_input| pending_input.timeout()) - .expect("pending input timeout"); - assert!(timeout.is_paused()); - assert_eq!(timeout.remaining(cx), crate::PENDING_INPUT_TIMEOUT * 3 / 10); - }); - - cx.executor() - .advance_clock(crate::PENDING_INPUT_TIMEOUT * 2); - cx.run_until_parked(); - cx.update(|window, _| assert!(window.has_pending_keystrokes())); - assert_eq!(action_count.get(), 0); - - cx.update(|window, cx| { - assert!(window.set_pending_input_timeout_paused(&pause_owner, false, cx)); - }); - cx.run_until_parked(); - cx.update(|window, cx| { - let timeout = window - .pending_input() - .and_then(|pending_input| pending_input.timeout()) - .expect("pending input timeout"); - assert!(!timeout.is_paused()); - assert_eq!(timeout.remaining(cx), crate::PENDING_INPUT_TIMEOUT * 3 / 10); - }); - - cx.executor() - .advance_clock(crate::PENDING_INPUT_TIMEOUT * 3 / 10); - cx.run_until_parked(); - - cx.update(|window, _| assert!(!window.has_pending_keystrokes())); - assert_eq!(action_count.get(), 1); - assert_eq!(secondary_action_count.get(), 0); - } - - #[crate::test] - fn test_pending_input_timeout_resumes_when_owner_is_released(cx: &mut TestAppContext) { - let (cx, action_count, secondary_action_count) = setup_pending_input_timeout_test(cx); - simulate_pending_binding(cx); - - cx.executor() - .advance_clock(crate::PENDING_INPUT_TIMEOUT * 7 / 10); - cx.run_until_parked(); - - let pause_owner = cx.update(|_, cx| cx.new(|_| PendingInputTimeoutPauseOwner)); - cx.update(|window, cx| { - assert!(window.set_pending_input_timeout_paused(&pause_owner, true, cx)); - }); - cx.run_until_parked(); - - drop(pause_owner); - cx.update(|_, _| {}); - cx.run_until_parked(); - cx.update(|window, cx| { - let timeout = window - .pending_input() - .and_then(|pending_input| pending_input.timeout()) - .expect("pending input timeout"); - assert!(!timeout.is_paused()); - assert_eq!(timeout.remaining(cx), crate::PENDING_INPUT_TIMEOUT * 3 / 10); - }); - - cx.executor() - .advance_clock(crate::PENDING_INPUT_TIMEOUT * 3 / 10); - cx.run_until_parked(); - - cx.update(|window, _| assert!(!window.has_pending_keystrokes())); - assert_eq!(action_count.get(), 1); - assert_eq!(secondary_action_count.get(), 0); - } - - #[crate::test] - fn test_pending_input_timeout_resets_when_binding_advances(cx: &mut TestAppContext) { - let (cx, action_count, secondary_action_count) = setup_pending_input_timeout_test(cx); - simulate_pending_binding(cx); - cx.executor() - .advance_clock(crate::PENDING_INPUT_TIMEOUT / 2); - cx.run_until_parked(); - let pause_owner = cx.update(|_, cx| cx.new(|_| PendingInputTimeoutPauseOwner)); - cx.update(|window, cx| { - assert!(window.set_pending_input_timeout_paused(&pause_owner, true, cx)); - }); - cx.run_until_parked(); - cx.update(|window, cx| { - let timeout = window - .pending_input() - .and_then(|pending_input| pending_input.timeout()) - .expect("pending input timeout"); - assert_eq!(timeout.remaining(cx), crate::PENDING_INPUT_TIMEOUT / 2); - }); - - cx.simulate_keystrokes("h"); - cx.run_until_parked(); - cx.update(|window, cx| { - let pending_input = window.pending_input().expect("pending input"); - let timeout = pending_input.timeout().expect("pending input timeout"); - assert_eq!(pending_input.keystrokes().len(), 2); - assert!(timeout.is_paused()); - assert_eq!(timeout.remaining(cx), crate::PENDING_INPUT_TIMEOUT); - }); - - cx.executor() - .advance_clock(crate::PENDING_INPUT_TIMEOUT * 2); - cx.run_until_parked(); - cx.update(|window, _| assert!(window.has_pending_keystrokes())); - assert_eq!(action_count.get(), 0); - assert_eq!(secondary_action_count.get(), 0); - - cx.update(|window, cx| { - assert!(window.set_pending_input_timeout_paused(&pause_owner, false, cx)); - }); - cx.run_until_parked(); - cx.executor().advance_clock(crate::PENDING_INPUT_TIMEOUT); - cx.run_until_parked(); - - cx.update(|window, _| assert!(!window.has_pending_keystrokes())); - assert_eq!(action_count.get(), 0); - assert_eq!(secondary_action_count.get(), 1); - } - - #[crate::test] - fn test_clearing_pending_input_invalidates_timeout_pause(cx: &mut TestAppContext) { - let (cx, action_count, secondary_action_count) = setup_pending_input_timeout_test(cx); - simulate_pending_binding(cx); - let pause_owner = cx.update(|_, cx| cx.new(|_| PendingInputTimeoutPauseOwner)); - cx.update(|window, cx| { - assert!(window.set_pending_input_timeout_paused(&pause_owner, true, cx)); - window.focus(&cx.focus_handle(), cx); - assert!(!window.has_pending_keystrokes()); - }); - - drop(pause_owner); - cx.update(|_, _| {}); - cx.run_until_parked(); - cx.executor().advance_clock(crate::PENDING_INPUT_TIMEOUT); - cx.run_until_parked(); - - cx.update(|window, _| assert!(window.pending_input_is_none())); - assert_eq!(action_count.get(), 0); - assert_eq!(secondary_action_count.get(), 0); - } - - #[crate::test] - fn test_input_handler_pending(cx: &mut TestAppContext) { - cx.update(|cx| { - cx.bind_keys([KeyBinding::new("ctrl-b", TestAction, Some("Terminal"))]); - cx.bind_keys([KeyBinding::new("ctrl-b h", TestAction, Some("Terminal"))]); - cx.bind_keys([KeyBinding::new("ctrl-x k", TestAction, Some("Terminal"))]); - }); - let (test, cx) = cx.add_window_view(|_, cx| PendingTextInputTestView::new(cx)); - let focus_handle = test.update(cx, |test, _| test.focus_handle.clone()); - cx.update(|window, cx| { - window.focus(&focus_handle, cx); - window.activate_window(); - }); - - assert_eq!(query_prefers_ime_for_printable_keys(cx), Some(true)); - cx.simulate_keystrokes("ctrl-x"); - cx.update(|window, _| assert!(window.has_pending_keystrokes())); - assert_eq!(query_prefers_ime_for_printable_keys(cx), Some(false)); - - let prefers_ime_after_blur = { - let mut platform_window = cx.test_window(cx.window_handle()); - let mut input_handler = platform_window.take_input_handler(); - cx.update(|window, cx| { - window.blur(cx); - assert!(!window.has_pending_keystrokes()); - assert!(window.pending_input_is_none()); - }); - let prefers_ime = input_handler - .as_mut() - .map(|input_handler| input_handler.query_prefers_ime_for_printable_keys()); - if let Some(input_handler) = input_handler { - platform_window.set_input_handler(input_handler); - } - prefers_ime - }; - assert_eq!(prefers_ime_after_blur, Some(true)); - cx.update(|window, cx| window.focus(&focus_handle, cx)); - - cx.simulate_keystrokes("ctrl-x"); - assert_eq!(query_prefers_ime_for_printable_keys(cx), Some(false)); - cx.simulate_keystrokes("k"); - cx.update(|window, _| assert!(!window.has_pending_keystrokes())); - assert_eq!(query_prefers_ime_for_printable_keys(cx), Some(true)); - test.update(cx, |test, _| { - assert_eq!(test.action_count.get(), 1); - assert_eq!(test.text.borrow().as_str(), ""); - }); - - cx.simulate_keystrokes("ctrl-b ["); - test.update(cx, |test, _| assert_eq!(test.text.borrow().as_str(), "[")) - } -} diff --git a/crates/gpui_pre/src/keymap.rs b/crates/gpui_pre/src/keymap.rs deleted file mode 100644 index bef0d03..0000000 --- a/crates/gpui_pre/src/keymap.rs +++ /dev/null @@ -1,927 +0,0 @@ -mod binding; -mod context; - -pub use binding::*; -pub use context::*; - -use crate::{Action, AsKeystroke, Keystroke, Unbind, is_no_action, is_unbind}; -use collections::{HashSet, TypeIdHashMap}; -use smallvec::SmallVec; - -/// An opaque identifier of which version of the keymap is currently active. -/// The keymap's version is changed whenever bindings are added or removed. -#[derive(Copy, Clone, Eq, PartialEq, Default)] -pub struct KeymapVersion(usize); - -/// A collection of key bindings for the user's application. -#[derive(Default)] -pub struct Keymap { - bindings: Vec, - binding_indices_by_action_id: TypeIdHashMap>, - disabled_binding_indices: Vec, - version: KeymapVersion, -} - -/// Index of a binding within a keymap. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] -pub struct BindingIndex(usize); - -fn disabled_binding_matches_context(disabled_binding: &KeyBinding, binding: &KeyBinding) -> bool { - match ( - &disabled_binding.context_predicate, - &binding.context_predicate, - ) { - (None, _) => true, - (Some(_), None) => false, - (Some(disabled_predicate), Some(predicate)) => disabled_predicate.is_superset(predicate), - } -} - -fn binding_is_unbound(disabled_binding: &KeyBinding, binding: &KeyBinding) -> bool { - disabled_binding.keystrokes == binding.keystrokes - && disabled_binding - .action() - .as_any() - .downcast_ref::() - .is_some_and(|unbind| unbind.0.as_ref() == binding.action.name()) -} - -impl Keymap { - /// Create a new keymap with the given bindings. - pub fn new(bindings: Vec) -> Self { - let mut this = Self::default(); - this.add_bindings(bindings); - this - } - - /// Get the current version of the keymap. - pub fn version(&self) -> KeymapVersion { - self.version - } - - /// Add more bindings to the keymap. - pub fn add_bindings>(&mut self, bindings: T) { - for binding in bindings { - let action_id = binding.action().as_any().type_id(); - if is_no_action(&*binding.action) || is_unbind(&*binding.action) { - self.disabled_binding_indices.push(self.bindings.len()); - } else { - self.binding_indices_by_action_id - .entry(action_id) - .or_default() - .push(self.bindings.len()); - } - self.bindings.push(binding); - } - - self.version.0 += 1; - } - - /// Reset this keymap to its initial state. - pub fn clear(&mut self) { - self.bindings.clear(); - self.binding_indices_by_action_id.clear(); - self.disabled_binding_indices.clear(); - self.version.0 += 1; - } - - /// Iterate over all bindings, in the order they were added. - pub fn bindings(&self) -> impl DoubleEndedIterator + ExactSizeIterator { - self.bindings.iter() - } - - /// Iterate over all bindings for the given action, in the order they were added. For display, - /// the last binding should take precedence. - pub fn bindings_for_action<'a>( - &'a self, - action: &'a dyn Action, - ) -> impl 'a + DoubleEndedIterator { - let action_id = action.type_id(); - let binding_indices = self - .binding_indices_by_action_id - .get(&action_id) - .map_or(&[] as _, SmallVec::as_slice) - .iter(); - - binding_indices.filter_map(|ix| { - let binding = &self.bindings[*ix]; - if !binding.action().partial_eq(action) { - return None; - } - - for disabled_ix in &self.disabled_binding_indices { - if disabled_ix > ix { - let disabled_binding = &self.bindings[*disabled_ix]; - if disabled_binding.keystrokes != binding.keystrokes { - continue; - } - - if is_no_action(&*disabled_binding.action) { - if disabled_binding_matches_context(disabled_binding, binding) { - return None; - } - } else if is_unbind(&*disabled_binding.action) - && disabled_binding_matches_context(disabled_binding, binding) - && binding_is_unbound(disabled_binding, binding) - { - return None; - } - } - } - - Some(binding) - }) - } - - /// Returns all bindings that might match the input without checking context. The bindings - /// returned in precedence order (reverse of the order they were added to the keymap). - pub fn all_bindings_for_input(&self, input: &[Keystroke]) -> Vec { - self.bindings() - .rev() - .filter(|binding| { - binding - .match_keystrokes(input) - .is_some_and(|pending| !pending) - }) - .cloned() - .collect() - } - - /// Returns a list of bindings that match the given input, and a boolean indicating whether or - /// not more bindings might match if the input was longer. Bindings are returned in precedence - /// order (higher precedence first, reverse of the order they were added to the keymap). - /// - /// Precedence is defined by the depth in the tree (matches on the Editor take precedence over - /// matches on the Pane, then the Workspace, etc.). Bindings with no context are treated as the - /// same as the deepest context. - /// - /// In the case of multiple bindings at the same depth, the ones added to the keymap later take - /// precedence. User bindings are added after built-in bindings so that they take precedence. - /// - /// If a binding has been disabled with `"x": null` it will not be returned. Disabled bindings - /// are evaluated with the same precedence rules so you can disable a rule in a given context - /// only. A disabled binding only suppresses bindings from sources with equal or weaker - /// precedence: a base keymap null hides default bindings, but user bindings still apply. - pub fn bindings_for_input( - &self, - input: &[impl AsKeystroke], - context_stack: &[KeyContext], - ) -> (SmallVec<[KeyBinding; 1]>, bool) { - let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new(); - let mut pending_bindings = SmallVec::<[(BindingIndex, &KeyBinding); 1]>::new(); - - for (ix, binding) in self.bindings().enumerate().rev() { - let Some(depth) = self.binding_enabled(binding, context_stack) else { - continue; - }; - let Some(pending) = binding.match_keystrokes(input) else { - continue; - }; - - if !pending { - matched_bindings.push((depth, BindingIndex(ix), binding)); - } else { - pending_bindings.push((BindingIndex(ix), binding)); - } - } - - matched_bindings.sort_by(|(depth_a, ix_a, _), (depth_b, ix_b, _)| { - depth_b.cmp(depth_a).then(ix_b.cmp(ix_a)) - }); - - let mut bindings: SmallVec<[_; 1]> = SmallVec::new(); - let mut first_binding_index = None; - let mut unbound_bindings: Vec<&KeyBinding> = Vec::new(); - // A `NoAction` binding suppresses out-ranked bindings from sources with - // equal or weaker precedence, while bindings from stronger sources (a - // smaller meta, e.g. a user binding vs a base keymap null) still apply. - // Bindings without a meta are treated as user bindings. - let mut no_action_meta: Option = None; - - for (_, ix, binding) in matched_bindings { - let meta = binding.meta.map_or(0, |meta| meta.0); - if is_no_action(&*binding.action) { - no_action_meta = Some(no_action_meta.map_or(meta, |existing| existing.min(meta))); - continue; - } - - if no_action_meta.is_some_and(|no_action_meta| meta >= no_action_meta) { - continue; - } - - if is_unbind(&*binding.action) { - unbound_bindings.push(binding); - continue; - } - - if unbound_bindings - .iter() - .any(|disabled_binding| binding_is_unbound(disabled_binding, binding)) - { - continue; - } - - bindings.push(binding.clone()); - first_binding_index.get_or_insert(ix); - } - - let mut pending = HashSet::default(); - for (ix, binding) in pending_bindings.into_iter().rev() { - if let Some(binding_ix) = first_binding_index - && binding_ix > ix - { - continue; - } - if is_no_action(&*binding.action) || is_unbind(&*binding.action) { - pending.remove(&&binding.keystrokes); - continue; - } - pending.insert(&binding.keystrokes); - } - - (bindings, !pending.is_empty()) - } - /// Check if the given binding is enabled, given a certain key context. - /// Returns the deepest depth at which the binding matches, or None if it doesn't match. - fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option { - if let Some(predicate) = &binding.context_predicate { - predicate.depth_of(contexts) - } else { - Some(contexts.len()) - } - } - - /// Find the bindings that can follow the current input sequence. - pub fn possible_next_bindings_for_input( - &self, - input: &[Keystroke], - context_stack: &[KeyContext], - ) -> Vec { - let mut bindings = self - .bindings() - .enumerate() - .rev() - .filter_map(|(ix, binding)| { - let depth = self.binding_enabled(binding, context_stack)?; - let pending = binding.match_keystrokes(input); - match pending { - None => None, - Some(is_pending) => { - if !is_pending - || is_no_action(&*binding.action) - || is_unbind(&*binding.action) - { - return None; - } - Some((depth, BindingIndex(ix), binding)) - } - } - }) - .collect::>(); - - bindings.sort_by(|(depth_a, ix_a, _), (depth_b, ix_b, _)| { - depth_b.cmp(depth_a).then(ix_b.cmp(ix_a)) - }); - - bindings - .into_iter() - .map(|(_, _, binding)| binding.clone()) - .collect::>() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate as gpui; - use gpui::{NoAction, Unbind}; - - actions!( - test_only, - [ActionAlpha, ActionBeta, ActionGamma, ActionDelta,] - ); - - #[test] - fn test_keymap() { - let bindings = [ - KeyBinding::new("ctrl-a", ActionAlpha {}, None), - KeyBinding::new("ctrl-a", ActionBeta {}, Some("pane")), - KeyBinding::new("ctrl-a", ActionGamma {}, Some("editor && mode==full")), - ]; - - let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); - - // global bindings are enabled in all contexts - assert_eq!(keymap.binding_enabled(&bindings[0], &[]), Some(0)); - assert_eq!( - keymap.binding_enabled(&bindings[0], &[KeyContext::parse("terminal").unwrap()]), - Some(1) - ); - - // contextual bindings are enabled in contexts that match their predicate - assert_eq!( - keymap.binding_enabled(&bindings[1], &[KeyContext::parse("barf x=y").unwrap()]), - None - ); - assert_eq!( - keymap.binding_enabled(&bindings[1], &[KeyContext::parse("pane x=y").unwrap()]), - Some(1) - ); - - assert_eq!( - keymap.binding_enabled(&bindings[2], &[KeyContext::parse("editor").unwrap()]), - None - ); - assert_eq!( - keymap.binding_enabled( - &bindings[2], - &[KeyContext::parse("editor mode=full").unwrap()] - ), - Some(1) - ); - } - - #[test] - fn test_depth_precedence() { - let bindings = [ - KeyBinding::new("ctrl-a", ActionBeta {}, Some("pane")), - KeyBinding::new("ctrl-a", ActionGamma {}, Some("editor")), - ]; - - let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); - - let (result, pending) = keymap.bindings_for_input( - &[Keystroke::parse("ctrl-a").unwrap()], - &[ - KeyContext::parse("pane").unwrap(), - KeyContext::parse("editor").unwrap(), - ], - ); - - assert!(!pending); - assert_eq!(result.len(), 2); - assert!(result[0].action.partial_eq(&ActionGamma {})); - assert!(result[1].action.partial_eq(&ActionBeta {})); - } - - #[test] - fn test_keymap_disabled() { - let bindings = [ - KeyBinding::new("ctrl-a", ActionAlpha {}, Some("editor")), - KeyBinding::new("ctrl-b", ActionAlpha {}, Some("editor")), - KeyBinding::new("ctrl-a", NoAction {}, Some("editor && mode==full")), - KeyBinding::new("ctrl-b", NoAction {}, None), - ]; - - let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); - - // binding is only enabled in a specific context - assert!( - keymap - .bindings_for_input( - &[Keystroke::parse("ctrl-a").unwrap()], - &[KeyContext::parse("barf").unwrap()], - ) - .0 - .is_empty() - ); - assert!( - !keymap - .bindings_for_input( - &[Keystroke::parse("ctrl-a").unwrap()], - &[KeyContext::parse("editor").unwrap()], - ) - .0 - .is_empty() - ); - - // binding is disabled in a more specific context - assert!( - keymap - .bindings_for_input( - &[Keystroke::parse("ctrl-a").unwrap()], - &[KeyContext::parse("editor mode=full").unwrap()], - ) - .0 - .is_empty() - ); - - // binding is globally disabled - assert!( - keymap - .bindings_for_input( - &[Keystroke::parse("ctrl-b").unwrap()], - &[KeyContext::parse("barf").unwrap()], - ) - .0 - .is_empty() - ); - } - - #[test] - /// Tests for https://github.com/zed-industries/zed/issues/30259 - fn test_multiple_keystroke_binding_disabled() { - let bindings = [ - KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")), - KeyBinding::new("space w w", NoAction {}, Some("editor")), - ]; - - let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); - - let space = || Keystroke::parse("space").unwrap(); - let w = || Keystroke::parse("w").unwrap(); - - let space_w = [space(), w()]; - let space_w_w = [space(), w(), w()]; - - let workspace_context = || [KeyContext::parse("workspace").unwrap()]; - - let editor_workspace_context = || { - [ - KeyContext::parse("workspace").unwrap(), - KeyContext::parse("editor").unwrap(), - ] - }; - - // Ensure `space` results in pending input on the workspace, but not editor - let space_workspace = keymap.bindings_for_input(&[space()], &workspace_context()); - assert!(space_workspace.0.is_empty()); - assert!(space_workspace.1); - - let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); - assert!(space_editor.0.is_empty()); - assert!(!space_editor.1); - - // Ensure `space w` results in pending input on the workspace, but not editor - let space_w_workspace = keymap.bindings_for_input(&space_w, &workspace_context()); - assert!(space_w_workspace.0.is_empty()); - assert!(space_w_workspace.1); - - let space_w_editor = keymap.bindings_for_input(&space_w, &editor_workspace_context()); - assert!(space_w_editor.0.is_empty()); - assert!(!space_w_editor.1); - - // Ensure `space w w` results in the binding in the workspace, but not in the editor - let space_w_w_workspace = keymap.bindings_for_input(&space_w_w, &workspace_context()); - assert!(!space_w_w_workspace.0.is_empty()); - assert!(!space_w_w_workspace.1); - - let space_w_w_editor = keymap.bindings_for_input(&space_w_w, &editor_workspace_context()); - assert!(space_w_w_editor.0.is_empty()); - assert!(!space_w_w_editor.1); - - // Now test what happens if we have another binding defined AFTER the NoAction - // that should result in pending - let bindings = [ - KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")), - KeyBinding::new("space w w", NoAction {}, Some("editor")), - KeyBinding::new("space w x", ActionAlpha {}, Some("editor")), - ]; - let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); - - let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); - assert!(space_editor.0.is_empty()); - assert!(space_editor.1); - - // Now test what happens if we have another binding defined BEFORE the NoAction - // that should result in pending - let bindings = [ - KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")), - KeyBinding::new("space w x", ActionAlpha {}, Some("editor")), - KeyBinding::new("space w w", NoAction {}, Some("editor")), - ]; - let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); - - let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); - assert!(space_editor.0.is_empty()); - assert!(space_editor.1); - - // Now test what happens if we have another binding defined at a higher context - // that should result in pending - let bindings = [ - KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")), - KeyBinding::new("space w x", ActionAlpha {}, Some("workspace")), - KeyBinding::new("space w w", NoAction {}, Some("editor")), - ]; - let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); - - let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); - assert!(space_editor.0.is_empty()); - assert!(space_editor.1); - } - - #[test] - fn test_override_multikey() { - let bindings = [ - KeyBinding::new("ctrl-w left", ActionAlpha {}, Some("editor")), - KeyBinding::new("ctrl-w", NoAction {}, Some("editor")), - ]; - - let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); - - // Ensure `space` results in pending input on the workspace, but not editor - let (result, pending) = keymap.bindings_for_input( - &[Keystroke::parse("ctrl-w").unwrap()], - &[KeyContext::parse("editor").unwrap()], - ); - assert!(result.is_empty()); - assert!(pending); - - let bindings = [ - KeyBinding::new("ctrl-w left", ActionAlpha {}, Some("editor")), - KeyBinding::new("ctrl-w", ActionBeta {}, Some("editor")), - ]; - - let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); - - // Ensure `space` results in pending input on the workspace, but not editor - let (result, pending) = keymap.bindings_for_input( - &[Keystroke::parse("ctrl-w").unwrap()], - &[KeyContext::parse("editor").unwrap()], - ); - assert_eq!(result.len(), 1); - assert!(!pending); - } - - #[test] - fn test_simple_disable() { - let bindings = [ - KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")), - KeyBinding::new("ctrl-x", NoAction {}, Some("editor")), - ]; - - let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); - - // Ensure `space` results in pending input on the workspace, but not editor - let (result, pending) = keymap.bindings_for_input( - &[Keystroke::parse("ctrl-x").unwrap()], - &[KeyContext::parse("editor").unwrap()], - ); - assert!(result.is_empty()); - assert!(!pending); - } - - #[test] - fn test_disable_weaker_sources_only() { - const USER: KeyBindingMetaIndex = KeyBindingMetaIndex(0); - const VIM: KeyBindingMetaIndex = KeyBindingMetaIndex(1); - const BASE: KeyBindingMetaIndex = KeyBindingMetaIndex(2); - const DEFAULT: KeyBindingMetaIndex = KeyBindingMetaIndex(3); - - let editor_context = || [KeyContext::parse("editor").unwrap()]; - let ctrl_x = || [Keystroke::parse("ctrl-x").unwrap()]; - - // A base keymap null disables a default binding in the same context. - let mut keymap = Keymap::default(); - keymap.add_bindings([ - KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")).with_meta(DEFAULT), - KeyBinding::new("ctrl-x", NoAction {}, Some("editor")).with_meta(BASE), - ]); - let (result, _) = keymap.bindings_for_input(&ctrl_x(), &editor_context()); - assert!(result.is_empty()); - - // A user binding is not affected by base keymap or default nulls. - let mut keymap = Keymap::default(); - keymap.add_bindings([ - KeyBinding::new("ctrl-x", NoAction {}, Some("editor")).with_meta(DEFAULT), - KeyBinding::new("ctrl-x", NoAction {}, Some("editor")).with_meta(BASE), - KeyBinding::new("ctrl-x", ActionBeta {}, None).with_meta(USER), - ]); - let (result, _) = keymap.bindings_for_input(&ctrl_x(), &editor_context()); - assert_eq!(result.len(), 1); - assert!(result[0].action.partial_eq(&ActionBeta {})); - - // A user binding at a shallower context is not disabled by a deeper - // base keymap null. - let mut keymap = Keymap::default(); - keymap.add_bindings([ - KeyBinding::new("ctrl-x", NoAction {}, Some("editor")).with_meta(BASE), - KeyBinding::new("ctrl-x", ActionBeta {}, Some("workspace")).with_meta(USER), - ]); - let (result, _) = keymap.bindings_for_input( - &ctrl_x(), - &[ - KeyContext::parse("workspace").unwrap(), - KeyContext::parse("editor").unwrap(), - ], - ); - assert_eq!(result.len(), 1); - assert!(result[0].action.partial_eq(&ActionBeta {})); - - // A vim binding survives a base keymap null, and a user null disables - // everything. - let mut keymap = Keymap::default(); - keymap.add_bindings([ - KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")).with_meta(DEFAULT), - KeyBinding::new("ctrl-x", NoAction {}, Some("editor")).with_meta(BASE), - KeyBinding::new("ctrl-x", ActionGamma {}, Some("editor")).with_meta(VIM), - ]); - let (result, _) = keymap.bindings_for_input(&ctrl_x(), &editor_context()); - assert_eq!(result.len(), 1); - assert!(result[0].action.partial_eq(&ActionGamma {})); - - let mut keymap = Keymap::default(); - keymap.add_bindings([ - KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")).with_meta(DEFAULT), - KeyBinding::new("ctrl-x", ActionGamma {}, Some("editor")).with_meta(VIM), - KeyBinding::new("ctrl-x", NoAction {}, Some("editor")).with_meta(USER), - ]); - let (result, _) = keymap.bindings_for_input(&ctrl_x(), &editor_context()); - assert!(result.is_empty()); - } - - #[test] - fn test_fail_to_disable() { - // disabled at the wrong level - let bindings = [ - KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")), - KeyBinding::new("ctrl-x", NoAction {}, Some("workspace")), - ]; - - let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); - - // Ensure `space` results in pending input on the workspace, but not editor - let (result, pending) = keymap.bindings_for_input( - &[Keystroke::parse("ctrl-x").unwrap()], - &[ - KeyContext::parse("workspace").unwrap(), - KeyContext::parse("editor").unwrap(), - ], - ); - assert_eq!(result.len(), 1); - assert!(!pending); - } - - #[test] - fn test_disable_deeper() { - let bindings = [ - KeyBinding::new("ctrl-x", ActionAlpha {}, Some("workspace")), - KeyBinding::new("ctrl-x", NoAction {}, Some("editor")), - ]; - - let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); - - // Ensure `space` results in pending input on the workspace, but not editor - let (result, pending) = keymap.bindings_for_input( - &[Keystroke::parse("ctrl-x").unwrap()], - &[ - KeyContext::parse("workspace").unwrap(), - KeyContext::parse("editor").unwrap(), - ], - ); - assert_eq!(result.len(), 0); - assert!(!pending); - } - - #[test] - fn test_pending_match_enabled() { - let bindings = [ - KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")), - KeyBinding::new("ctrl-x 0", ActionAlpha, Some("Workspace")), - ]; - let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); - - let matched = keymap.bindings_for_input( - &[Keystroke::parse("ctrl-x")].map(Result::unwrap), - &[ - KeyContext::parse("Workspace"), - KeyContext::parse("Pane"), - KeyContext::parse("Editor vim_mode=normal"), - ] - .map(Result::unwrap), - ); - assert_eq!(matched.0.len(), 1); - assert!(matched.0[0].action.partial_eq(&ActionBeta)); - assert!(matched.1); - } - - #[test] - fn test_pending_match_enabled_extended() { - let bindings = [ - KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")), - KeyBinding::new("ctrl-x 0", NoAction, Some("Workspace")), - ]; - let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); - - let matched = keymap.bindings_for_input( - &[Keystroke::parse("ctrl-x")].map(Result::unwrap), - &[ - KeyContext::parse("Workspace"), - KeyContext::parse("Pane"), - KeyContext::parse("Editor vim_mode=normal"), - ] - .map(Result::unwrap), - ); - assert_eq!(matched.0.len(), 1); - assert!(matched.0[0].action.partial_eq(&ActionBeta)); - assert!(!matched.1); - let bindings = [ - KeyBinding::new("ctrl-x", ActionBeta, Some("Workspace")), - KeyBinding::new("ctrl-x 0", NoAction, Some("vim_mode == normal")), - ]; - let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); - - let matched = keymap.bindings_for_input( - &[Keystroke::parse("ctrl-x")].map(Result::unwrap), - &[ - KeyContext::parse("Workspace"), - KeyContext::parse("Pane"), - KeyContext::parse("Editor vim_mode=normal"), - ] - .map(Result::unwrap), - ); - assert_eq!(matched.0.len(), 1); - assert!(matched.0[0].action.partial_eq(&ActionBeta)); - assert!(!matched.1); - } - - #[test] - fn test_overriding_prefix() { - let bindings = [ - KeyBinding::new("ctrl-x 0", ActionAlpha, Some("Workspace")), - KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")), - ]; - let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); - - let matched = keymap.bindings_for_input( - &[Keystroke::parse("ctrl-x")].map(Result::unwrap), - &[ - KeyContext::parse("Workspace"), - KeyContext::parse("Pane"), - KeyContext::parse("Editor vim_mode=normal"), - ] - .map(Result::unwrap), - ); - assert_eq!(matched.0.len(), 1); - assert!(matched.0[0].action.partial_eq(&ActionBeta)); - assert!(!matched.1); - } - - #[test] - fn test_context_precedence_with_same_source() { - // Test case: User has both Workspace and Editor bindings for the same key - // Editor binding should take precedence over Workspace binding - let bindings = [ - KeyBinding::new("cmd-r", ActionAlpha {}, Some("Workspace")), - KeyBinding::new("cmd-r", ActionBeta {}, Some("Editor")), - ]; - - let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); - - // Test with context stack: [Workspace, Editor] (Editor is deeper) - let (result, _) = keymap.bindings_for_input( - &[Keystroke::parse("cmd-r").unwrap()], - &[ - KeyContext::parse("Workspace").unwrap(), - KeyContext::parse("Editor").unwrap(), - ], - ); - - // Both bindings should be returned, but Editor binding should be first (highest precedence) - assert_eq!(result.len(), 2); - assert!(result[0].action.partial_eq(&ActionBeta {})); // Editor binding first - assert!(result[1].action.partial_eq(&ActionAlpha {})); // Workspace binding second - } - - #[test] - fn test_bindings_for_action() { - let bindings = [ - KeyBinding::new("ctrl-a", ActionAlpha {}, Some("pane")), - KeyBinding::new("ctrl-b", ActionBeta {}, Some("editor && mode == full")), - KeyBinding::new("ctrl-c", ActionGamma {}, Some("workspace")), - KeyBinding::new("ctrl-a", NoAction {}, Some("pane && active")), - KeyBinding::new("ctrl-b", NoAction {}, Some("editor")), - ]; - - let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); - - assert_bindings(&keymap, &ActionAlpha {}, &["ctrl-a"]); - assert_bindings(&keymap, &ActionBeta {}, &[]); - assert_bindings(&keymap, &ActionGamma {}, &["ctrl-c"]); - - #[track_caller] - fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) { - let actual = keymap - .bindings_for_action(action) - .map(|binding| binding.keystrokes[0].inner().unparse()) - .collect::>(); - assert_eq!(actual, expected, "{:?}", action); - } - } - - #[test] - fn test_targeted_unbind_ignores_target_context() { - let bindings = [ - KeyBinding::new("tab", ActionAlpha {}, Some("Editor")), - KeyBinding::new("tab", ActionBeta {}, Some("Editor && showing_completions")), - KeyBinding::new( - "tab", - Unbind("test_only::ActionAlpha".into()), - Some("Editor && edit_prediction"), - ), - ]; - - let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); - - let (result, pending) = keymap.bindings_for_input( - &[Keystroke::parse("tab").unwrap()], - &[KeyContext::parse("Editor showing_completions edit_prediction").unwrap()], - ); - - assert!(!pending); - assert_eq!(result.len(), 1); - assert!(result[0].action.partial_eq(&ActionBeta {})); - } - - #[test] - fn test_bindings_for_action_keeps_binding_for_narrower_targeted_unbind() { - let bindings = [ - KeyBinding::new("tab", ActionAlpha {}, Some("Editor")), - KeyBinding::new( - "tab", - Unbind("test_only::ActionAlpha".into()), - Some("Editor && edit_prediction"), - ), - KeyBinding::new("tab", ActionBeta {}, Some("Editor && showing_completions")), - ]; - - let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); - - assert_bindings(&keymap, &ActionAlpha {}, &["tab"]); - assert_bindings(&keymap, &ActionBeta {}, &["tab"]); - - #[track_caller] - fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) { - let actual = keymap - .bindings_for_action(action) - .map(|binding| binding.keystrokes[0].inner().unparse()) - .collect::>(); - assert_eq!(actual, expected, "{:?}", action); - } - } - - #[test] - fn test_bindings_for_action_removes_binding_for_broader_targeted_unbind() { - let bindings = [ - KeyBinding::new("tab", ActionAlpha {}, Some("Editor && edit_prediction")), - KeyBinding::new( - "tab", - Unbind("test_only::ActionAlpha".into()), - Some("Editor"), - ), - ]; - - let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); - - assert!(keymap.bindings_for_action(&ActionAlpha {}).next().is_none()); - } - - #[test] - fn test_source_precedence_sorting() { - // KeybindSource precedence: User (0) > Vim (1) > Base (2) > Default (3) - // Test that user keymaps take precedence over default keymaps at the same context depth - let mut keymap = Keymap::default(); - - // Add a default keymap binding first - let mut default_binding = KeyBinding::new("cmd-r", ActionAlpha {}, Some("Editor")); - default_binding.set_meta(KeyBindingMetaIndex(3)); // Default source - keymap.add_bindings([default_binding]); - - // Add a user keymap binding - let mut user_binding = KeyBinding::new("cmd-r", ActionBeta {}, Some("Editor")); - user_binding.set_meta(KeyBindingMetaIndex(0)); // User source - keymap.add_bindings([user_binding]); - - // Test with Editor context stack - let (result, _) = keymap.bindings_for_input( - &[Keystroke::parse("cmd-r").unwrap()], - &[KeyContext::parse("Editor").unwrap()], - ); - - // User binding should take precedence over default binding - assert_eq!(result.len(), 2); - assert!(result[0].action.partial_eq(&ActionBeta {})); - assert!(result[1].action.partial_eq(&ActionAlpha {})); - } -} diff --git a/crates/gpui_pre/src/keymap/binding.rs b/crates/gpui_pre/src/keymap/binding.rs deleted file mode 100644 index fc4b329..0000000 --- a/crates/gpui_pre/src/keymap/binding.rs +++ /dev/null @@ -1,143 +0,0 @@ -use std::rc::Rc; - -use crate::{ - Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate, - KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString, -}; -use smallvec::SmallVec; - -/// A keybinding and its associated metadata, from the keymap. -pub struct KeyBinding { - pub(crate) action: Box, - pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>, - pub(crate) context_predicate: Option>, - pub(crate) meta: Option, - /// The json input string used when building the keybinding, if any - pub(crate) action_input: Option, -} - -impl Clone for KeyBinding { - fn clone(&self) -> Self { - KeyBinding { - action: self.action.boxed_clone(), - keystrokes: self.keystrokes.clone(), - context_predicate: self.context_predicate.clone(), - meta: self.meta, - action_input: self.action_input.clone(), - } - } -} - -impl KeyBinding { - /// Construct a new keybinding from the given data. Panics on parse error. - pub fn new(keystrokes: &str, action: A, context: Option<&str>) -> Self { - let context_predicate = - context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into()); - Self::load( - keystrokes, - Box::new(action), - context_predicate, - false, - None, - &DummyKeyboardMapper, - ) - .unwrap() - } - - /// Load a keybinding from the given raw data. - pub fn load( - keystrokes: &str, - action: Box, - context_predicate: Option>, - use_key_equivalents: bool, - action_input: Option, - keyboard_mapper: &dyn PlatformKeyboardMapper, - ) -> std::result::Result { - let keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes - .split_whitespace() - .map(|source| { - let keystroke = Keystroke::parse(source)?; - Ok(KeybindingKeystroke::new_with_mapper( - keystroke, - use_key_equivalents, - keyboard_mapper, - )) - }) - .collect::>()?; - - Ok(Self { - keystrokes, - action, - context_predicate, - meta: None, - action_input, - }) - } - - /// Set the metadata for this binding. - pub fn with_meta(mut self, meta: KeyBindingMetaIndex) -> Self { - self.meta = Some(meta); - self - } - - /// Set the metadata for this binding. - pub fn set_meta(&mut self, meta: KeyBindingMetaIndex) { - self.meta = Some(meta); - } - - /// Check if the given keystrokes match this binding. - pub fn match_keystrokes(&self, typed: &[impl AsKeystroke]) -> Option { - if self.keystrokes.len() < typed.len() { - return None; - } - - for (target, typed) in self.keystrokes.iter().zip(typed.iter()) { - if !typed.as_keystroke().should_match(target) { - return None; - } - } - - Some(self.keystrokes.len() > typed.len()) - } - - /// Get the keystrokes associated with this binding - pub fn keystrokes(&self) -> &[KeybindingKeystroke] { - self.keystrokes.as_slice() - } - - /// Get the action associated with this binding - pub fn action(&self) -> &dyn Action { - self.action.as_ref() - } - - /// Get the predicate used to match this binding - pub fn predicate(&self) -> Option> { - self.context_predicate.as_ref().map(|rc| rc.clone()) - } - - /// Get the metadata for this binding - pub fn meta(&self) -> Option { - self.meta - } - - /// Get the action input associated with the action for this binding - pub fn action_input(&self) -> Option { - self.action_input.clone() - } -} - -impl std::fmt::Debug for KeyBinding { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("KeyBinding") - .field("keystrokes", &self.keystrokes) - .field("context_predicate", &self.context_predicate) - .field("action", &self.action.name()) - .finish() - } -} - -/// A unique identifier for retrieval of metadata associated with a key binding. -/// Intended to be used as an index or key into a user-defined store of metadata -/// associated with the binding, such as the source of the binding. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct KeyBindingMetaIndex(pub u32); diff --git a/crates/gpui_pre/src/keymap/context.rs b/crates/gpui_pre/src/keymap/context.rs deleted file mode 100644 index 27f361b..0000000 --- a/crates/gpui_pre/src/keymap/context.rs +++ /dev/null @@ -1,891 +0,0 @@ -use crate::SharedString; -use anyhow::{Context as _, Result}; -use std::fmt; - -/// A datastructure for resolving whether an action should be dispatched -/// at this point in the element tree. Contains a set of identifiers -/// and/or key value pairs representing the current context for the -/// keymap. -#[derive(Clone, Default, Eq, PartialEq, Hash)] -pub struct KeyContext(Vec); - -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -/// An entry in a KeyContext -pub struct ContextEntry { - /// The key (or name if no value) - pub key: SharedString, - /// The value - pub value: Option, -} - -impl<'a> TryFrom<&'a str> for KeyContext { - type Error = anyhow::Error; - - fn try_from(value: &'a str) -> Result { - Self::parse(value) - } -} - -impl KeyContext { - /// Initialize a new [`KeyContext`] that contains an `os` key set to either `macos`, `linux`, `windows` or `unknown`. - pub fn new_with_defaults() -> Self { - let mut context = Self::default(); - #[cfg(target_os = "macos")] - context.set("os", "macos"); - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - context.set("os", "linux"); - #[cfg(target_os = "windows")] - context.set("os", "windows"); - #[cfg(not(any( - target_os = "macos", - target_os = "linux", - target_os = "freebsd", - target_os = "windows" - )))] - context.set("os", "unknown"); - context - } - - /// Returns the primary context entry (usually the name of the component) - pub fn primary(&self) -> Option<&ContextEntry> { - self.0.iter().find(|p| p.value.is_none()) - } - - /// Returns everything except the primary context entry. - pub fn secondary(&self) -> impl Iterator { - let primary = self.primary(); - self.0.iter().filter(move |&p| Some(p) != primary) - } - - /// Parse a key context from a string. - /// The key context format is very simple: - /// - either a single identifier, such as `StatusBar` - /// - or a key value pair, such as `mode = visible` - /// - separated by whitespace, such as `StatusBar mode = visible` - pub fn parse(source: &str) -> Result { - let mut context = Self::default(); - let source = skip_whitespace(source); - Self::parse_expr(source, &mut context)?; - Ok(context) - } - - fn parse_expr(mut source: &str, context: &mut Self) -> Result<()> { - if source.is_empty() { - return Ok(()); - } - - let key = source - .chars() - .take_while(|c| is_identifier_char(*c)) - .collect::(); - source = skip_whitespace(&source[key.len()..]); - if let Some(suffix) = source.strip_prefix('=') { - source = skip_whitespace(suffix); - let value = source - .chars() - .take_while(|c| is_identifier_char(*c)) - .collect::(); - source = skip_whitespace(&source[value.len()..]); - context.set(key, value); - } else { - context.add(key); - } - - Self::parse_expr(source, context) - } - - /// Check if this context is empty. - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - /// Clear this context. - pub fn clear(&mut self) { - self.0.clear(); - } - - /// Extend this context with another context. - pub fn extend(&mut self, other: &Self) { - for entry in &other.0 { - if !self.contains(&entry.key) { - self.0.push(entry.clone()); - } - } - } - - /// Add an identifier to this context, if it's not already in this context. - pub fn add>(&mut self, identifier: I) { - let key = identifier.into(); - - if !self.contains(&key) { - self.0.push(ContextEntry { key, value: None }) - } - } - - /// Set a key value pair in this context, if it's not already set. - pub fn set, S2: Into>(&mut self, key: S1, value: S2) { - let key = key.into(); - if !self.contains(&key) { - self.0.push(ContextEntry { - key, - value: Some(value.into()), - }) - } - } - - /// Check if this context contains a given identifier or key. - pub fn contains(&self, key: &str) -> bool { - self.0.iter().any(|entry| entry.key.as_ref() == key) - } - - /// Get the associated value for a given identifier or key. - pub fn get(&self, key: &str) -> Option<&SharedString> { - self.0 - .iter() - .find(|entry| entry.key.as_ref() == key)? - .value - .as_ref() - } -} - -impl fmt::Debug for KeyContext { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut entries = self.0.iter().peekable(); - while let Some(entry) = entries.next() { - if let Some(ref value) = entry.value { - write!(f, "{}={}", entry.key, value)?; - } else { - write!(f, "{}", entry.key)?; - } - if entries.peek().is_some() { - write!(f, " ")?; - } - } - Ok(()) - } -} - -/// A datastructure for resolving whether an action should be dispatched -/// Representing a small language for describing which contexts correspond -/// to which actions. -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub enum KeyBindingContextPredicate { - /// A predicate that will match a given identifier. - Identifier(SharedString), - /// A predicate that will match a given key-value pair. - Equal(SharedString, SharedString), - /// A predicate that will match a given key-value pair not being present. - NotEqual(SharedString, SharedString), - /// A predicate that will match a given predicate appearing below another predicate. - /// in the element tree - Descendant( - Box, - Box, - ), - /// Predicate that will invert another predicate. - Not(Box), - /// A predicate that will match if both of its children match. - And( - Box, - Box, - ), - /// A predicate that will match if either of its children match. - Or( - Box, - Box, - ), -} - -impl fmt::Display for KeyBindingContextPredicate { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Identifier(name) => write!(f, "{name}"), - Self::Equal(left, right) => write!(f, "{left} == {right}"), - Self::NotEqual(left, right) => write!(f, "{left} != {right}"), - Self::Descendant(parent, child) => write!(f, "{parent} > {child}"), - Self::Not(pred) => match pred.as_ref() { - Self::Identifier(name) => write!(f, "!{name}"), - _ => write!(f, "!({pred})"), - }, - Self::And(..) => self.fmt_joined(f, " && ", LogicalOperator::And, |node| { - matches!(node, Self::Or(..)) - }), - Self::Or(..) => self.fmt_joined(f, " || ", LogicalOperator::Or, |node| { - matches!(node, Self::And(..)) - }), - } - } -} - -impl KeyBindingContextPredicate { - /// Parse a string in the same format as the keymap's context field. - /// - /// A basic equivalence check against a set of identifiers can performed by - /// simply writing a string: - /// - /// `StatusBar` -> A predicate that will match a context with the identifier `StatusBar` - /// - /// You can also specify a key-value pair: - /// - /// `mode == visible` -> A predicate that will match a context with the key `mode` - /// with the value `visible` - /// - /// And a logical operations combining these two checks: - /// - /// `StatusBar && mode == visible` -> A predicate that will match a context with the - /// identifier `StatusBar` and the key `mode` - /// with the value `visible` - /// - /// - /// There is also a special child `>` operator that will match a predicate that is - /// below another predicate: - /// - /// `StatusBar > mode == visible` -> A predicate that will match a context identifier `StatusBar` - /// and a child context that has the key `mode` with the - /// value `visible` - /// - /// This syntax supports `!=`, `||` and `&&` as logical operators. - /// You can also preface an operation or check with a `!` to negate it. - pub fn parse(source: &str) -> Result { - let source = skip_whitespace(source); - let (predicate, rest) = Self::parse_expr(source, 0)?; - if let Some(next) = rest.chars().next() { - anyhow::bail!("unexpected character '{next:?}'"); - } else { - Ok(predicate) - } - } - - /// Find the deepest depth at which the predicate matches. - pub fn depth_of(&self, contexts: &[KeyContext]) -> Option { - for depth in (0..=contexts.len()).rev() { - let context_slice = &contexts[0..depth]; - if self.eval_inner(context_slice, contexts) { - return Some(depth); - } - } - None - } - - /// Eval a predicate against a set of contexts, arranged from lowest to highest. - #[allow(unused)] - pub fn eval(&self, contexts: &[KeyContext]) -> bool { - self.eval_inner(contexts, contexts) - } - - /// Eval a predicate against a set of contexts, arranged from lowest to highest. - pub fn eval_inner(&self, contexts: &[KeyContext], all_contexts: &[KeyContext]) -> bool { - let Some(context) = contexts.last() else { - return false; - }; - match self { - Self::Identifier(name) => context.contains(name), - Self::Equal(left, right) => context - .get(left) - .map(|value| value == right) - .unwrap_or(false), - Self::NotEqual(left, right) => context - .get(left) - .map(|value| value != right) - .unwrap_or(true), - Self::Not(pred) => { - for i in 0..all_contexts.len() { - if pred.eval_inner(&all_contexts[..=i], all_contexts) { - return false; - } - } - true - } - // Workspace > Pane > Editor - // - // Pane > (Pane > Editor) // should match? - // (Pane > Pane) > Editor // should not match? - // Pane > !Workspace <-- should match? - // !Workspace <-- shouldn't match? - Self::Descendant(parent, child) => { - for i in 0..contexts.len() - 1 { - // [Workspace > Pane], [Editor] - if parent.eval_inner(&contexts[..=i], all_contexts) { - if !child.eval_inner(&contexts[i + 1..], &contexts[i + 1..]) { - return false; - } - return true; - } - } - false - } - Self::And(left, right) => { - left.eval_inner(contexts, all_contexts) && right.eval_inner(contexts, all_contexts) - } - Self::Or(left, right) => { - left.eval_inner(contexts, all_contexts) || right.eval_inner(contexts, all_contexts) - } - } - } - - /// Returns whether or not this predicate matches all possible contexts matched by - /// the other predicate. - pub fn is_superset(&self, other: &Self) -> bool { - if self == other { - return true; - } - - if let KeyBindingContextPredicate::Or(left, right) = self { - return left.is_superset(other) || right.is_superset(other); - } - - match other { - KeyBindingContextPredicate::Descendant(_, child) => self.is_superset(child), - KeyBindingContextPredicate::And(left, right) => { - self.is_superset(left) || self.is_superset(right) - } - KeyBindingContextPredicate::Identifier(_) => false, - KeyBindingContextPredicate::Equal(_, _) => false, - KeyBindingContextPredicate::NotEqual(_, _) => false, - KeyBindingContextPredicate::Not(_) => false, - KeyBindingContextPredicate::Or(_, _) => false, - } - } - - fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> { - type Op = fn( - KeyBindingContextPredicate, - KeyBindingContextPredicate, - ) -> Result; - - let (mut predicate, rest) = Self::parse_primary(source)?; - source = rest; - - 'parse: loop { - for (operator, precedence, constructor) in [ - (">", PRECEDENCE_CHILD, Self::new_child as Op), - ("&&", PRECEDENCE_AND, Self::new_and as Op), - ("||", PRECEDENCE_OR, Self::new_or as Op), - ("==", PRECEDENCE_EQ, Self::new_eq as Op), - ("!=", PRECEDENCE_EQ, Self::new_neq as Op), - ] { - if source.starts_with(operator) && precedence >= min_precedence { - source = skip_whitespace(&source[operator.len()..]); - let (right, rest) = Self::parse_expr(source, precedence + 1)?; - predicate = constructor(predicate, right)?; - source = rest; - continue 'parse; - } - } - break; - } - - Ok((predicate, source)) - } - - fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> { - let next = source.chars().next().context("unexpected end")?; - match next { - '(' => { - source = skip_whitespace(&source[1..]); - let (predicate, rest) = Self::parse_expr(source, 0)?; - let stripped = rest.strip_prefix(')').context("expected a ')'")?; - source = skip_whitespace(stripped); - Ok((predicate, source)) - } - '!' => { - let source = skip_whitespace(&source[1..]); - let (predicate, source) = Self::parse_expr(source, PRECEDENCE_NOT)?; - Ok((KeyBindingContextPredicate::Not(Box::new(predicate)), source)) - } - _ if is_identifier_char(next) => { - let len = source - .find(|c: char| !is_identifier_char(c) && !is_vim_operator_char(c)) - .unwrap_or(source.len()); - let (identifier, rest) = source.split_at(len); - source = skip_whitespace(rest); - Ok(( - KeyBindingContextPredicate::Identifier(identifier.to_string().into()), - source, - )) - } - _ if is_vim_operator_char(next) => { - let (operator, rest) = source.split_at(1); - source = skip_whitespace(rest); - Ok(( - KeyBindingContextPredicate::Identifier(operator.to_string().into()), - source, - )) - } - _ => anyhow::bail!("unexpected character '{next:?}'"), - } - } - - fn new_or(self, other: Self) -> Result { - Ok(Self::Or(Box::new(self), Box::new(other))) - } - - fn new_and(self, other: Self) -> Result { - Ok(Self::And(Box::new(self), Box::new(other))) - } - - fn new_child(self, other: Self) -> Result { - Ok(Self::Descendant(Box::new(self), Box::new(other))) - } - - fn new_eq(self, other: Self) -> Result { - if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) { - Ok(Self::Equal(left, right)) - } else { - anyhow::bail!("operands of == must be identifiers"); - } - } - - fn new_neq(self, other: Self) -> Result { - if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) { - Ok(Self::NotEqual(left, right)) - } else { - anyhow::bail!("operands of != must be identifiers"); - } - } - - fn fmt_joined( - &self, - f: &mut fmt::Formatter<'_>, - separator: &str, - operator: LogicalOperator, - needs_parens: impl Fn(&Self) -> bool + Copy, - ) -> fmt::Result { - let mut first = true; - self.fmt_joined_inner(f, separator, operator, needs_parens, &mut first) - } - - fn fmt_joined_inner( - &self, - f: &mut fmt::Formatter<'_>, - separator: &str, - operator: LogicalOperator, - needs_parens: impl Fn(&Self) -> bool + Copy, - first: &mut bool, - ) -> fmt::Result { - match (operator, self) { - (LogicalOperator::And, Self::And(left, right)) - | (LogicalOperator::Or, Self::Or(left, right)) => { - left.fmt_joined_inner(f, separator, operator, needs_parens, first)?; - right.fmt_joined_inner(f, separator, operator, needs_parens, first) - } - (_, node) => { - if !*first { - f.write_str(separator)?; - } - *first = false; - - if needs_parens(node) { - write!(f, "({node})") - } else { - write!(f, "{node}") - } - } - } - } -} - -#[derive(Clone, Copy)] -enum LogicalOperator { - And, - Or, -} - -const PRECEDENCE_CHILD: u32 = 1; -const PRECEDENCE_OR: u32 = 2; -const PRECEDENCE_AND: u32 = 3; -const PRECEDENCE_EQ: u32 = 4; -const PRECEDENCE_NOT: u32 = 5; - -fn is_identifier_char(c: char) -> bool { - c.is_alphanumeric() || c == '_' || c == '-' -} - -fn is_vim_operator_char(c: char) -> bool { - c == '>' || c == '<' || c == '~' || c == '"' || c == '?' -} - -fn skip_whitespace(source: &str) -> &str { - let len = source - .find(|c: char| !c.is_whitespace()) - .unwrap_or(source.len()); - &source[len..] -} - -#[cfg(test)] -mod tests { - use core::slice; - - use super::*; - use crate as gpui; - use KeyBindingContextPredicate::*; - - #[test] - fn test_actions_definition() { - { - actions!(test_only, [A, B, C, D, E, F, G]); - } - - { - actions!( - test_only, - [ - H, I, J, K, L, M, N, // Don't wrap, test the trailing comma - ] - ); - } - } - - #[test] - fn test_parse_context() { - let mut expected = KeyContext::default(); - expected.add("baz"); - expected.set("foo", "bar"); - assert_eq!(KeyContext::parse("baz foo=bar").unwrap(), expected); - assert_eq!(KeyContext::parse("baz foo = bar").unwrap(), expected); - assert_eq!( - KeyContext::parse(" baz foo = bar baz").unwrap(), - expected - ); - assert_eq!(KeyContext::parse(" baz foo = bar").unwrap(), expected); - } - - #[test] - fn test_parse_identifiers() { - // Identifiers - assert_eq!( - KeyBindingContextPredicate::parse("abc12").unwrap(), - Identifier("abc12".into()) - ); - assert_eq!( - KeyBindingContextPredicate::parse("_1a").unwrap(), - Identifier("_1a".into()) - ); - } - - #[test] - fn test_parse_negations() { - assert_eq!( - KeyBindingContextPredicate::parse("!abc").unwrap(), - Not(Box::new(Identifier("abc".into()))) - ); - assert_eq!( - KeyBindingContextPredicate::parse(" ! ! abc").unwrap(), - Not(Box::new(Not(Box::new(Identifier("abc".into()))))) - ); - } - - #[test] - fn test_parse_equality_operators() { - assert_eq!( - KeyBindingContextPredicate::parse("a == b").unwrap(), - Equal("a".into(), "b".into()) - ); - assert_eq!( - KeyBindingContextPredicate::parse("c!=d").unwrap(), - NotEqual("c".into(), "d".into()) - ); - assert_eq!( - KeyBindingContextPredicate::parse("c == !d") - .unwrap_err() - .to_string(), - "operands of == must be identifiers" - ); - } - - #[test] - fn test_parse_boolean_operators() { - assert_eq!( - KeyBindingContextPredicate::parse("a || b").unwrap(), - Or( - Box::new(Identifier("a".into())), - Box::new(Identifier("b".into())) - ) - ); - assert_eq!( - KeyBindingContextPredicate::parse("a || !b && c").unwrap(), - Or( - Box::new(Identifier("a".into())), - Box::new(And( - Box::new(Not(Box::new(Identifier("b".into())))), - Box::new(Identifier("c".into())) - )) - ) - ); - assert_eq!( - KeyBindingContextPredicate::parse("a && b || c&&d").unwrap(), - Or( - Box::new(And( - Box::new(Identifier("a".into())), - Box::new(Identifier("b".into())) - )), - Box::new(And( - Box::new(Identifier("c".into())), - Box::new(Identifier("d".into())) - )) - ) - ); - assert_eq!( - KeyBindingContextPredicate::parse("a == b && c || d == e && f").unwrap(), - Or( - Box::new(And( - Box::new(Equal("a".into(), "b".into())), - Box::new(Identifier("c".into())) - )), - Box::new(And( - Box::new(Equal("d".into(), "e".into())), - Box::new(Identifier("f".into())) - )) - ) - ); - assert_eq!( - KeyBindingContextPredicate::parse("a && b && c && d").unwrap(), - And( - Box::new(And( - Box::new(And( - Box::new(Identifier("a".into())), - Box::new(Identifier("b".into())) - )), - Box::new(Identifier("c".into())), - )), - Box::new(Identifier("d".into())) - ), - ); - } - - #[test] - fn test_parse_parenthesized_expressions() { - assert_eq!( - KeyBindingContextPredicate::parse("a && (b == c || d != e)").unwrap(), - And( - Box::new(Identifier("a".into())), - Box::new(Or( - Box::new(Equal("b".into(), "c".into())), - Box::new(NotEqual("d".into(), "e".into())), - )), - ), - ); - assert_eq!( - KeyBindingContextPredicate::parse(" ( a || b ) ").unwrap(), - Or( - Box::new(Identifier("a".into())), - Box::new(Identifier("b".into())), - ) - ); - } - - #[test] - fn test_is_superset() { - assert_is_superset("editor", "editor", true); - assert_is_superset("editor", "workspace", false); - - assert_is_superset("editor", "editor && vim_mode", true); - assert_is_superset("editor", "mode == full && editor", true); - assert_is_superset("editor && mode == full", "editor", false); - - assert_is_superset("editor", "something > editor", true); - assert_is_superset("editor", "editor > menu", false); - - assert_is_superset("foo || bar || baz", "bar", true); - assert_is_superset("foo || bar || baz", "quux", false); - - #[track_caller] - fn assert_is_superset(a: &str, b: &str, result: bool) { - let a = KeyBindingContextPredicate::parse(a).unwrap(); - let b = KeyBindingContextPredicate::parse(b).unwrap(); - assert_eq!(a.is_superset(&b), result, "({a:?}).is_superset({b:?})"); - } - } - - #[test] - fn test_child_operator() { - let predicate = KeyBindingContextPredicate::parse("parent > child").unwrap(); - - let parent_context = KeyContext::try_from("parent").unwrap(); - let child_context = KeyContext::try_from("child").unwrap(); - - let contexts = vec![parent_context.clone(), child_context.clone()]; - assert!(predicate.eval(&contexts)); - - let grandparent_context = KeyContext::try_from("grandparent").unwrap(); - - let contexts = vec![ - grandparent_context, - parent_context.clone(), - child_context.clone(), - ]; - assert!(predicate.eval(&contexts)); - - let other_context = KeyContext::try_from("other").unwrap(); - - let contexts = vec![other_context.clone(), child_context.clone()]; - assert!(!predicate.eval(&contexts)); - - let contexts = vec![parent_context.clone(), other_context, child_context.clone()]; - assert!(predicate.eval(&contexts)); - - assert!(!predicate.eval(&[])); - assert!(!predicate.eval(slice::from_ref(&child_context))); - assert!(!predicate.eval(&[parent_context])); - - let zany_predicate = KeyBindingContextPredicate::parse("child > child").unwrap(); - assert!(!zany_predicate.eval(slice::from_ref(&child_context))); - assert!(zany_predicate.eval(&[child_context.clone(), child_context])); - } - - #[test] - fn test_not_operator() { - let not_predicate = KeyBindingContextPredicate::parse("!editor").unwrap(); - let editor_context = KeyContext::try_from("editor").unwrap(); - let workspace_context = KeyContext::try_from("workspace").unwrap(); - let parent_context = KeyContext::try_from("parent").unwrap(); - let child_context = KeyContext::try_from("child").unwrap(); - - assert!(not_predicate.eval(slice::from_ref(&workspace_context))); - assert!(!not_predicate.eval(slice::from_ref(&editor_context))); - assert!(!not_predicate.eval(&[editor_context.clone(), workspace_context.clone()])); - assert!(!not_predicate.eval(&[workspace_context.clone(), editor_context.clone()])); - - let complex_not = KeyBindingContextPredicate::parse("!editor && workspace").unwrap(); - assert!(complex_not.eval(slice::from_ref(&workspace_context))); - assert!(!complex_not.eval(&[editor_context.clone(), workspace_context.clone()])); - - let not_mode_predicate = KeyBindingContextPredicate::parse("!(mode == full)").unwrap(); - let mut mode_context = KeyContext::default(); - mode_context.set("mode", "full"); - assert!(!not_mode_predicate.eval(&[mode_context.clone()])); - - let mut other_mode_context = KeyContext::default(); - other_mode_context.set("mode", "partial"); - assert!(not_mode_predicate.eval(&[other_mode_context])); - - let not_descendant = KeyBindingContextPredicate::parse("!(parent > child)").unwrap(); - assert!(not_descendant.eval(slice::from_ref(&parent_context))); - assert!(not_descendant.eval(slice::from_ref(&child_context))); - assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()])); - - let not_descendant = KeyBindingContextPredicate::parse("parent > !child").unwrap(); - assert!(!not_descendant.eval(slice::from_ref(&parent_context))); - assert!(!not_descendant.eval(slice::from_ref(&child_context))); - assert!(!not_descendant.eval(&[parent_context, child_context])); - - let double_not = KeyBindingContextPredicate::parse("!!editor").unwrap(); - assert!(double_not.eval(slice::from_ref(&editor_context))); - assert!(!double_not.eval(slice::from_ref(&workspace_context))); - - // Test complex descendant cases - let workspace_context = KeyContext::try_from("Workspace").unwrap(); - let pane_context = KeyContext::try_from("Pane").unwrap(); - let editor_context = KeyContext::try_from("Editor").unwrap(); - - // Workspace > Pane > Editor - let workspace_pane_editor = vec![ - workspace_context.clone(), - pane_context.clone(), - editor_context.clone(), - ]; - - // Pane > (Pane > Editor) - should not match - let pane_pane_editor = KeyBindingContextPredicate::parse("Pane > (Pane > Editor)").unwrap(); - assert!(!pane_pane_editor.eval(&workspace_pane_editor)); - - let workspace_pane_editor_predicate = - KeyBindingContextPredicate::parse("Workspace > Pane > Editor").unwrap(); - assert!(workspace_pane_editor_predicate.eval(&workspace_pane_editor)); - - // (Pane > Pane) > Editor - should not match - let pane_pane_then_editor = - KeyBindingContextPredicate::parse("(Pane > Pane) > Editor").unwrap(); - assert!(!pane_pane_then_editor.eval(&workspace_pane_editor)); - - // Pane > !Workspace - should match - let pane_not_workspace = KeyBindingContextPredicate::parse("Pane > !Workspace").unwrap(); - assert!(pane_not_workspace.eval(&[pane_context.clone(), editor_context.clone()])); - assert!(!pane_not_workspace.eval(&[pane_context.clone(), workspace_context.clone()])); - - // !Workspace - shouldn't match when Workspace is in the context - let not_workspace = KeyBindingContextPredicate::parse("!Workspace").unwrap(); - assert!(!not_workspace.eval(slice::from_ref(&workspace_context))); - assert!(not_workspace.eval(slice::from_ref(&pane_context))); - assert!(not_workspace.eval(slice::from_ref(&editor_context))); - assert!(!not_workspace.eval(&workspace_pane_editor)); - } - - // MARK: - Display - - #[test] - fn test_context_display() { - fn ident(s: &str) -> Box { - Box::new(Identifier(SharedString::new(s))) - } - fn eq(a: &str, b: &str) -> Box { - Box::new(Equal(SharedString::new(a), SharedString::new(b))) - } - fn not_eq(a: &str, b: &str) -> Box { - Box::new(NotEqual(SharedString::new(a), SharedString::new(b))) - } - fn and( - a: Box, - b: Box, - ) -> Box { - Box::new(And(a, b)) - } - fn or( - a: Box, - b: Box, - ) -> Box { - Box::new(Or(a, b)) - } - fn descendant( - a: Box, - b: Box, - ) -> Box { - Box::new(Descendant(a, b)) - } - fn not(a: Box) -> Box { - Box::new(Not(a)) - } - - let test_cases = [ - (ident("a"), "a"), - (eq("a", "b"), "a == b"), - (not_eq("a", "b"), "a != b"), - (descendant(ident("a"), ident("b")), "a > b"), - (not(ident("a")), "!a"), - (not_eq("a", "b"), "a != b"), - (descendant(ident("a"), ident("b")), "a > b"), - (not(and(ident("a"), ident("b"))), "!(a && b)"), - (not(or(ident("a"), ident("b"))), "!(a || b)"), - (and(ident("a"), ident("b")), "a && b"), - (and(and(ident("a"), ident("b")), ident("c")), "a && b && c"), - (or(ident("a"), ident("b")), "a || b"), - (or(or(ident("a"), ident("b")), ident("c")), "a || b || c"), - (or(ident("a"), and(ident("b"), ident("c"))), "a || (b && c)"), - ( - and( - and( - and(ident("a"), eq("b", "c")), - not(descendant(ident("d"), ident("e"))), - ), - eq("f", "g"), - ), - "a && b == c && !(d > e) && f == g", - ), - ( - and(and(ident("a"), or(ident("b"), ident("c"))), ident("d")), - "a && (b || c) && d", - ), - ( - or(or(ident("a"), and(ident("b"), ident("c"))), ident("d")), - "a || (b && c) || d", - ), - ]; - - for (predicate, expected) in test_cases { - let actual = predicate.to_string(); - assert_eq!(actual, expected); - let parsed = KeyBindingContextPredicate::parse(&actual).unwrap(); - assert_eq!(parsed, *predicate); - } - } -} diff --git a/crates/gpui_pre/src/path_builder.rs b/crates/gpui_pre/src/path_builder.rs deleted file mode 100644 index 40a6e71..0000000 --- a/crates/gpui_pre/src/path_builder.rs +++ /dev/null @@ -1,347 +0,0 @@ -use anyhow::Error; -use etagere::euclid::{Point2D, Vector2D}; -use lyon::geom::Angle; -use lyon::math::{Vector, vector}; -use lyon::path::traits::SvgPathBuilder; -use lyon::path::{ArcFlags, Polygon}; -use lyon::tessellation::{ - BuffersBuilder, FillTessellator, FillVertex, StrokeTessellator, StrokeVertex, VertexBuffers, -}; - -pub use lyon::math::Transform; -pub use lyon::tessellation::{FillOptions, FillRule, StrokeOptions}; - -use crate::{Path, Pixels, Point, point, px}; - -/// Style of the PathBuilder -pub enum PathStyle { - /// Stroke style - Stroke(StrokeOptions), - /// Fill style - Fill(FillOptions), -} - -/// A [`Path`] builder. -pub struct PathBuilder { - raw: lyon::path::builder::WithSvg, - transform: Option, - /// PathStyle of the PathBuilder - pub style: PathStyle, - dash_array: Option>, -} - -impl From for PathBuilder { - fn from(builder: lyon::path::Builder) -> Self { - Self { - raw: builder.with_svg(), - ..Default::default() - } - } -} - -impl From> for PathBuilder { - fn from(raw: lyon::path::builder::WithSvg) -> Self { - Self { - raw, - ..Default::default() - } - } -} - -impl From for Point { - fn from(p: lyon::math::Point) -> Self { - point(px(p.x), px(p.y)) - } -} - -impl From> for lyon::math::Point { - fn from(p: Point) -> Self { - lyon::math::point(p.x.0, p.y.0) - } -} - -impl From> for Vector { - fn from(p: Point) -> Self { - vector(p.x.0, p.y.0) - } -} - -impl From> for Point2D { - fn from(p: Point) -> Self { - Point2D::new(p.x.0, p.y.0) - } -} - -impl Default for PathBuilder { - fn default() -> Self { - Self { - raw: lyon::path::Path::builder().with_svg(), - style: PathStyle::Fill(FillOptions::default()), - transform: None, - dash_array: None, - } - } -} - -impl PathBuilder { - /// Creates a new [`PathBuilder`] to build a Stroke path. - pub fn stroke(width: Pixels) -> Self { - Self { - style: PathStyle::Stroke(StrokeOptions::default().with_line_width(width.0)), - ..Self::default() - } - } - - /// Creates a new [`PathBuilder`] to build a Fill path. - pub fn fill() -> Self { - Self::default() - } - - /// Sets the style of the [`PathBuilder`]. - pub fn with_style(self, style: PathStyle) -> Self { - Self { style, ..self } - } - - /// Sets the dash array of the [`PathBuilder`]. - /// - /// [MDN](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/stroke-dasharray) - pub fn dash_array(mut self, dash_array: &[Pixels]) -> Self { - // If an odd number of values is provided, then the list of values is repeated to yield an even number of values. - // Thus, 5,3,2 is equivalent to 5,3,2,5,3,2. - let array = if dash_array.len() % 2 == 1 { - let mut new_dash_array = dash_array.to_vec(); - new_dash_array.extend_from_slice(dash_array); - new_dash_array - } else { - dash_array.to_vec() - }; - - self.dash_array = Some(array); - self - } - - /// Move the current point to the given point. - #[inline] - pub fn move_to(&mut self, to: Point) { - self.raw.move_to(to.into()); - } - - /// Draw a straight line from the current point to the given point. - #[inline] - pub fn line_to(&mut self, to: Point) { - self.raw.line_to(to.into()); - } - - /// Draw a curve from the current point to the given point, using the given control point. - #[inline] - pub fn curve_to(&mut self, to: Point, ctrl: Point) { - self.raw.quadratic_bezier_to(ctrl.into(), to.into()); - } - - /// Adds a cubic Bézier to the [`Path`] given its two control points - /// and its end point. - #[inline] - pub fn cubic_bezier_to( - &mut self, - to: Point, - control_a: Point, - control_b: Point, - ) { - self.raw - .cubic_bezier_to(control_a.into(), control_b.into(), to.into()); - } - - /// Adds an elliptical arc. - pub fn arc_to( - &mut self, - radii: Point, - x_rotation: Pixels, - large_arc: bool, - sweep: bool, - to: Point, - ) { - self.raw.arc_to( - radii.into(), - Angle::degrees(x_rotation.into()), - ArcFlags { large_arc, sweep }, - to.into(), - ); - } - - /// Equivalent to `arc_to` in relative coordinates. - pub fn relative_arc_to( - &mut self, - radii: Point, - x_rotation: Pixels, - large_arc: bool, - sweep: bool, - to: Point, - ) { - self.raw.relative_arc_to( - radii.into(), - Angle::degrees(x_rotation.into()), - ArcFlags { large_arc, sweep }, - to.into(), - ); - } - - /// Adds a polygon. - pub fn add_polygon(&mut self, points: &[Point], closed: bool) { - let points = points.iter().copied().map(|p| p.into()).collect::>(); - self.raw.add_polygon(Polygon { - points: points.as_ref(), - closed, - }); - } - - /// Close the current sub-path. - #[inline] - pub fn close(&mut self) { - self.raw.close(); - } - - /// Applies a transform to the path. - #[inline] - pub fn transform(&mut self, transform: Transform) { - self.transform = Some(transform); - } - - /// Applies a translation to the path. - #[inline] - pub fn translate(&mut self, to: Point) { - if let Some(transform) = self.transform { - self.transform = Some(transform.then_translate(Vector2D::new(to.x.0, to.y.0))); - } else { - self.transform = Some(Transform::translation(to.x.0, to.y.0)) - } - } - - /// Applies a scale to the path. - #[inline] - pub fn scale(&mut self, scale: f32) { - if let Some(transform) = self.transform { - self.transform = Some(transform.then_scale(scale, scale)); - } else { - self.transform = Some(Transform::scale(scale, scale)); - } - } - - /// Applies a rotation to the path. - /// - /// The `angle` is in degrees value in the range 0.0 to 360.0. - #[inline] - pub fn rotate(&mut self, angle: f32) { - let radians = angle.to_radians(); - if let Some(transform) = self.transform { - self.transform = Some(transform.then_rotate(Angle::radians(radians))); - } else { - self.transform = Some(Transform::rotation(Angle::radians(radians))); - } - } - - /// Builds into a [`Path`]. - #[inline] - pub fn build(self) -> Result, Error> { - let path = if let Some(transform) = self.transform { - self.raw.build().transformed(&transform) - } else { - self.raw.build() - }; - - match self.style { - PathStyle::Stroke(options) => Self::tessellate_stroke(self.dash_array, &path, &options), - PathStyle::Fill(options) => Self::tessellate_fill(&path, &options), - } - } - - fn tessellate_fill( - path: &lyon::path::Path, - options: &FillOptions, - ) -> Result, Error> { - // Will contain the result of the tessellation. - let mut buf: VertexBuffers = VertexBuffers::new(); - let mut tessellator = FillTessellator::new(); - - // Compute the tessellation. - tessellator.tessellate_path( - path, - options, - &mut BuffersBuilder::new(&mut buf, |vertex: FillVertex| vertex.position()), - )?; - - Ok(Self::build_path(buf)) - } - - fn tessellate_stroke( - dash_array: Option>, - path: &lyon::path::Path, - options: &StrokeOptions, - ) -> Result, Error> { - let path = if let Some(dash_array) = dash_array { - let measurements = lyon::algorithms::measure::PathMeasurements::from_path(path, 0.01); - let mut sampler = measurements - .create_sampler(path, lyon::algorithms::measure::SampleType::Normalized); - let mut builder = lyon::path::Path::builder(); - - let total_length = sampler.length(); - let dash_array_len = dash_array.len(); - let mut pos = 0.; - let mut dash_index = 0; - while pos < total_length { - let dash_length = dash_array[dash_index % dash_array_len].0; - let next_pos = (pos + dash_length).min(total_length); - if dash_index % 2 == 0 { - let start = pos / total_length; - let end = next_pos / total_length; - sampler.split_range(start..end, &mut builder); - } - pos = next_pos; - dash_index += 1; - } - - &builder.build() - } else { - path - }; - - // Will contain the result of the tessellation. - let mut buf: VertexBuffers = VertexBuffers::new(); - let mut tessellator = StrokeTessellator::new(); - - // Compute the tessellation. - tessellator.tessellate_path( - path, - options, - &mut BuffersBuilder::new(&mut buf, |vertex: StrokeVertex| vertex.position()), - )?; - - Ok(Self::build_path(buf)) - } - - /// Builds a [`Path`] from a [`lyon::tessellation::VertexBuffers`]. - pub fn build_path(buf: VertexBuffers) -> Path { - if buf.vertices.is_empty() { - return Path::new(Point::default()); - } - - let first_point = buf.vertices[0]; - - let mut path = Path::new(first_point.into()); - for i in 0..buf.indices.len() / 3 { - let i0 = buf.indices[i * 3] as usize; - let i1 = buf.indices[i * 3 + 1] as usize; - let i2 = buf.indices[i * 3 + 2] as usize; - - let v0 = buf.vertices[i0]; - let v1 = buf.vertices[i1]; - let v2 = buf.vertices[i2]; - - path.push_triangle( - (v0.into(), v1.into(), v2.into()), - (point(0., 1.), point(0., 1.), point(0., 1.)), - ); - } - - path - } -} diff --git a/crates/gpui_pre/src/platform.rs b/crates/gpui_pre/src/platform.rs deleted file mode 100644 index eeba999..0000000 --- a/crates/gpui_pre/src/platform.rs +++ /dev/null @@ -1,3113 +0,0 @@ -mod app_menu; -mod keyboard; -mod keystroke; - -#[cfg(all(target_os = "linux", feature = "wayland"))] -#[expect(missing_docs)] -pub mod layer_shell; - -/// Types for configuring parent-anchored popup windows such as menus, dropdowns and tooltips. -pub mod popup; - -#[cfg(any(test, feature = "test-support", feature = "bench-support"))] -mod threaded_dispatcher; - -#[cfg(any(test, feature = "test-support", feature = "bench-support"))] -mod test; - -#[cfg(all(target_os = "macos", any(test, feature = "test-support")))] -mod visual_test; - -#[cfg(all( - feature = "screen-capture", - any(target_os = "windows", target_os = "linux", target_os = "freebsd",) -))] -pub mod scap_screen_capture; - -#[cfg(all( - any(target_os = "windows", target_os = "linux"), - feature = "screen-capture" -))] -pub(crate) type PlatformScreenCaptureFrame = scap::frame::Frame; -#[cfg(not(feature = "screen-capture"))] -pub(crate) type PlatformScreenCaptureFrame = (); -#[cfg(all(target_os = "macos", feature = "screen-capture"))] -pub(crate) type PlatformScreenCaptureFrame = core_video::image_buffer::CVImageBuffer; - -use crate::{ - Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds, - DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Edges, ExternalDragPayload, Font, - FontId, FontMetrics, FontRun, ForegroundExecutor, GlyphId, GpuSpecs, Hsla, ImageSource, Keymap, - LineLayout, Pixels, PlatformGestures, PlatformInput, Point, Priority, RenderGlyphParams, - RenderImage, RenderImageParams, RenderSvgParams, Scene, ShapedGlyph, ShapedRun, SharedString, - Size, SvgRenderer, SystemWindowTab, Task, Window, WindowControlArea, hash, point, px, size, -}; -#[cfg(any(target_os = "linux", target_os = "freebsd"))] -use anyhow::bail; -use anyhow::{Context as _, Result}; -use async_task::Runnable; -use futures::channel::oneshot; -#[cfg(any(test, feature = "test-support", feature = "bench-support"))] -use image::RgbaImage; -use image::codecs::gif::GifDecoder; -use image::{AnimationDecoder as _, DynamicImage, Frame}; -use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; -use scheduler::Instant; -pub use scheduler::RunnableMeta; -use schemars::JsonSchema; -use seahash::SeaHasher; -use serde::{Deserialize, Serialize}; -use smallvec::SmallVec; -use std::borrow::Cow; -use std::hash::{Hash, Hasher}; -use std::io::Cursor; -use std::ops; -use std::time::Duration; -use std::{ - ffi::OsString, - fmt::{self, Debug}, - ops::Range, - path::{Path, PathBuf}, - rc::Rc, - sync::Arc, -}; -use strum::EnumIter; -use uuid::Uuid; - -pub use app_menu::*; -pub use keyboard::*; -pub use keystroke::*; - -#[cfg(any(test, feature = "test-support", feature = "bench-support"))] -pub(crate) use test::*; - -#[cfg(any(test, feature = "test-support"))] -pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream}; - -#[cfg(any(test, feature = "test-support", feature = "bench-support"))] -pub use threaded_dispatcher::ThreadedDispatcher; - -#[cfg(all(target_os = "macos", any(test, feature = "test-support")))] -pub use visual_test::VisualTestPlatform; - -// TODO(jk): return an enum instead of a string -/// Return which compositor we're guessing we'll use. -/// Does not attempt to connect to the given compositor. -#[cfg(any(target_os = "linux", target_os = "freebsd"))] -#[inline] -pub fn guess_compositor() -> &'static str { - if std::env::var_os("ZED_HEADLESS").is_some() { - return "Headless"; - } - - #[cfg(feature = "wayland")] - let wayland_display = std::env::var_os("WAYLAND_DISPLAY"); - #[cfg(not(feature = "wayland"))] - let wayland_display: Option = None; - - #[cfg(feature = "x11")] - let x11_display = std::env::var_os("DISPLAY"); - #[cfg(not(feature = "x11"))] - let x11_display: Option = None; - - let use_wayland = wayland_display.is_some_and(|display| !display.is_empty()); - let use_x11 = x11_display.is_some_and(|display| !display.is_empty()); - - if use_wayland { - "Wayland" - } else if use_x11 { - "X11" - } else { - "Headless" - } -} - -#[expect(missing_docs)] -pub trait Platform: 'static { - fn background_executor(&self) -> BackgroundExecutor; - fn foreground_executor(&self) -> ForegroundExecutor; - fn text_system(&self) -> Arc; - - fn run(&self, on_finish_launching: Box); - fn quit(&self); - fn restart(&self, binary_path: Option, arguments: Vec); - fn activate(&self, ignoring_other_apps: bool); - fn hide(&self); - fn hide_other_apps(&self); - fn unhide_other_apps(&self); - - fn displays(&self) -> Vec>; - fn primary_display(&self) -> Option>; - fn active_window(&self) -> Option; - fn window_stack(&self) -> Option> { - None - } - - fn is_screen_capture_supported(&self) -> bool { - false - } - - fn screen_capture_sources( - &self, - ) -> oneshot::Receiver>>> { - let (sources_tx, sources_rx) = oneshot::channel(); - sources_tx - .send(Err(anyhow::anyhow!( - "gpui was compiled without the screen-capture feature" - ))) - .ok(); - sources_rx - } - - fn open_window( - &self, - handle: AnyWindowHandle, - options: WindowParams, - ) -> anyhow::Result>; - - /// Returns the appearance of the application's windows. - fn window_appearance(&self) -> WindowAppearance; - - /// Overrides the appearance (light/dark) applied to the app's windows, independent - /// of the OS-wide setting. Pass `None` to clear the override and follow the system - /// again. The override is reflected by [`Platform::window_appearance`]. - /// - /// Currently only implemented on macOS, where it sets `NSApplication.appearance` so - /// the native window chrome (the window border and titlebar) of every window matches - /// a dark app theme even when the system is in light mode (or vice versa). A no-op on - /// other platforms. - fn set_window_appearance(&self, _appearance: Option) {} - - /// Returns the window button layout configuration when supported. - fn button_layout(&self) -> Option { - None - } - - fn open_url(&self, url: &str); - fn on_open_urls(&self, callback: Box)>); - fn register_url_scheme(&self, url: &str) -> Task>; - - fn prompt_for_paths( - &self, - options: PathPromptOptions, - ) -> oneshot::Receiver>>>; - fn prompt_for_new_path( - &self, - directory: &Path, - suggested_name: Option<&str>, - ) -> oneshot::Receiver>>; - fn can_select_mixed_files_and_dirs(&self) -> bool; - fn reveal_path(&self, path: &Path); - fn open_with_system(&self, path: &Path); - - fn on_quit(&self, callback: Box bool>); - fn on_reopen(&self, callback: Box); - fn on_system_wake(&self, callback: Box); - - // Mobile platform methods. On mobile the OS owns the application - // lifecycle: apps are backgrounded, foregrounded, and killed at the - // system's discretion, and must react rather than decide. - - /// Registers a callback invoked whenever the application's lifecycle - /// phase changes. See [`AppLifecyclePhase`] for the phase vocabulary and - /// its mapping onto iOS and Android. - /// - /// Desktop platforms never invoke this. - fn on_app_lifecycle(&self, _callback: Box) {} - - /// Registers a callback invoked when the OS signals memory pressure - /// (iOS `didReceiveMemoryWarning`, Android `onTrimMemory`). - /// - /// Desktop platforms never invoke this. - fn on_memory_warning(&self, _callback: Box) {} - - /// The platform's gesture recognition services, if it provides any - /// beyond gpui's portable recognizers. See - /// [`PlatformGestures`](crate::PlatformGestures). - fn gestures(&self) -> Option> { - None - } - - fn set_menus(&self, menus: Vec, keymap: &Keymap); - fn get_menus(&self) -> Option> { - None - } - - fn set_dock_menu(&self, menu: Vec, keymap: &Keymap); - fn perform_dock_menu_action(&self, _action: usize) {} - fn add_recent_document(&self, _path: &Path) {} - fn update_jump_list( - &self, - _menus: Vec, - _entries: Vec>, - ) -> Task>> { - Task::ready(Vec::new()) - } - fn on_app_menu_action(&self, callback: Box); - fn on_will_open_app_menu(&self, callback: Box); - fn on_validate_app_menu_command(&self, callback: Box bool>); - - fn thermal_state(&self) -> ThermalState; - fn on_thermal_state_change(&self, callback: Box); - - /// Sets the application's process-wide identity and user-visible name. - /// - /// The identifier is used for platform identity mechanisms such as the - /// Windows AppUserModelID. The name is used wherever the operating system - /// presents the application to the user. Call this once, early in startup, - /// before opening windows or posting notifications. - fn set_app_identity(&self, identifier: &str, name: &str) { - _ = (identifier, name); - } - - /// Posts a notification to the operating system's notification center. - /// - /// Posting a notification whose [`SystemNotification::tag`] matches an - /// earlier one replaces that notification where the platform supports it. - /// No-op on platforms without notification support, or when delivery is - /// unavailable (e.g. authorization was denied). - fn show_system_notification(&self, notification: SystemNotification) { - _ = notification; - } - - /// Removes the delivered or pending notification with this tag. - /// - /// Best-effort: some platforms cannot retract a notification once shown, - /// in which case it ages out of the notification center on its own. - fn dismiss_system_notification(&self, tag: &str) { - _ = tag; - } - - /// Registers the callback invoked when the user activates a system - /// notification, either by clicking its body or one of its action - /// buttons. - /// - /// Implementations must invoke the callback on the main thread. - fn on_system_notification_response( - &self, - callback: Box, - ) { - _ = callback; - } - - fn compositor_name(&self) -> &'static str { - "" - } - fn app_path(&self) -> Result; - fn path_for_auxiliary_executable(&self, name: &str) -> Result; - - fn set_cursor_style(&self, style: CursorStyle); - - /// Hides the mouse cursor until the user moves the mouse over one of - /// this application's windows. - fn hide_cursor_until_mouse_moves(&self); - - /// Returns whether the mouse cursor is currently visible. - fn is_cursor_visible(&self) -> bool; - - fn should_auto_hide_scrollbars(&self) -> bool; - - fn read_from_clipboard(&self) -> Option; - fn write_to_clipboard(&self, item: ClipboardItem); - - /// Reads the clipboard, resolving once its contents are available. - /// - /// Most platforms read synchronously and return a ready task. Platforms - /// whose clipboard access is inherently asynchronous and permission-gated - /// (e.g. the browser's async clipboard API) override this method; on those - /// platforms [`Platform::read_from_clipboard`] cannot return the clipboard - /// contents, so callers that can await should prefer this method. - fn read_from_clipboard_async(&self) -> Task, ClipboardReadError>> { - Task::ready(Ok(self.read_from_clipboard())) - } - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - fn read_from_primary(&self) -> Option; - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - fn write_to_primary(&self, item: ClipboardItem); - - #[cfg(target_os = "macos")] - fn read_from_find_pasteboard(&self) -> Option; - #[cfg(target_os = "macos")] - fn write_to_find_pasteboard(&self, item: ClipboardItem); - - fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task>; - fn read_credentials(&self, url: &str) -> Task)>>>; - fn delete_credentials(&self, url: &str) -> Task>; - - fn keyboard_layout(&self) -> Box; - fn keyboard_mapper(&self) -> Rc; - fn on_keyboard_layout_change(&self, callback: Box); -} - -/// A handle to a platform's display, e.g. a monitor or laptop screen. -pub trait PlatformDisplay: Debug { - /// Get the ID for this display - fn id(&self) -> DisplayId; - - /// Returns a stable identifier for this display that can be persisted and used - /// across system restarts. - fn uuid(&self) -> Result; - - /// Get the bounds for this display - fn bounds(&self) -> Bounds; - - /// Get the visible bounds for this display, excluding taskbar/dock areas. - /// This is the usable area where windows can be placed without being obscured. - /// Defaults to the full display bounds if not overridden. - fn visible_bounds(&self) -> Bounds { - self.bounds() - } - - /// Get the default bounds for this display to place a window - fn default_bounds(&self) -> Bounds { - let bounds = self.bounds(); - let center = bounds.center(); - let clipped_window_size = DEFAULT_WINDOW_SIZE.min(&bounds.size); - - let offset = clipped_window_size / 2.0; - let origin = point(center.x - offset.width, center.y - offset.height); - Bounds::new(origin, clipped_window_size) - } -} - -/// A notification posted to the operating system's notification center, -/// rather than rendered as in-app UI. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SystemNotification { - /// Stable identity for the notification. Posting a new notification with - /// the same tag replaces the previous one where the platform supports it, - /// and responses carry the tag back to the application. - pub tag: SharedString, - /// The notification's headline. - pub title: SharedString, - /// Additional text displayed below the title. - pub body: SharedString, - /// Buttons offered on the notification. Platforms that cannot display - /// action buttons show the notification without them. - pub actions: Vec, -} - -/// A button offered on a [`SystemNotification`]. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct SystemNotificationAction { - /// Identifies the action in [`SystemNotificationResponse::action_id`] - /// when the user presses this button. - pub id: SharedString, - /// The button's user-visible label. - pub label: SharedString, -} - -/// The user's activation of a [`SystemNotification`]. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SystemNotificationResponse { - /// The [`SystemNotification::tag`] of the activated notification. - pub tag: SharedString, - /// The pressed action button's [`SystemNotificationAction::id`], or - /// `None` when the user activated the notification body itself. - pub action_id: Option, -} - -/// Thermal state of the system -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ThermalState { - /// System has no thermal constraints - Nominal, - /// System is slightly constrained, reduce discretionary work - Fair, - /// System is moderately constrained, reduce CPU/GPU intensive work - Serious, - /// System is critically constrained, minimize all resource usage - Critical, -} - -/// Metadata for a given [ScreenCaptureSource] -#[derive(Clone)] -pub struct SourceMetadata { - /// Opaque identifier of this screen. - pub id: u64, - /// Human-readable label for this source. - pub label: Option, - /// Whether this source is the main display. - pub is_main: Option, - /// Video resolution of this source. - pub resolution: Size, -} - -/// A source of on-screen video content that can be captured. -pub trait ScreenCaptureSource { - /// Returns metadata for this source. - fn metadata(&self) -> Result; - - /// Start capture video from this source, invoking the given callback - /// with each frame. - fn stream( - &self, - foreground_executor: &ForegroundExecutor, - frame_callback: Box, - ) -> oneshot::Receiver>>; -} - -/// A video stream captured from a screen. -pub trait ScreenCaptureStream { - /// Returns metadata for this source. - fn metadata(&self) -> Result; -} - -/// A frame of video captured from a screen. -pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame); - -/// An opaque identifier for a hardware display -#[derive(PartialEq, Eq, Hash, Copy, Clone)] -pub struct DisplayId(pub(crate) u64); - -impl DisplayId { - /// Create a new `DisplayId` from a raw platform display identifier. - pub fn new(id: u64) -> Self { - Self(id) - } -} - -impl From for DisplayId { - fn from(id: u64) -> Self { - Self(id) - } -} - -impl From for u64 { - fn from(id: DisplayId) -> Self { - id.0 - } -} - -impl Debug for DisplayId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "DisplayId({})", self.0) - } -} - -/// Which part of the window to resize -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ResizeEdge { - /// The top edge - Top, - /// The top right corner - TopRight, - /// The right edge - Right, - /// The bottom right corner - BottomRight, - /// The bottom edge - Bottom, - /// The bottom left corner - BottomLeft, - /// The left edge - Left, - /// The top left corner - TopLeft, -} - -/// A type to describe the appearance of a window -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)] -pub enum WindowDecorations { - #[default] - /// Server side decorations - Server, - /// Client side decorations - Client, -} - -/// A type to describe how this window is currently configured -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)] -pub enum Decorations { - /// The window is configured to use server side decorations - #[default] - Server, - /// The window is configured to use client side decorations - Client { - /// The edge tiling state - tiling: Tiling, - }, -} - -/// What window controls this platform supports -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct WindowControls { - /// Whether this platform supports fullscreen - pub fullscreen: bool, - /// Whether this platform supports maximize - pub maximize: bool, - /// Whether this platform supports minimize - pub minimize: bool, - /// Whether this platform supports a window menu - pub window_menu: bool, -} - -impl Default for WindowControls { - fn default() -> Self { - // Assume that we can do anything, unless told otherwise - Self { - fullscreen: true, - maximize: true, - minimize: true, - window_menu: true, - } - } -} - -/// A window control button type used in [`WindowButtonLayout`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum WindowButton { - /// The minimize button - Minimize, - /// The maximize button - Maximize, - /// The close button - Close, -} - -impl WindowButton { - /// Returns a stable element ID for rendering this button. - pub fn id(&self) -> &'static str { - match self { - WindowButton::Minimize => "minimize", - WindowButton::Maximize => "maximize", - WindowButton::Close => "close", - } - } - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - fn index(&self) -> usize { - match self { - WindowButton::Minimize => 0, - WindowButton::Maximize => 1, - WindowButton::Close => 2, - } - } -} - -/// Maximum number of [`WindowButton`]s per side in the titlebar. -pub const MAX_BUTTONS_PER_SIDE: usize = 3; - -/// Describes which [`WindowButton`]s appear on each side of the titlebar. -/// -/// On Linux, this is read from the desktop environment's configuration -/// (e.g. GNOME's `gtk-decoration-layout` gsetting) via [`WindowButtonLayout::parse`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct WindowButtonLayout { - /// Buttons on the left side of the titlebar. - pub left: [Option; MAX_BUTTONS_PER_SIDE], - /// Buttons on the right side of the titlebar. - pub right: [Option; MAX_BUTTONS_PER_SIDE], -} - -#[cfg(any(target_os = "linux", target_os = "freebsd"))] -impl WindowButtonLayout { - /// Returns Zed's built-in fallback button layout for Linux titlebars. - pub fn linux_default() -> Self { - Self { - left: [None; MAX_BUTTONS_PER_SIDE], - right: [ - Some(WindowButton::Minimize), - Some(WindowButton::Maximize), - Some(WindowButton::Close), - ], - } - } - - /// Parses a GNOME-style `button-layout` string (e.g. `"close,minimize:maximize"`). - pub fn parse(layout_string: &str) -> Result { - fn parse_side( - s: &str, - seen_buttons: &mut [bool; MAX_BUTTONS_PER_SIDE], - unrecognized: &mut Vec, - ) -> [Option; MAX_BUTTONS_PER_SIDE] { - let mut result = [None; MAX_BUTTONS_PER_SIDE]; - let mut i = 0; - for name in s.split(',') { - let trimmed = name.trim(); - if trimmed.is_empty() { - continue; - } - let button = match trimmed { - "minimize" => Some(WindowButton::Minimize), - "maximize" => Some(WindowButton::Maximize), - "close" => Some(WindowButton::Close), - other => { - unrecognized.push(other.to_string()); - None - } - }; - if let Some(button) = button { - if seen_buttons[button.index()] { - continue; - } - if let Some(slot) = result.get_mut(i) { - *slot = Some(button); - seen_buttons[button.index()] = true; - i += 1; - } - } - } - result - } - - let (left_str, right_str) = layout_string.split_once(':').unwrap_or(("", layout_string)); - let mut unrecognized = Vec::new(); - let mut seen_buttons = [false; MAX_BUTTONS_PER_SIDE]; - let layout = Self { - left: parse_side(left_str, &mut seen_buttons, &mut unrecognized), - right: parse_side(right_str, &mut seen_buttons, &mut unrecognized), - }; - - if !unrecognized.is_empty() - && layout.left.iter().all(Option::is_none) - && layout.right.iter().all(Option::is_none) - { - bail!( - "button layout string {:?} contains no valid buttons (unrecognized: {})", - layout_string, - unrecognized.join(", ") - ); - } - - Ok(layout) - } - - /// Formats the layout back into a GNOME-style `button-layout` string. - #[cfg(test)] - pub fn format(&self) -> String { - fn format_side(buttons: &[Option; MAX_BUTTONS_PER_SIDE]) -> String { - buttons - .iter() - .flatten() - .map(|button| match button { - WindowButton::Minimize => "minimize", - WindowButton::Maximize => "maximize", - WindowButton::Close => "close", - }) - .collect::>() - .join(",") - } - - format!("{}:{}", format_side(&self.left), format_side(&self.right)) - } -} - -/// A type to describe which sides of the window are currently tiled in some way -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)] -pub struct Tiling { - /// Whether the top edge is tiled - pub top: bool, - /// Whether the left edge is tiled - pub left: bool, - /// Whether the right edge is tiled - pub right: bool, - /// Whether the bottom edge is tiled - pub bottom: bool, -} - -impl Tiling { - /// Initializes a [`Tiling`] type with all sides tiled - pub fn tiled() -> Self { - Self { - top: true, - left: true, - right: true, - bottom: true, - } - } - - /// Whether any edge is tiled - pub fn is_tiled(&self) -> bool { - self.top || self.left || self.right || self.bottom - } -} - -/// Callbacks for the accessibility adapter. -pub struct A11yCallbacks { - /// Called when the adapter is activated (a screen reader connects). - pub activation: Box Option + Send + 'static>, - /// Called when an action is requested by the screen reader. - pub action: Box, - /// Called when the adapter is deactivated (screen reader disconnects). - pub deactivation: Box, -} - -#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] -#[expect(missing_docs)] -pub struct RequestFrameOptions { - /// Whether a presentation is required. - pub require_presentation: bool, - /// Force refresh of all rendering states when true. - pub force_render: bool, -} - -/// The application's lifecycle phase, as owned and reported by a mobile OS. -/// -/// `Inactive` means visible but not receiving input (a system dialog on -/// top), while `Background` means not visible at all, with process death -/// possible at any time thereafter. -/// -/// | Phase | iOS | Android | -/// |--------------|------------------------------|--------------| -/// | `Active` | `didBecomeActive` | `onResume` | -/// | `Inactive` | `willResignActive` | `onPause` | -/// | `Background` | `didEnterBackground` | `onStop` | -/// | `Foreground` | `willEnterForeground` | `onStart` | -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum AppLifecyclePhase { - /// Foreground and receiving input. - Active, - /// Foreground (visible) but not receiving input. - Inactive, - /// Not visible. The GPU surface may be destroyed while backgrounded and - /// the process may be killed without further notice. - Background, - /// Becoming visible again, before input is restored. - Foreground, -} - -/// Regions of a window that are obscured or reserved by the system. -/// -/// Mobile applications often share space in their window with system-specific -/// geometry, from keyboards to camera notches. In GPUI, all this is abstracted -/// into a single "inset" which should be overlaid on the window's bounds. -/// It is up to the application develop to determine how to handle these cases. -#[derive(Debug, Clone, Default, PartialEq)] -pub struct WindowInsets { - /// Regions covered by system UI or hardware: status bar, display - /// cutouts/notch, home indicator, navigation bars. - /// (iOS: `safeAreaInsets`. Android: `WindowInsets` of types - /// `systemBars() | displayCutout()`.) - pub safe_area: Edges, - /// The region covered by the keyboard, when present. - /// (iOS: derived from `keyboardWillShow`/frame-change notifications. - /// Android: `WindowInsets.Type.ime()`.) - pub ime: Edges, -} - -impl WindowInsets { - /// The combined inset content should avoid. - pub fn effective(&self) -> Edges { - Edges { - top: self.safe_area.top.max(self.ime.top), - right: self.safe_area.right.max(self.ime.right), - bottom: self.safe_area.bottom.max(self.ime.bottom), - left: self.safe_area.left.max(self.ime.left), - } - } -} - -/// A change in the state of the focused text input. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum TextInputStateChange { - /// An editable element gained focus. - FocusGained, - /// The focused editable element lost focus. - FocusLost, - /// The selection or caret moved - SelectionChanged, - /// The document content changed outside of platform-initiated edits. - ContentChanged, -} - -#[expect(missing_docs)] -pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle { - fn bounds(&self) -> Bounds; - fn is_maximized(&self) -> bool; - fn window_bounds(&self) -> WindowBounds; - fn content_size(&self) -> Size; - fn resize(&mut self, size: Size); - fn scale_factor(&self) -> f32; - fn appearance(&self) -> WindowAppearance; - fn display(&self) -> Option>; - fn mouse_position(&self) -> Point; - fn modifiers(&self) -> Modifiers; - fn capslock(&self) -> Capslock; - fn set_input_handler(&mut self, input_handler: PlatformInputHandler); - fn take_input_handler(&mut self) -> Option; - /// Apply the focused text region's [`TextInputConfiguration`] to the - /// platform's text input session (e.g. attributes of the hidden editable - /// element on web). Called only when the configuration changes, because - /// reconfiguring a live input session can restart the IME connection. - fn set_text_input_configuration(&mut self, _configuration: TextInputConfiguration) {} - fn prompt( - &self, - level: PromptLevel, - msg: &str, - detail: Option<&str>, - answers: &[PromptButton], - ) -> Option>; - fn activate(&self); - /// Requests that the operating system draw attention to this window. - fn request_attention(&self) {} - fn is_active(&self) -> bool; - fn is_hovered(&self) -> bool; - fn background_appearance(&self) -> WindowBackgroundAppearance; - fn set_title(&mut self, title: &str); - fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance); - fn minimize(&self); - fn zoom(&self); - fn toggle_fullscreen(&self); - fn is_fullscreen(&self) -> bool; - fn frame_waker(&self) -> Option> { - None - } - fn on_request_frame(&self, callback: Box); - fn on_input(&self, callback: Box DispatchEventResult>); - fn on_active_status_change(&self, callback: Box); - fn on_hover_status_change(&self, callback: Box); - fn on_resize(&self, callback: Box, f32)>); - fn on_moved(&self, callback: Box); - fn on_should_close(&self, callback: Box bool>); - fn on_hit_test_window_control(&self, callback: Box Option>); - fn on_close(&self, callback: Box); - fn on_appearance_changed(&self, callback: Box); - fn on_button_layout_changed(&self, _callback: Box) {} - fn draw(&self, scene: &Scene); - fn schedule_frame(&self) {} - fn sprite_atlas(&self) -> Arc; - fn is_subpixel_rendering_supported(&self) -> bool; - - // macOS specific methods - fn get_title(&self) -> String { - String::new() - } - fn tabbed_windows(&self) -> Option> { - None - } - fn tab_bar_visible(&self) -> bool { - false - } - fn set_edited(&mut self, _edited: bool) {} - fn set_document_path(&self, _path: Option<&std::path::Path>) {} - fn toggle_simple_fullscreen(&self) {} - fn is_simple_fullscreen(&self) -> bool { - false - } - #[cfg(target_os = "macos")] - fn set_traffic_light_position(&self, _position: Point) {} - fn show_character_palette(&self) {} - fn titlebar_double_click(&self, _is_resizable: bool, _is_minimizable: bool) {} - fn on_move_tab_to_new_window(&self, _callback: Box) {} - fn on_merge_all_windows(&self, _callback: Box) {} - fn on_select_previous_tab(&self, _callback: Box) {} - fn on_select_next_tab(&self, _callback: Box) {} - fn on_toggle_tab_bar(&self, _callback: Box) {} - fn merge_all_windows(&self) {} - fn move_tab_to_new_window(&self) {} - fn toggle_window_tab_overview(&self) {} - fn set_tabbing_identifier(&self, _identifier: Option) {} - - #[cfg(target_os = "windows")] - fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND; - - // Linux specific methods - fn inner_window_bounds(&self) -> WindowBounds { - self.window_bounds() - } - fn request_decorations(&self, _decorations: WindowDecorations) {} - fn show_window_menu(&self, _position: Point) {} - fn start_window_move(&self) {} - fn can_start_external_drag(&self) -> bool { - false - } - fn start_external_drag(&self, _payload: &ExternalDragPayload) -> bool { - false - } - fn start_window_resize(&self, _edge: ResizeEdge) {} - fn set_exclusive_zone(&self, _zone: Pixels) {} - #[cfg(all(target_os = "linux", feature = "wayland"))] - fn set_exclusive_edge(&self, _edge: layer_shell::Anchor) {} - fn set_input_region(&self, _region: Option<&[Bounds]>) {} - fn window_decorations(&self) -> Decorations { - Decorations::Server - } - fn set_app_id(&mut self, _app_id: &str) {} - fn map_window(&mut self) -> anyhow::Result<()> { - Ok(()) - } - fn window_controls(&self) -> WindowControls { - WindowControls::default() - } - fn set_client_inset(&self, _inset: Pixels) {} - fn gpu_specs(&self) -> Option; - - fn update_ime_position(&self, _bounds: Bounds); - - // Mobile platform methods. - - /// The regions of this window currently obscured or reserved by the - /// system. Zero on platforms without such regions. - fn insets(&self) -> WindowInsets { - WindowInsets::default() - } - - /// Registers a callback invoked whenever [`Self::insets`] change. - /// - /// Contract: fires continuously during animated transitions (Android - /// `WindowInsetsAnimation` progress; on iOS the platform interpolates - /// the keyboard animation curve on frame ticks) and is exact at rest. - fn on_insets_changed(&self, _callback: Box) {} - - /// Sets the handler for the system back action (Android back - /// button/gesture; no source on iOS or desktop). - fn set_back_handler(&self, _callback: Box) {} - - /// Declares whether the application would currently handle the system - /// back action (e.g. navigation depth > 0). - fn set_back_enabled(&self, _enabled: bool) {} - - /// Requests that the soft keyboard be shown. - fn show_soft_keyboard(&self) {} - - /// Requests that the soft keyboard be hidden. - fn hide_soft_keyboard(&self) {} - - /// Inform the operating system that the text input state has changed - fn text_input_state_changed(&self, _change: TextInputStateChange) {} - - fn play_system_bell(&self) {} - - /// Initialize the accessibility adapter with callbacks. - fn a11y_init(&self, _callbacks: A11yCallbacks) {} - - /// Provide a TreeUpdate to the accessibility adapter. - fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {} - - /// Inform the adapter of updated window bounds. - fn a11y_update_window_bounds(&self) {} - - #[cfg(any(test, feature = "test-support", feature = "bench-support"))] - fn as_test(&mut self) -> Option<&mut TestWindow> { - None - } - - /// Renders the given scene to a texture and returns the pixel data as an RGBA image. - /// This does not present the frame to screen - useful for visual testing where we want - /// to capture what would be rendered without displaying it or requiring the window to be visible. - #[cfg(any(test, feature = "test-support"))] - fn render_to_image(&self, _scene: &Scene) -> Result { - anyhow::bail!("render_to_image not implemented for this platform") - } -} - -/// A renderer for headless windows that can produce real rendered output. -#[cfg(any(test, feature = "test-support", feature = "bench-support"))] -pub trait PlatformHeadlessRenderer { - /// Render a scene and return the result as an RGBA image. - fn render_scene_to_image( - &mut self, - scene: &Scene, - size: Size, - ) -> Result; - - /// Render a scene to an offscreen target without reading the result back. - /// - /// This is the headless analogue of presenting a frame: it performs the - /// same CPU-side scene encoding and GPU submission as drawing to a real - /// window, but doesn't block on GPU completion or copy pixels back. - fn render_scene(&mut self, scene: &Scene, size: Size) -> Result<()>; - - /// Returns the sprite atlas used by this renderer. - fn sprite_atlas(&self) -> Arc; -} - -/// Type alias for runnables with metadata. -/// Previously an enum with a single variant, now simplified to a direct type alias. -#[doc(hidden)] -pub type RunnableVariant = Runnable; - -#[doc(hidden)] -pub type TimerResolutionGuard = gpui_util::Deferred>; - -#[doc(hidden)] -pub enum TasksIncluded { - OnlyCompleted, - CompletedAndRunning, -} - -/// This type is public so that our test macro can generate and use it, but it should not -/// be considered part of our public API. -#[doc(hidden)] -pub trait PlatformDispatcher: Send + Sync { - fn is_main_thread(&self) -> bool; - fn dispatch(&self, runnable: RunnableVariant, priority: Priority); - fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority); - fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant); - - fn dispatch_on_main_thread_when_idle( - &self, - runnable: RunnableVariant, - timeout: Option, - ) { - let _ = timeout; - self.dispatch_on_main_thread(runnable, Priority::Low); - } - - fn idle_time_remaining(&self) -> Option { - None - } - - fn spawn_realtime(&self, f: Box); - - fn now(&self) -> Instant { - Instant::now() - } - - fn increase_timer_resolution(&self) -> TimerResolutionGuard { - gpui_util::defer(Box::new(|| {})) - } - - #[cfg(any(test, feature = "test-support", feature = "bench-support"))] - fn as_test(&self) -> Option<&TestDispatcher> { - None - } - - // This cfg must match the `threaded_dispatcher` module's, which implements - // this method whenever it compiles. - #[cfg(any(test, feature = "test-support", feature = "bench-support"))] - fn as_threaded(&self) -> Option<&ThreadedDispatcher> { - None - } -} - -#[expect(missing_docs)] -pub trait PlatformTextSystem: Send + Sync { - fn add_fonts(&self, fonts: Vec>) -> Result<()>; - /// Get all available font names. - fn all_font_names(&self) -> Vec; - /// Get the font ID for a font descriptor. - fn font_id(&self, descriptor: &Font) -> Result; - /// Prewarm any system font caches needed to shape text. - fn prewarm_fonts(&self, _font_ids: &[FontId]) {} - /// Get metrics for a font. - fn font_metrics(&self, font_id: FontId) -> FontMetrics; - /// Get typographic bounds for a glyph. - fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result>; - /// Get the advance width for a glyph. - fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result>; - /// Get the glyph ID for a character. - fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option; - /// Get raster bounds for a glyph. - fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result>; - /// Rasterize a glyph. - fn rasterize_glyph( - &self, - params: &RenderGlyphParams, - raster_bounds: Bounds, - ) -> Result<(Size, Vec)>; - /// Layout a line of text with the given font runs. - fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout; - /// Returns the recommended text rendering mode for the given font and size. - fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels) - -> TextRenderingMode; - /// Returns the dilation level to use for a glyph painted in the given color. - fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 { - 0 - } -} - -#[expect(missing_docs)] -pub struct NoopTextSystem; - -#[expect(missing_docs)] -impl NoopTextSystem { - #[allow(dead_code)] - pub fn new() -> Self { - Self - } -} - -impl PlatformTextSystem for NoopTextSystem { - fn add_fonts(&self, _fonts: Vec>) -> Result<()> { - Ok(()) - } - - fn all_font_names(&self) -> Vec { - Vec::new() - } - - fn font_id(&self, _descriptor: &Font) -> Result { - Ok(FontId(1)) - } - - fn font_metrics(&self, _font_id: FontId) -> FontMetrics { - FontMetrics { - units_per_em: 1000, - ascent: 1025.0, - descent: -275.0, - line_gap: 0.0, - underline_position: -95.0, - underline_thickness: 60.0, - cap_height: 698.0, - x_height: 516.0, - bounding_box: Bounds { - origin: Point { - x: -260.0, - y: -245.0, - }, - size: Size { - width: 1501.0, - height: 1364.0, - }, - }, - } - } - - fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result> { - Ok(Bounds { - origin: Point { x: 54.0, y: 0.0 }, - size: size(392.0, 528.0), - }) - } - - fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result> { - Ok(size(600.0 * glyph_id.0 as f32, 0.0)) - } - - fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option { - Some(GlyphId(ch.len_utf16() as u32)) - } - - fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result> { - Ok(Default::default()) - } - - fn rasterize_glyph( - &self, - _params: &RenderGlyphParams, - raster_bounds: Bounds, - ) -> Result<(Size, Vec)> { - Ok((raster_bounds.size, Vec::new())) - } - - fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout { - let mut position = px(0.); - let metrics = self.font_metrics(FontId(0)); - let em_width = font_size - * self - .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap()) - .unwrap() - .width - / metrics.units_per_em as f32; - let mut glyphs = Vec::new(); - for (ix, c) in text.char_indices() { - if let Some(glyph) = self.glyph_for_char(FontId(0), c) { - glyphs.push(ShapedGlyph { - id: glyph, - position: point(position, px(0.)), - index: ix, - is_emoji: glyph.0 == 2, - }); - if glyph.0 == 2 { - position += em_width * 2.0; - } else { - position += em_width; - } - } else { - position += em_width - } - } - let mut runs = Vec::default(); - if !glyphs.is_empty() { - runs.push(ShapedRun { - font_id: FontId(0), - glyphs, - }); - } else { - position = px(0.); - } - - LineLayout { - font_size, - width: position, - ascent: font_size * (metrics.ascent / metrics.units_per_em as f32), - descent: font_size * (metrics.descent / metrics.units_per_em as f32), - runs, - len: text.len(), - } - } - - fn recommended_rendering_mode( - &self, - _font_id: FontId, - _font_size: Pixels, - ) -> TextRenderingMode { - TextRenderingMode::Grayscale - } -} - -// Adapted from https://github.com/microsoft/terminal/blob/1283c0f5b99a2961673249fa77c6b986efb5086c/src/renderer/atlas/dwrite.cpp -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -/// Compute gamma correction ratios for subpixel text rendering. -#[allow(dead_code)] -pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] { - const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [ - [0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0], // gamma = 1.0 - [0.0166 / 4.0, -0.0807 / 4.0, 0.2227 / 4.0, -0.0751 / 4.0], // gamma = 1.1 - [0.0350 / 4.0, -0.1760 / 4.0, 0.4325 / 4.0, -0.1370 / 4.0], // gamma = 1.2 - [0.0543 / 4.0, -0.2821 / 4.0, 0.6302 / 4.0, -0.1876 / 4.0], // gamma = 1.3 - [0.0739 / 4.0, -0.3963 / 4.0, 0.8167 / 4.0, -0.2287 / 4.0], // gamma = 1.4 - [0.0933 / 4.0, -0.5161 / 4.0, 0.9926 / 4.0, -0.2616 / 4.0], // gamma = 1.5 - [0.1121 / 4.0, -0.6395 / 4.0, 1.1588 / 4.0, -0.2877 / 4.0], // gamma = 1.6 - [0.1300 / 4.0, -0.7649 / 4.0, 1.3159 / 4.0, -0.3080 / 4.0], // gamma = 1.7 - [0.1469 / 4.0, -0.8911 / 4.0, 1.4644 / 4.0, -0.3234 / 4.0], // gamma = 1.8 - [0.1627 / 4.0, -1.0170 / 4.0, 1.6051 / 4.0, -0.3347 / 4.0], // gamma = 1.9 - [0.1773 / 4.0, -1.1420 / 4.0, 1.7385 / 4.0, -0.3426 / 4.0], // gamma = 2.0 - [0.1908 / 4.0, -1.2652 / 4.0, 1.8650 / 4.0, -0.3476 / 4.0], // gamma = 2.1 - [0.2031 / 4.0, -1.3864 / 4.0, 1.9851 / 4.0, -0.3501 / 4.0], // gamma = 2.2 - ]; - - const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32; - const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32; - - let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10; - let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index]; - - [ - ratios[0] * NORM13, - ratios[1] * NORM24, - ratios[2] * NORM13, - ratios[3] * NORM24, - ] -} - -#[derive(PartialEq, Eq, Hash, Clone)] -#[expect(missing_docs)] -pub enum AtlasKey { - Glyph(RenderGlyphParams), - Svg(RenderSvgParams), - Image(RenderImageParams), -} - -impl AtlasKey { - #[cfg_attr( - all( - any(target_os = "linux", target_os = "freebsd"), - not(any(feature = "x11", feature = "wayland")) - ), - allow(dead_code) - )] - /// Returns the texture kind for this atlas key. - pub fn texture_kind(&self) -> AtlasTextureKind { - match self { - AtlasKey::Glyph(params) => { - if params.is_emoji { - AtlasTextureKind::Polychrome - } else if params.subpixel_rendering { - AtlasTextureKind::Subpixel - } else { - AtlasTextureKind::Monochrome - } - } - AtlasKey::Svg(_) => AtlasTextureKind::Monochrome, - AtlasKey::Image(_) => AtlasTextureKind::Polychrome, - } - } -} - -impl From for AtlasKey { - fn from(params: RenderGlyphParams) -> Self { - Self::Glyph(params) - } -} - -impl From for AtlasKey { - fn from(params: RenderSvgParams) -> Self { - Self::Svg(params) - } -} - -impl From for AtlasKey { - fn from(params: RenderImageParams) -> Self { - Self::Image(params) - } -} - -#[expect(missing_docs)] -pub trait PlatformAtlas { - fn get_or_insert_with<'a>( - &self, - key: &AtlasKey, - build: &mut dyn FnMut() -> Result, Cow<'a, [u8]>)>>, - ) -> Result>; - fn remove(&self, key: &AtlasKey); - - #[cfg(any(test, feature = "test-support", feature = "bench-support"))] - fn contains(&self, _key: &AtlasKey) -> bool { - false - } -} - -#[doc(hidden)] -pub struct AtlasTextureList { - pub textures: Vec>, - pub free_list: Vec, -} - -impl Default for AtlasTextureList { - fn default() -> Self { - Self { - textures: Vec::default(), - free_list: Vec::default(), - } - } -} - -impl ops::Index for AtlasTextureList { - type Output = Option; - - fn index(&self, index: usize) -> &Self::Output { - &self.textures[index] - } -} - -impl AtlasTextureList { - #[allow(unused)] - pub fn drain(&mut self) -> std::vec::Drain<'_, Option> { - self.free_list.clear(); - self.textures.drain(..) - } - - #[allow(dead_code)] - pub fn iter_mut(&mut self) -> impl DoubleEndedIterator { - self.textures.iter_mut().flatten() - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[repr(C)] -#[expect(missing_docs)] -pub struct AtlasTile { - /// The texture this tile belongs to. - pub texture_id: AtlasTextureId, - /// The unique ID of this tile within its texture. - pub tile_id: TileId, - /// Padding around the tile content in pixels. - pub padding: u32, - /// The bounds of this tile within the texture. - pub bounds: Bounds, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -#[repr(C)] -#[expect(missing_docs)] -pub struct AtlasTextureId { - // We use u32 instead of usize for Metal Shader Language compatibility - /// The index of this texture in the atlas. - pub index: u32, - /// The kind of content stored in this texture. - pub kind: AtlasTextureKind, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -#[repr(C)] -#[cfg_attr( - all( - any(target_os = "linux", target_os = "freebsd"), - not(any(feature = "x11", feature = "wayland")) - ), - allow(dead_code) -)] -#[expect(missing_docs)] -pub enum AtlasTextureKind { - Monochrome = 0, - Polychrome = 1, - Subpixel = 2, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -#[repr(C)] -#[expect(missing_docs)] -pub struct TileId(pub u32); - -impl From for TileId { - fn from(id: etagere::AllocId) -> Self { - Self(id.serialize()) - } -} - -impl From for etagere::AllocId { - fn from(id: TileId) -> Self { - Self::deserialize(id.0) - } -} - -#[expect(missing_docs)] -pub struct PlatformInputHandler { - cx: AsyncWindowContext, - handler: Box, -} - -#[expect(missing_docs)] -#[cfg_attr( - all( - any(target_os = "linux", target_os = "freebsd"), - not(any(feature = "x11", feature = "wayland")) - ), - allow(dead_code) -)] -impl PlatformInputHandler { - pub fn new(cx: AsyncWindowContext, handler: Box) -> Self { - Self { cx, handler } - } - - pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option { - self.cx - .update(|window, cx| { - self.handler - .selected_text_range(ignore_disabled_input, window, cx) - }) - .ok() - .flatten() - } - - #[cfg_attr(target_os = "windows", allow(dead_code))] - pub fn marked_text_range(&mut self) -> Option> { - self.cx - .update(|window, cx| self.handler.marked_text_range(window, cx)) - .ok() - .flatten() - } - - #[cfg_attr( - any(target_os = "linux", target_os = "freebsd", target_os = "windows"), - allow(dead_code) - )] - pub fn text_for_range( - &mut self, - range_utf16: Range, - adjusted: &mut Option>, - ) -> Option { - self.cx - .update(|window, cx| { - self.handler - .text_for_range(range_utf16, adjusted, window, cx) - }) - .ok() - .flatten() - } - - pub fn replace_text_in_range(&mut self, replacement_range: Option>, text: &str) { - self.cx - .update(|window, cx| { - self.handler - .replace_text_in_range(replacement_range, text, window, cx); - }) - .ok(); - } - - pub fn replace_and_mark_text_in_range( - &mut self, - range_utf16: Option>, - new_text: &str, - new_selected_range: Option>, - ) { - self.cx - .update(|window, cx| { - self.handler.replace_and_mark_text_in_range( - range_utf16, - new_text, - new_selected_range, - window, - cx, - ) - }) - .ok(); - } - - #[cfg_attr(target_os = "windows", allow(dead_code))] - pub fn unmark_text(&mut self) { - self.cx - .update(|window, cx| self.handler.unmark_text(window, cx)) - .ok(); - } - - pub fn paste(&mut self, item: ClipboardItem) { - self.cx - .update(|window, cx| self.handler.paste(item, window, cx)) - .ok(); - } - - pub fn bounds_for_range(&mut self, range_utf16: Range) -> Option> { - self.cx - .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx)) - .ok() - .flatten() - } - - #[allow(dead_code)] - pub fn apple_press_and_hold_enabled(&mut self) -> bool { - self.handler.apple_press_and_hold_enabled() - } - - pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) { - self.handler.replace_text_in_range(None, input, window, cx); - } - - pub fn compute_ime_candidate_bounds( - marked_range: Option>, - selection: &UTF16Selection, - mut bounds_for_range: impl FnMut(Range) -> Option>, - ) -> Option> { - if let Some(marked_range) = marked_range { - // Default to the start of the marked (composing) range. - let mut line_start = marked_range.start; - - // Walk backward from the caret looking for a line break. A change in - // the Y coordinate means we crossed into the previous visual line, so - // the line start is one position after the break point. - let caret = selection.range.end; - if let Some(caret_bounds) = bounds_for_range(caret..caret) { - for i in (marked_range.start..caret).rev() { - if let Some(b) = bounds_for_range(i..i) { - if (b.origin.y - caret_bounds.origin.y).abs() > px(0.1) { - line_start = i + 1; - break; - } - } - } - } - bounds_for_range(line_start..line_start) - } else { - // No active composition — use the selection endpoint. - let offset = if selection.reversed { - selection.range.start - } else { - selection.range.end - }; - bounds_for_range(offset..offset) - } - } - - pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option> { - let marked_range = self.handler.marked_text_range(window, cx); - let selection = self.handler.selected_text_range(true, window, cx)?; - Self::compute_ime_candidate_bounds(marked_range, &selection, |range| { - self.handler.bounds_for_range(range, window, cx) - }) - } - - pub fn ime_candidate_bounds(&mut self) -> Option> { - let marked_range = self.marked_text_range(); - let selection = self.selected_text_range(true)?; - Self::compute_ime_candidate_bounds(marked_range, &selection, |range| { - self.bounds_for_range(range) - }) - } - - #[allow(unused)] - pub fn character_index_for_point(&mut self, point: Point) -> Option { - self.cx - .update(|window, cx| self.handler.character_index_for_point(point, window, cx)) - .ok() - .flatten() - } - - /// See [`InputHandler::set_selected_text_range`]. - pub fn set_selected_text_range(&mut self, range_utf16: Range) { - self.cx - .update(|window, cx| { - self.handler - .set_selected_text_range(range_utf16, window, cx) - }) - .ok(); - } - - /// See [`InputHandler::element_bounds`]. - pub fn element_bounds(&mut self) -> Option> { - self.cx - .update(|window, cx| self.handler.element_bounds(window, cx)) - .ok() - .flatten() - } - - /// See [`InputHandler::text_length_utf16`]. - pub fn text_length_utf16(&mut self) -> Option { - self.cx - .update(|window, cx| self.handler.text_length_utf16(window, cx)) - .ok() - .flatten() - } - - #[allow(dead_code)] - pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool { - self.handler.accepts_text_input(window, cx) - } - - #[allow(dead_code)] - pub fn query_accepts_text_input(&mut self) -> bool { - self.cx - .update(|window, cx| self.handler.accepts_text_input(window, cx)) - .unwrap_or(true) - } - - /// See [`InputHandler::prefers_ime_for_printable_keys`]. - /// - /// This is not a pure delegation to the handler: while a multi-stroke binding is pending this - /// returns `false` regardless of the handler's preference, because the next printable key may - /// complete a binding whose prefix already bypassed the IME. - pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool { - self.cx - .update(|window, cx| { - // The next printable key may complete a chord whose prefix bypassed the IME. - !window.has_pending_keystrokes() - && self.handler.prefers_ime_for_printable_keys(window, cx) - }) - .unwrap_or(false) - } - - /// See [`InputHandler::text_input_configuration`]. - pub fn text_input_configuration( - &mut self, - window: &mut Window, - cx: &mut App, - ) -> TextInputConfiguration { - self.handler.text_input_configuration(window, cx) - } - - /// See [`InputHandler::text_input_editable_range`]. - pub fn text_input_editable_range(&mut self) -> Option> { - self.cx - .update(|window, cx| self.handler.text_input_editable_range(window, cx)) - .ok() - .flatten() - } -} - -/// A struct representing a selection in a text buffer, in UTF16 characters. -/// This is different from a range because the head may be before the tail. -#[derive(Debug)] -pub struct UTF16Selection { - /// The range of text in the document this selection corresponds to - /// in UTF16 characters. - pub range: Range, - /// Whether the head of this selection is at the start (true), or end (false) - /// of the range - pub reversed: bool, -} - -/// Zed's interface for handling text input from the platform's IME system -/// This is currently a 1:1 exposure of the NSTextInputClient API: -/// -/// -pub trait InputHandler: 'static { - /// Get the range of the user's currently selected text, if any - /// Corresponds to [selectedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438242-selectedrange) - /// - /// Return value is in terms of UTF-16 characters, from 0 to the length of the document - fn selected_text_range( - &mut self, - ignore_disabled_input: bool, - window: &mut Window, - cx: &mut App, - ) -> Option; - - /// Get the range of the currently marked text, if any - /// Corresponds to [markedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438250-markedrange) - /// - /// Return value is in terms of UTF-16 characters, from 0 to the length of the document - fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option>; - - /// Get the text for the given document range in UTF-16 characters - /// Corresponds to [attributedSubstring(forProposedRange: actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438238-attributedsubstring) - /// - /// range_utf16 is in terms of UTF-16 characters - fn text_for_range( - &mut self, - range_utf16: Range, - adjusted_range: &mut Option>, - window: &mut Window, - cx: &mut App, - ) -> Option; - - /// Replace the text in the given document range with the given text - /// Corresponds to [insertText(_:replacementRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438258-inserttext) - /// - /// replacement_range is in terms of UTF-16 characters - fn replace_text_in_range( - &mut self, - replacement_range: Option>, - text: &str, - window: &mut Window, - cx: &mut App, - ); - - /// Replace the text in the given document range with the given text, - /// and mark the given text as part of an IME 'composing' state - /// Corresponds to [setMarkedText(_:selectedRange:replacementRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438246-setmarkedtext) - /// - /// range_utf16 is in terms of UTF-16 characters - /// new_selected_range is in terms of UTF-16 characters - fn replace_and_mark_text_in_range( - &mut self, - range_utf16: Option>, - new_text: &str, - new_selected_range: Option>, - window: &mut Window, - cx: &mut App, - ); - - /// Remove the IME 'composing' state from the document - /// Corresponds to [unmarkText()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438239-unmarktext) - fn unmark_text(&mut self, window: &mut Window, cx: &mut App); - - /// Insert a platform-initiated paste at the current selection. - /// - /// Platforms that deliver paste as an input event rather than through an - /// application-defined action (e.g. the DOM `paste` event on web) call - /// this with the full clipboard contents. The default implementation - /// inserts only the plain-text portion of the item. - fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut App) { - if let Some(text) = item.text() { - self.replace_text_in_range(None, &text, window, cx); - } - } - - /// Get the bounds of the given document range in screen coordinates - /// Corresponds to [firstRect(forCharacterRange:actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438240-firstrect) - /// - /// This is used for positioning the IME candidate window - fn bounds_for_range( - &mut self, - range_utf16: Range, - window: &mut Window, - cx: &mut App, - ) -> Option>; - - /// Get the character offset for the given point in terms of UTF16 characters - /// - /// Corresponds to [characterIndexForPoint:](https://developer.apple.com/documentation/appkit/nstextinputclient/characterindex(for:)) - fn character_index_for_point( - &mut self, - point: Point, - window: &mut Window, - cx: &mut App, - ) -> Option; - - /// Set the range of the user's currently selected text. - /// - /// This is the reverse data-flow direction from [`Self::selected_text_range`]: - /// platforms call it when the system text machinery moves the selection on the - /// application's behalf — e.g. the user drags a system selection handle or - /// invokes Select All from system UI (iOS `UITextInput setSelectedTextRange:`, - /// Android `InputConnection.setSelection`). - /// - /// range_utf16 is in terms of UTF-16 characters, from 0 to the length of the document - fn set_selected_text_range( - &mut self, - _range_utf16: Range, - _window: &mut Window, - _cx: &mut App, - ) { - } - - /// Get the bounds of the focused text element in window coordinates, if known. - /// - /// This is the pull counterpart to the [`PlatformWindow::update_ime_position`] - /// push: mobile platforms ask for the focused element's geometry when they - /// need it (e.g. to frame system text-interaction UI overlaid on the focused - /// element). - fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option> { - None - } - - /// Get the length of the document in UTF-16 characters, if known. - fn text_length_utf16(&mut self, _window: &mut Window, _cx: &mut App) -> Option { - None - } - - /// Allows a given input context to opt into getting raw key repeats instead of - /// sending these to the platform. - /// TODO: Ideally we should be able to set ApplePressAndHoldEnabled in NSUserDefaults - /// (which is how iTerm does it) but it doesn't seem to work for me. - #[allow(dead_code)] - fn apple_press_and_hold_enabled(&mut self) -> bool { - true - } - - /// Returns whether this handler is accepting text input to be inserted. - fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool { - true - } - - /// The contiguous range of text, in UTF-16 code units, that platform text - /// input may read and edit around the current selection. - /// - /// Platforms that mirror document text into an IME-editable buffer clamp - /// the mirrored window to this range, so multi-step IME edit gestures - /// (word deletion, autocorrect rewrites, suggestion picks) cannot reach - /// content outside it. The range should contain the current selection; - /// when it cannot (a selection spanning a region boundary), platforms - /// degrade the mirrored IME context rather than widening the range. - /// `None` places no bound. - fn text_input_editable_range( - &mut self, - _window: &mut Window, - _cx: &mut App, - ) -> Option> { - None - } - - /// Returns whether printable keys should be routed to the IME before keybinding - /// matching when a non-ASCII input source (e.g. Japanese, Korean, Chinese IME) - /// is active. This prevents multi-stroke keybindings like `jj` from intercepting - /// keys that the IME should compose. - /// - /// Defaults to `false`. The editor overrides this based on whether it expects - /// character input (e.g. Vim insert mode returns `true`, normal mode returns `false`). - /// The terminal keeps the default `false` so that raw keys reach the terminal process. - fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool { - false - } - - /// Get this handler's preferences for platform text assistance. - /// - /// GPUI re-queries this every frame and forwards it to the platform window - /// only when it changes, so implementations must be cheap and may vary the - /// result with application state (e.g. with the cursor's position). - fn text_input_configuration( - &mut self, - _window: &mut Window, - _cx: &mut App, - ) -> TextInputConfiguration { - TextInputConfiguration::default() - } -} - -/// Platform text-assistance preferences for the focused text region. -/// -/// Returned by [`InputHandler::text_input_configuration`] and forwarded to the -/// platform whenever it changes; the platform maps the fields onto its native -/// input-session attributes (on web, DOM attributes of the hidden editable -/// element such as `autocorrect` and `enterkeyhint`). -/// -/// The default disables all text assistance and requests no particular action -/// key presentation. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct TextInputConfiguration { - /// Whether the platform may automatically correct entered text. - pub autocorrect: bool, - /// How software keyboards automatically capitalize entered text. - pub autocapitalize: Autocapitalize, - /// Whether software keyboards may offer word suggestions and spellcheck. - pub suggestions: bool, - /// The action advertised on a software keyboard's confirm ("enter") key. - pub input_action: TextInputAction, -} - -/// Automatic capitalization applied by software keyboards. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum Autocapitalize { - /// No automatic capitalization. - #[default] - None, - /// Capitalize the first letter of each word. - Words, - /// Capitalize the first letter of each sentence. - Sentences, - /// Capitalize every letter. - Characters, -} - -/// The action a software keyboard advertises on its confirm ("enter") key. -/// -/// This affects only how the key is presented (icon or label); pressing it is -/// still delivered as ordinary input. -/// -/// The variants are the HTML `enterkeyhint` attribute's value set -/// (), -/// which also maps onto Android's `IME_ACTION_*` constants and iOS's -/// `UIReturnKeyType`; [`TextInputAction::Unspecified`] means "emit no hint". -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum TextInputAction { - /// Let the platform choose its default presentation. - #[default] - Unspecified, - /// Inserting a line break. - Enter, - /// Committing the field's value. - Done, - /// Navigating to the typed target. - Go, - /// Moving to the next field. - Next, - /// Moving to the previous field. - Previous, - /// Executing a search. - Search, - /// Sending a message. - Send, -} - -/// The variables that can be configured when creating a new window -#[derive(Debug)] -pub struct WindowOptions { - /// Specifies the state and bounds of the window in screen coordinates. - /// - `None`: Inherit the bounds. - /// - `Some(WindowBounds)`: Open a window with corresponding state and its restore size. - pub window_bounds: Option, - - /// The titlebar configuration of the window - pub titlebar: Option, - - /// Whether the window should be focused when created - pub focus: bool, - - /// Whether the window should be shown when created - pub show: bool, - - /// The kind of window to create - pub kind: WindowKind, - - /// Whether the window can be moved by the user. When `false`, the user cannot drag - /// the window (on macOS this sets `NSWindow.isMovable`, which also disables the - /// Window-menu tiling items); programmatic moves are still allowed. - pub is_movable: bool, - - /// Whether the application owns dragging of the (custom) titlebar, rather than - /// AppKit. Only has an effect on macOS. - /// - /// Set this to `true` for windows that draw their own titlebar and move the window - /// themselves via [`Window::start_window_move`]. It marks the whole content view as - /// app-owned titlebar content, so AppKit neither drags the window from the titlebar - /// nor delays titlebar clicks while disambiguating double-clicks (a delay first - /// observed on macOS 27). It is independent of `is_movable`, so such windows stay - /// user-movable (via their own drag) and keep the Window-menu tiling items enabled. - /// - /// Leave this `false` for windows that rely on AppKit's native titlebar dragging. - pub app_owns_titlebar_drag: bool, - - /// The minimum interval between animation frames while the window is inactive. - /// - /// Set to `None` to disable inactive-window animation frame throttling. - pub inactive_frame_interval: Option, - - /// Whether the window should be resizable by the user - pub is_resizable: bool, - - /// Whether the window should be minimized by the user - pub is_minimizable: bool, - - /// The display to create the window on, if this is None, - /// the window will be created on the main display - pub display_id: Option, - - /// The appearance of the window background. - pub window_background: WindowBackgroundAppearance, - - /// Application identifier of the window. Can by used by desktop environments to group applications together. - pub app_id: Option, - - /// Window minimum size - pub window_min_size: Option>, - - /// Whether to use client or server-side decorations on X11 and Wayland. - /// The platform may ignore requests it cannot satisfy. - pub window_decorations: Option, - - /// Icon image (X11 only) - pub icon: Option>, - - /// Tab group name, allows opening the window as a native tab on macOS 10.12+. Windows with the same tabbing identifier will be grouped together. - pub tabbing_identifier: Option, -} - -/// The variables that can be configured when creating a new window -#[derive(Debug)] -#[cfg_attr( - all( - any(target_os = "linux", target_os = "freebsd"), - not(any(feature = "x11", feature = "wayland")) - ), - allow(dead_code) -)] -#[allow(missing_docs)] -pub struct WindowParams { - pub bounds: Bounds, - - /// The titlebar configuration of the window - #[cfg_attr(feature = "wayland", allow(dead_code))] - pub titlebar: Option, - - /// The kind of window to create - #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] - pub kind: WindowKind, - - /// Whether the window should be movable by the user - #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] - pub is_movable: bool, - - /// Whether the application owns dragging of the (custom) titlebar (macOS only) - #[cfg_attr( - any(target_os = "linux", target_os = "freebsd", target_os = "windows"), - allow(dead_code) - )] - pub app_owns_titlebar_drag: bool, - - /// Whether the window should be resizable by the user - #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] - pub is_resizable: bool, - - /// Whether the window should be minimized by the user - #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] - pub is_minimizable: bool, - - #[cfg_attr( - any(target_os = "linux", target_os = "freebsd", target_os = "windows"), - allow(dead_code) - )] - pub focus: bool, - - #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] - pub show: bool, - - /// An image to set as the window icon (x11 only) - #[cfg_attr(feature = "wayland", allow(dead_code))] - pub icon: Option>, - - #[cfg_attr(feature = "wayland", allow(dead_code))] - pub display_id: Option, - - #[cfg_attr(feature = "wayland", allow(dead_code))] - pub app_id: Option, - - pub window_min_size: Option>, - - #[cfg(target_os = "macos")] - pub tabbing_identifier: Option, -} - -/// Represents the status of how a window should be opened. -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum WindowBounds { - /// Indicates that the window should open in a windowed state with the given bounds. - Windowed(Bounds), - /// Indicates that the window should open in a maximized state. - /// The bounds provided here represent the restore size of the window. - Maximized(Bounds), - /// Indicates that the window should open in fullscreen mode. - /// The bounds provided here represent the restore size of the window. - Fullscreen(Bounds), -} - -impl Default for WindowBounds { - fn default() -> Self { - WindowBounds::Windowed(Bounds::default()) - } -} - -impl WindowBounds { - /// Retrieve the inner bounds - pub fn get_bounds(&self) -> Bounds { - match self { - WindowBounds::Windowed(bounds) => *bounds, - WindowBounds::Maximized(bounds) => *bounds, - WindowBounds::Fullscreen(bounds) => *bounds, - } - } - - /// Creates a new window bounds that centers the window on the screen. - pub fn centered(size: Size, cx: &App) -> Self { - WindowBounds::Windowed(Bounds::centered(None, size, cx)) - } -} - -impl Default for WindowOptions { - fn default() -> Self { - Self { - window_bounds: None, - titlebar: Some(TitlebarOptions { - title: Default::default(), - appears_transparent: Default::default(), - traffic_light_position: Default::default(), - }), - focus: true, - show: true, - kind: WindowKind::Normal, - is_movable: true, - app_owns_titlebar_drag: false, - inactive_frame_interval: Some(Duration::from_micros(33_333)), - is_resizable: true, - is_minimizable: true, - display_id: None, - window_background: WindowBackgroundAppearance::default(), - icon: None, - app_id: None, - window_min_size: None, - window_decorations: None, - tabbing_identifier: None, - } - } -} - -/// The options that can be configured for a window's titlebar -#[derive(Debug, Default)] -pub struct TitlebarOptions { - /// The initial title of the window - pub title: Option, - - /// Should the default system titlebar be hidden to allow for a custom-drawn titlebar? (macOS and Windows only) - /// Refer to [`WindowOptions::window_decorations`] on Linux - pub appears_transparent: bool, - - /// The position of the macOS traffic light buttons - pub traffic_light_position: Option>, -} - -/// The kind of window to create -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum WindowKind { - /// A normal application window - Normal, - - /// A window that appears above all other windows, usually used for alerts or popups - /// use sparingly! - PopUp, - - /// A parent-anchored, platform-native popup window for menus, comboboxes, context menus and - /// tooltips. Unlike [`WindowKind::PopUp`], it is positioned relative to a parent window. - /// - /// The popup's size comes from [`WindowOptions::window_bounds`], whose origin is ignored. - /// See [`popup::PopupOptions`] for the placement options. Platforms without a native - /// implementation reject it with [`popup::PopupNotSupportedError`]. - AnchoredPopup(popup::PopupOptions), - - /// A floating window that appears on top of its parent window - Floating, - - /// A Wayland LayerShell window, used to draw overlays or backgrounds for applications such as - /// docks, notifications or wallpapers. - #[cfg(all(target_os = "linux", feature = "wayland"))] - LayerShell(layer_shell::LayerShellOptions), - - /// A window that appears on top of its parent window and blocks interaction with it - /// until the modal window is closed - Dialog, -} - -/// The appearance of the window, as defined by the operating system. -/// -/// On macOS, this corresponds to named [`NSAppearance`](https://developer.apple.com/documentation/appkit/nsappearance) -/// values. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -pub enum WindowAppearance { - /// A light appearance. - /// - /// On macOS, this corresponds to the `aqua` appearance. - #[default] - Light, - - /// A light appearance with vibrant colors. - /// - /// On macOS, this corresponds to the `NSAppearanceNameVibrantLight` appearance. - VibrantLight, - - /// A dark appearance. - /// - /// On macOS, this corresponds to the `darkAqua` appearance. - Dark, - - /// A dark appearance with vibrant colors. - /// - /// On macOS, this corresponds to the `NSAppearanceNameVibrantDark` appearance. - VibrantDark, -} - -/// The appearance of the background of the window itself, when there is -/// no content or the content is transparent. -#[derive(Copy, Clone, Debug, Default, PartialEq)] -pub enum WindowBackgroundAppearance { - /// Opaque. - /// - /// This lets the window manager know that content behind this - /// window does not need to be drawn. - /// - /// Actual color depends on the system and themes should define a fully - /// opaque background color instead. - #[default] - Opaque, - /// Plain alpha transparency. - Transparent, - /// Transparency, but the contents behind the window are blurred. - /// - /// Not always supported. - Blurred, - /// The Mica backdrop material, supported on Windows 11. - MicaBackdrop, - /// The Mica Alt backdrop material, supported on Windows 11. - MicaAltBackdrop, -} - -/// The text rendering mode to use for drawing glyphs. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -pub enum TextRenderingMode { - /// Use the platform's default text rendering mode. - #[default] - PlatformDefault, - /// Use subpixel (ClearType-style) text rendering. - Subpixel, - /// Use grayscale text rendering. - Grayscale, -} - -/// The options that can be configured for a file dialog prompt -#[derive(Clone, Debug)] -pub struct PathPromptOptions { - /// Should the prompt allow files to be selected? - pub files: bool, - /// Should the prompt allow directories to be selected? - pub directories: bool, - /// Should the prompt allow multiple files to be selected? - pub multiple: bool, - /// The prompt to show to a user when selecting a path - pub prompt: Option, -} - -/// What kind of prompt styling to show -#[derive(Copy, Clone, Debug, PartialEq)] -pub enum PromptLevel { - /// A prompt that is shown when the user should be notified of something - Info, - - /// A prompt that is shown when the user needs to be warned of a potential problem - Warning, - - /// A prompt that is shown when a critical problem has occurred - Critical, -} - -/// Prompt Button -#[derive(Clone, Debug, PartialEq)] -pub enum PromptButton { - /// Ok button - Ok(SharedString), - /// Cancel button - Cancel(SharedString), - /// Other button - Other(SharedString), -} - -impl PromptButton { - /// Create a button with label - pub fn new(label: impl Into) -> Self { - PromptButton::Other(label.into()) - } - - /// Create an Ok button - pub fn ok(label: impl Into) -> Self { - PromptButton::Ok(label.into()) - } - - /// Create a Cancel button - pub fn cancel(label: impl Into) -> Self { - PromptButton::Cancel(label.into()) - } - - /// Returns true if this button is a cancel button. - #[allow(dead_code)] - pub fn is_cancel(&self) -> bool { - matches!(self, PromptButton::Cancel(_)) - } - - /// Returns the label of the button - pub fn label(&self) -> &SharedString { - match self { - PromptButton::Ok(label) => label, - PromptButton::Cancel(label) => label, - PromptButton::Other(label) => label, - } - } -} - -impl From<&str> for PromptButton { - fn from(value: &str) -> Self { - match value.to_lowercase().as_str() { - "ok" => PromptButton::Ok("OK".into()), - "cancel" => PromptButton::Cancel("Cancel".into()), - _ => PromptButton::Other(SharedString::from(value.to_owned())), - } - } -} - -/// The style of the cursor (pointer) -#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] -pub enum CursorStyle { - /// The default cursor - #[default] - Arrow, - - /// A text input cursor - /// corresponds to the CSS cursor value `text` - IBeam, - - /// A crosshair cursor - /// corresponds to the CSS cursor value `crosshair` - Crosshair, - - /// A closed hand cursor - /// corresponds to the CSS cursor value `grabbing` - ClosedHand, - - /// An open hand cursor - /// corresponds to the CSS cursor value `grab` - OpenHand, - - /// A pointing hand cursor - /// corresponds to the CSS cursor value `pointer` - PointingHand, - - /// A resize left cursor - /// corresponds to the CSS cursor value `w-resize` - ResizeLeft, - - /// A resize right cursor - /// corresponds to the CSS cursor value `e-resize` - ResizeRight, - - /// A resize cursor to the left and right - /// corresponds to the CSS cursor value `ew-resize` - ResizeLeftRight, - - /// A resize up cursor - /// corresponds to the CSS cursor value `n-resize` - ResizeUp, - - /// A resize down cursor - /// corresponds to the CSS cursor value `s-resize` - ResizeDown, - - /// A resize cursor directing up and down - /// corresponds to the CSS cursor value `ns-resize` - ResizeUpDown, - - /// A resize cursor directing up-left and down-right - /// corresponds to the CSS cursor value `nesw-resize` - ResizeUpLeftDownRight, - - /// A resize cursor directing up-right and down-left - /// corresponds to the CSS cursor value `nwse-resize` - ResizeUpRightDownLeft, - - /// A cursor indicating that the item/column can be resized horizontally. - /// corresponds to the CSS cursor value `col-resize` - ResizeColumn, - - /// A cursor indicating that the item/row can be resized vertically. - /// corresponds to the CSS cursor value `row-resize` - ResizeRow, - - /// A text input cursor for vertical layout - /// corresponds to the CSS cursor value `vertical-text` - IBeamCursorForVerticalLayout, - - /// A cursor indicating that the operation is not allowed - /// corresponds to the CSS cursor value `not-allowed` - OperationNotAllowed, - - /// A cursor indicating that the operation will result in a link - /// corresponds to the CSS cursor value `alias` - DragLink, - - /// A cursor indicating that the operation will result in a copy - /// corresponds to the CSS cursor value `copy` - DragCopy, - - /// A cursor indicating that the operation will result in a context menu - /// corresponds to the CSS cursor value `context-menu` - ContextualMenu, -} - -/// A clipboard item that should be copied to the clipboard -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ClipboardItem { - /// The entries in this clipboard item. - pub entries: Vec, -} - -/// An error produced by [`Platform::read_from_clipboard_async`]. -/// -/// Callers surface these failures to users, so the variants distinguish -/// conditions that call for different user-facing guidance. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum ClipboardReadError { - /// The platform clipboard is not available in this context, e.g. the - /// browser does not expose the async clipboard API or the page is not a - /// secure context. - Unavailable, - /// The platform refused access, e.g. the user declined the browser's - /// clipboard permission prompt or paste confirmation. - Denied(String), - /// The clipboard contents could not be converted into a - /// [`ClipboardItem`]. - UnsupportedContent, -} - -impl std::fmt::Display for ClipboardReadError { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Unavailable => formatter.write_str("the clipboard is unavailable"), - Self::Denied(message) => { - write!(formatter, "clipboard access was denied: {message}") - } - Self::UnsupportedContent => { - formatter.write_str("the clipboard contents are unsupported") - } - } - } -} - -impl std::error::Error for ClipboardReadError {} - -/// Either a ClipboardString or a ClipboardImage -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum ClipboardEntry { - /// A string entry - String(ClipboardString), - /// An image entry - Image(Image), - /// A file entry - ExternalPaths(crate::ExternalPaths), -} - -impl ClipboardItem { - /// Create a new ClipboardItem::String with no associated metadata - pub fn new_string(text: String) -> Self { - Self { - entries: vec![ClipboardEntry::String(ClipboardString::new(text))], - } - } - - /// Create a new ClipboardItem::String with the given text and associated metadata - pub fn new_string_with_metadata(text: String, metadata: String) -> Self { - Self { - entries: vec![ClipboardEntry::String(ClipboardString { - text, - metadata: Some(metadata), - })], - } - } - - /// Create a new ClipboardItem::String with the given text and associated metadata - pub fn new_string_with_json_metadata(text: String, metadata: T) -> Self { - Self { - entries: vec![ClipboardEntry::String( - ClipboardString::new(text).with_json_metadata(metadata), - )], - } - } - - /// Create a new ClipboardItem::Image with the given image with no associated metadata - pub fn new_image(image: &Image) -> Self { - Self { - entries: vec![ClipboardEntry::Image(image.clone())], - } - } - - /// Concatenates together all the ClipboardString entries in the item. - /// Returns None if there were no ClipboardString entries. - pub fn text(&self) -> Option { - let mut answer = String::new(); - - for entry in self.entries.iter() { - if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry { - answer.push_str(text); - } - } - - if answer.is_empty() { - for entry in self.entries.iter() { - if let ClipboardEntry::ExternalPaths(paths) = entry { - for path in &paths.0 { - use std::fmt::Write as _; - _ = write!(answer, "{}", path.display()); - } - } - } - } - - if !answer.is_empty() { - Some(answer) - } else { - None - } - } - - /// If this item is one ClipboardEntry::String, returns its metadata. - #[cfg_attr(not(target_os = "windows"), allow(dead_code))] - pub fn metadata(&self) -> Option<&String> { - match self.entries().first() { - Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => { - clipboard_string.metadata.as_ref() - } - _ => None, - } - } - - /// Get the item's entries - pub fn entries(&self) -> &[ClipboardEntry] { - &self.entries - } - - /// Get owned versions of the item's entries - pub fn into_entries(self) -> impl Iterator { - self.entries.into_iter() - } -} - -impl From for ClipboardEntry { - fn from(value: ClipboardString) -> Self { - Self::String(value) - } -} - -impl From for ClipboardEntry { - fn from(value: String) -> Self { - Self::from(ClipboardString::from(value)) - } -} - -impl From for ClipboardEntry { - fn from(value: Image) -> Self { - Self::Image(value) - } -} - -impl From for ClipboardItem { - fn from(value: ClipboardEntry) -> Self { - Self { - entries: vec![value], - } - } -} - -impl From for ClipboardItem { - fn from(value: String) -> Self { - Self::from(ClipboardEntry::from(value)) - } -} - -impl From for ClipboardItem { - fn from(value: Image) -> Self { - Self::from(ClipboardEntry::from(value)) - } -} - -/// One of the editor's supported image formats (e.g. PNG, JPEG) - used when dealing with images in the clipboard -#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)] -pub enum ImageFormat { - // Sorted from most to least likely to be pasted into an editor, - // which matters when we iterate through them trying to see if - // clipboard content matches them. - /// .png - Png, - /// .jpeg or .jpg - Jpeg, - /// .webp - Webp, - /// .gif - Gif, - /// .svg - Svg, - /// .bmp - Bmp, - /// .tif or .tiff - Tiff, - /// .ico - Ico, - /// Netpbm image formats (.pbm, .ppm, .pgm). - Pnm, -} - -impl ImageFormat { - /// Returns the mime type for the ImageFormat - pub const fn mime_type(self) -> &'static str { - match self { - ImageFormat::Png => "image/png", - ImageFormat::Jpeg => "image/jpeg", - ImageFormat::Webp => "image/webp", - ImageFormat::Gif => "image/gif", - ImageFormat::Svg => "image/svg+xml", - ImageFormat::Bmp => "image/bmp", - ImageFormat::Tiff => "image/tiff", - ImageFormat::Ico => "image/ico", - ImageFormat::Pnm => "image/x-portable-anymap", - } - } - - /// Returns the file extension for this image format (without leading dot). - pub const fn extension(self) -> &'static str { - match self { - ImageFormat::Png => "png", - ImageFormat::Jpeg => "jpg", - ImageFormat::Webp => "webp", - ImageFormat::Gif => "gif", - ImageFormat::Svg => "svg", - ImageFormat::Bmp => "bmp", - ImageFormat::Tiff => "tiff", - ImageFormat::Ico => "ico", - ImageFormat::Pnm => "pnm", - } - } - - /// Returns the ImageFormat for the given mime type, including known aliases. - pub fn from_mime_type(mime_type: &str) -> Option { - use strum::IntoEnumIterator; - Self::iter() - .find(|format| format.mime_type() == mime_type) - .or_else(|| Self::from_mime_type_alias(mime_type)) - } - - /// Non-canonical mime types that some producers use in the wild. - /// Unlike `mime_type()` which returns the single canonical form, - /// these are legacy or shortened variants we still need to recognize. - fn from_mime_type_alias(mime_type: &str) -> Option { - match mime_type { - "image/jpg" => Some(Self::Jpeg), - "image/tif" => Some(Self::Tiff), - _ => None, - } - } -} - -/// An image, with a format and certain bytes -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Image { - /// The image format the bytes represent (e.g. PNG) - pub format: ImageFormat, - /// The raw image bytes - pub bytes: Vec, - /// The unique ID for the image - pub id: u64, -} - -pub(crate) fn decode_static_image( - bytes: &[u8], - format: image::ImageFormat, -) -> Result> { - let decoder = image::ImageReader::with_format(Cursor::new(bytes), format) - .into_decoder() - .context("creating image decoder")?; - decode_static_image_from_decoder(decoder) -} - -pub(crate) fn decode_static_image_from_decoder( - mut decoder: impl image::ImageDecoder, -) -> Result> { - let orientation = decoder - .orientation() - .context("reading decoder's orientation")?; - let mut image = DynamicImage::from_decoder(decoder).context("decoding image")?; - image.apply_orientation(orientation); - - let mut data = image.into_rgba8(); - for pixel in data.chunks_exact_mut(4) { - pixel.swap(0, 2); - } - - Ok(SmallVec::from_elem(Frame::new(data), 1)) -} - -impl Hash for Image { - fn hash(&self, state: &mut H) { - state.write_u64(self.id); - } -} - -impl Image { - /// An empty image containing no data - pub fn empty() -> Self { - Self::from_bytes(ImageFormat::Png, Vec::new()) - } - - /// Create an image from a format and bytes - pub fn from_bytes(format: ImageFormat, bytes: Vec) -> Self { - Self { - id: hash(&bytes), - format, - bytes, - } - } - - /// Get this image's ID - pub fn id(&self) -> u64 { - self.id - } - - /// Use the GPUI `use_asset` API to make this image renderable - pub fn use_render_image( - self: Arc, - window: &mut Window, - cx: &mut App, - ) -> Option> { - ImageSource::Image(self) - .use_data(None, window, cx) - .and_then(|result| result.ok()) - } - - /// Use the GPUI `get_asset` API to make this image renderable - pub fn get_render_image( - self: Arc, - window: &mut Window, - cx: &mut App, - ) -> Option> { - ImageSource::Image(self) - .get_data(None, window, cx) - .and_then(|result| result.ok()) - } - - /// Use the GPUI `remove_asset` API to drop this image, if possible. - pub fn remove_asset(self: Arc, cx: &mut App) { - ImageSource::Image(self).remove_asset(cx); - } - - /// Check whether this image is present in GPUI's asset cache (loading or - /// loaded), without fetching it. - #[cfg(any(test, feature = "test-support"))] - pub fn is_asset_cached(self: &Arc, cx: &App) -> bool { - ImageSource::Image(self.clone()).is_asset_cached(cx) - } - - /// Convert the clipboard image to an `ImageData` object. - pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result> { - let frames = match self.format { - ImageFormat::Gif => { - let decoder = GifDecoder::new(Cursor::new(&self.bytes))?; - let mut frames = SmallVec::new(); - - for frame in decoder.into_frames() { - match frame { - Ok(mut frame) => { - // Convert from RGBA to BGRA. - for pixel in frame.buffer_mut().chunks_exact_mut(4) { - pixel.swap(0, 2); - } - frames.push(frame); - } - Err(err) => { - log::debug!("Skipping GIF frame due to decode error: {err}"); - } - } - } - - if frames.is_empty() { - anyhow::bail!("GIF could not be decoded: all frames failed"); - } - - frames - } - ImageFormat::Png => decode_static_image(&self.bytes, image::ImageFormat::Png)?, - ImageFormat::Jpeg => decode_static_image(&self.bytes, image::ImageFormat::Jpeg)?, - ImageFormat::Webp => decode_static_image(&self.bytes, image::ImageFormat::WebP)?, - ImageFormat::Bmp => decode_static_image(&self.bytes, image::ImageFormat::Bmp)?, - ImageFormat::Tiff => decode_static_image(&self.bytes, image::ImageFormat::Tiff)?, - ImageFormat::Ico => decode_static_image(&self.bytes, image::ImageFormat::Ico)?, - ImageFormat::Svg => { - return svg_renderer - .render_single_frame(&self.bytes, 1.0) - .map_err(Into::into); - } - ImageFormat::Pnm => decode_static_image(&self.bytes, image::ImageFormat::Pnm)?, - }; - - Ok(Arc::new(RenderImage::new(frames))) - } - - /// Get the format of the clipboard image - pub fn format(&self) -> ImageFormat { - self.format - } - - /// Get the raw bytes of the clipboard image - pub fn bytes(&self) -> &[u8] { - self.bytes.as_slice() - } -} - -/// A clipboard item that should be copied to the clipboard -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ClipboardString { - /// The text content. - pub text: String, - /// Optional metadata associated with this clipboard string. - pub metadata: Option, -} - -impl ClipboardString { - /// Create a new clipboard string with the given text - pub fn new(text: String) -> Self { - Self { - text, - metadata: None, - } - } - - /// Return a new clipboard item with the metadata replaced by the given metadata, - /// after serializing it as JSON. - pub fn with_json_metadata(mut self, metadata: T) -> Self { - self.metadata = Some(serde_json::to_string(&metadata).unwrap()); - self - } - - /// Get the text of the clipboard string - pub fn text(&self) -> &String { - &self.text - } - - /// Get the owned text of the clipboard string - pub fn into_text(self) -> String { - self.text - } - - /// Get the metadata of the clipboard string, formatted as JSON - pub fn metadata_json(&self) -> Option - where - T: for<'a> Deserialize<'a>, - { - self.metadata - .as_ref() - .and_then(|m| serde_json::from_str(m).ok()) - } - - #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] - /// Compute a hash of the given text for clipboard change detection. - pub fn text_hash(text: &str) -> u64 { - let mut hasher = SeaHasher::new(); - text.hash(&mut hasher); - hasher.finish() - } -} - -impl From for ClipboardString { - fn from(value: String) -> Self { - Self { - text: value, - metadata: None, - } - } -} - -#[cfg(test)] -mod image_tests { - use super::*; - use std::sync::Arc; - - #[test] - fn test_image_to_image_data_applies_exif_orientation() { - let image = Image::from_bytes( - ImageFormat::Jpeg, - include_bytes!("../examples/image/exif-orientation-rotate-180.jpg").to_vec(), - ); - - let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap(); - - assert_eq!(render_image.size(0), size(16.into(), 32.into())); - - let bytes = render_image.as_bytes(0).unwrap(); - assert_eq!(&bytes[..4], &[255, 255, 255, 255]); - assert_eq!(&bytes[(16 * 32 - 1) * 4..], &[0, 0, 0, 255]); - } - - #[test] - fn test_svg_image_to_image_data_converts_to_bgra() { - let image = Image::from_bytes( - ImageFormat::Svg, - br##" - -"## - .to_vec(), - ); - - let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap(); - let bytes = render_image.as_bytes(0).unwrap(); - - for pixel in bytes.chunks_exact(4) { - assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]); - } - } -} - -#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))] -mod tests { - use super::*; - use std::collections::HashSet; - - #[test] - fn test_window_button_layout_parse_standard() { - let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap(); - assert_eq!( - layout.left, - [ - Some(WindowButton::Close), - Some(WindowButton::Minimize), - None - ] - ); - assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]); - } - - #[test] - fn test_window_button_layout_parse_right_only() { - let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap(); - assert_eq!(layout.left, [None, None, None]); - assert_eq!( - layout.right, - [ - Some(WindowButton::Minimize), - Some(WindowButton::Maximize), - Some(WindowButton::Close) - ] - ); - } - - #[test] - fn test_window_button_layout_parse_left_only() { - let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap(); - assert_eq!( - layout.left, - [ - Some(WindowButton::Close), - Some(WindowButton::Minimize), - Some(WindowButton::Maximize) - ] - ); - assert_eq!(layout.right, [None, None, None]); - } - - #[test] - fn test_window_button_layout_parse_with_whitespace() { - let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap(); - assert_eq!( - layout.left, - [ - Some(WindowButton::Close), - Some(WindowButton::Minimize), - None - ] - ); - assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]); - } - - #[test] - fn test_window_button_layout_parse_empty() { - let layout = WindowButtonLayout::parse("").unwrap(); - assert_eq!(layout.left, [None, None, None]); - assert_eq!(layout.right, [None, None, None]); - } - - #[test] - fn test_window_button_layout_parse_intentionally_empty() { - let layout = WindowButtonLayout::parse(":").unwrap(); - assert_eq!(layout.left, [None, None, None]); - assert_eq!(layout.right, [None, None, None]); - } - - #[test] - fn test_window_button_layout_parse_invalid_buttons() { - let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap(); - assert_eq!( - layout.left, - [ - Some(WindowButton::Close), - Some(WindowButton::Minimize), - None - ] - ); - assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]); - } - - #[test] - fn test_window_button_layout_parse_deduplicates_same_side_buttons() { - let layout = WindowButtonLayout::parse("close,close,minimize").unwrap(); - assert_eq!( - layout.right, - [ - Some(WindowButton::Close), - Some(WindowButton::Minimize), - None - ] - ); - assert_eq!(layout.format(), ":close,minimize"); - } - - #[test] - fn test_window_button_layout_parse_deduplicates_buttons_across_sides() { - let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap(); - assert_eq!(layout.left, [Some(WindowButton::Close), None, None]); - assert_eq!( - layout.right, - [ - Some(WindowButton::Maximize), - Some(WindowButton::Minimize), - None - ] - ); - - let button_ids: Vec<_> = layout - .left - .iter() - .chain(layout.right.iter()) - .flatten() - .map(WindowButton::id) - .collect(); - let unique_button_ids = button_ids.iter().copied().collect::>(); - assert_eq!(unique_button_ids.len(), button_ids.len()); - assert_eq!(layout.format(), "close:maximize,minimize"); - } - - #[test] - fn test_window_button_layout_parse_gnome_style() { - let layout = WindowButtonLayout::parse("close").unwrap(); - assert_eq!(layout.left, [None, None, None]); - assert_eq!(layout.right, [Some(WindowButton::Close), None, None]); - } - - #[test] - fn test_window_button_layout_parse_elementary_style() { - let layout = WindowButtonLayout::parse("close:maximize").unwrap(); - assert_eq!(layout.left, [Some(WindowButton::Close), None, None]); - assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]); - } - - #[test] - fn test_window_button_layout_round_trip() { - let cases = [ - "close:minimize,maximize", - "minimize,maximize,close:", - ":close", - "close:", - "close:maximize", - ":", - ]; - - for case in cases { - let layout = WindowButtonLayout::parse(case).unwrap(); - assert_eq!(layout.format(), case, "Round-trip failed for: {}", case); - } - } - - #[test] - fn test_window_button_layout_linux_default() { - let layout = WindowButtonLayout::linux_default(); - assert_eq!(layout.left, [None, None, None]); - assert_eq!( - layout.right, - [ - Some(WindowButton::Minimize), - Some(WindowButton::Maximize), - Some(WindowButton::Close) - ] - ); - - let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap(); - assert_eq!(round_tripped, layout); - } - - #[test] - fn test_window_button_layout_parse_all_invalid() { - assert!(WindowButtonLayout::parse("asdfghjkl").is_err()); - } -} diff --git a/crates/gpui_pre/src/platform/app_menu.rs b/crates/gpui_pre/src/platform/app_menu.rs deleted file mode 100644 index 27c20c0..0000000 --- a/crates/gpui_pre/src/platform/app_menu.rs +++ /dev/null @@ -1,426 +0,0 @@ -use crate::{Action, App, Platform, SharedString}; - -/// A menu of the application, either a main menu or a submenu -pub struct Menu { - /// The name of the menu - pub name: SharedString, - - /// The items in the menu - pub items: Vec, - - /// Whether this menu is disabled - pub disabled: bool, -} - -impl Menu { - /// Create a new Menu with the given name - pub fn new(name: impl Into) -> Self { - Self { - name: name.into(), - items: vec![], - disabled: false, - } - } - - /// Set items to be in this menu - pub fn items(mut self, items: impl IntoIterator) -> Self { - self.items = items.into_iter().collect(); - self - } - - /// Set whether this menu is disabled - pub fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } - - /// Create an OwnedMenu from this Menu - pub fn owned(self) -> OwnedMenu { - OwnedMenu { - name: self.name.to_string().into(), - items: self.items.into_iter().map(|item| item.owned()).collect(), - disabled: self.disabled, - } - } -} - -/// OS menus are menus that are recognized by the operating system -/// This allows the operating system to provide specialized items for -/// these menus -pub struct OsMenu { - /// The name of the menu - pub name: SharedString, - - /// The type of menu - pub menu_type: SystemMenuType, -} - -impl OsMenu { - /// Create an OwnedOsMenu from this OsMenu - pub fn owned(self) -> OwnedOsMenu { - OwnedOsMenu { - name: self.name.to_string().into(), - menu_type: self.menu_type, - } - } -} - -/// The type of system menu -#[derive(Copy, Clone, Eq, PartialEq)] -pub enum SystemMenuType { - /// The 'Services' menu in the Application menu on macOS - Services, -} - -/// The different kinds of items that can be in a menu -pub enum MenuItem { - /// A separator between items - Separator, - - /// A submenu - Submenu(Menu), - - /// A menu, managed by the system (for example, the Services menu on macOS) - SystemMenu(OsMenu), - - /// An action that can be performed - Action { - /// The name of this menu item - name: SharedString, - - /// The action to perform when this menu item is selected - action: Box, - - /// The OS Action that corresponds to this action, if any - /// See [`OsAction`] for more information - os_action: Option, - - /// Whether this action is checked - checked: bool, - - /// Whether this action is disabled - disabled: bool, - }, -} - -impl MenuItem { - /// Creates a new menu item that is a separator - pub fn separator() -> Self { - Self::Separator - } - - /// Creates a new menu item that is a submenu - pub fn submenu(menu: Menu) -> Self { - Self::Submenu(menu) - } - - /// Creates a new submenu that is populated by the OS - pub fn os_submenu(name: impl Into, menu_type: SystemMenuType) -> Self { - Self::SystemMenu(OsMenu { - name: name.into(), - menu_type, - }) - } - - /// Creates a new menu item that invokes an action - pub fn action(name: impl Into, action: impl Action) -> Self { - Self::Action { - name: name.into(), - action: Box::new(action), - os_action: None, - checked: false, - disabled: false, - } - } - - /// Creates a new menu item that invokes an action and has an OS action - pub fn os_action( - name: impl Into, - action: impl Action, - os_action: OsAction, - ) -> Self { - Self::Action { - name: name.into(), - action: Box::new(action), - os_action: Some(os_action), - checked: false, - disabled: false, - } - } - - /// Create an OwnedMenuItem from this MenuItem - pub fn owned(self) -> OwnedMenuItem { - match self { - MenuItem::Separator => OwnedMenuItem::Separator, - MenuItem::Submenu(submenu) => OwnedMenuItem::Submenu(submenu.owned()), - MenuItem::Action { - name, - action, - os_action, - checked, - disabled, - } => OwnedMenuItem::Action { - name: name.into(), - action, - os_action, - checked, - disabled, - }, - MenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.owned()), - } - } - - /// Set whether this menu item is checked - /// - /// Only for [`MenuItem::Action`], otherwise, will be ignored - pub fn checked(mut self, checked: bool) -> Self { - match &mut self { - MenuItem::Action { checked: old, .. } => { - *old = checked; - } - _ => {} - } - self - } - - /// Returns whether this menu item is checked - /// - /// Only for [`MenuItem::Action`], otherwise, returns false - #[inline] - pub fn is_checked(&self) -> bool { - match self { - MenuItem::Action { checked, .. } => *checked, - _ => false, - } - } - - /// Set whether this menu item is disabled - pub fn disabled(mut self, disabled: bool) -> Self { - match &mut self { - MenuItem::Action { disabled: old, .. } => { - *old = disabled; - } - MenuItem::Submenu(submenu) => { - submenu.disabled = disabled; - } - _ => {} - } - self - } - - /// Returns whether this menu item is disabled - /// - /// Only for [`MenuItem::Action`] and [`MenuItem::Submenu`], otherwise, returns false - #[inline] - pub fn is_disabled(&self) -> bool { - match self { - MenuItem::Action { disabled, .. } => *disabled, - MenuItem::Submenu(submenu) => submenu.disabled, - _ => false, - } - } -} - -/// OS menus are menus that are recognized by the operating system -/// This allows the operating system to provide specialized items for -/// these menus -#[derive(Clone)] -pub struct OwnedOsMenu { - /// The name of the menu - pub name: SharedString, - - /// The type of menu - pub menu_type: SystemMenuType, -} - -/// A menu of the application, either a main menu or a submenu -#[derive(Clone)] -pub struct OwnedMenu { - /// The name of the menu - pub name: SharedString, - - /// The items in the menu - pub items: Vec, - - /// Whether this menu is disabled - pub disabled: bool, -} - -/// The different kinds of items that can be in a menu -pub enum OwnedMenuItem { - /// A separator between items - Separator, - - /// A submenu - Submenu(OwnedMenu), - - /// A menu, managed by the system (for example, the Services menu on macOS) - SystemMenu(OwnedOsMenu), - - /// An action that can be performed - Action { - /// The name of this menu item - name: String, - - /// The action to perform when this menu item is selected - action: Box, - - /// The OS Action that corresponds to this action, if any - /// See [`OsAction`] for more information - os_action: Option, - - /// Whether this action is checked - checked: bool, - - /// Whether this action is disabled - disabled: bool, - }, -} - -impl Clone for OwnedMenuItem { - fn clone(&self) -> Self { - match self { - OwnedMenuItem::Separator => OwnedMenuItem::Separator, - OwnedMenuItem::Submenu(submenu) => OwnedMenuItem::Submenu(submenu.clone()), - OwnedMenuItem::Action { - name, - action, - os_action, - checked, - disabled, - } => OwnedMenuItem::Action { - name: name.clone(), - action: action.boxed_clone(), - os_action: *os_action, - checked: *checked, - disabled: *disabled, - }, - OwnedMenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.clone()), - } - } -} - -// TODO: As part of the global selections refactor, these should -// be moved to GPUI-provided actions that make this association -// without leaking the platform details to GPUI users - -/// OS actions are actions that are recognized by the operating system -/// This allows the operating system to provide specialized behavior for -/// these actions -#[derive(Copy, Clone, Eq, PartialEq)] -pub enum OsAction { - /// The 'cut' action - Cut, - - /// The 'copy' action - Copy, - - /// The 'paste' action - Paste, - - /// The 'select all' action - SelectAll, - - /// The 'undo' action - Undo, - - /// The 'redo' action - Redo, -} - -pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &App) { - platform.on_will_open_app_menu(Box::new({ - let cx = cx.to_async(); - move || { - if let Some(app) = cx.app.upgrade() { - app.borrow_mut().update(|cx| cx.clear_pending_keystrokes()); - } - } - })); - - platform.on_validate_app_menu_command(Box::new({ - let cx = cx.to_async(); - move |action| { - cx.app - .upgrade() - .map(|app| app.borrow_mut().update(|cx| cx.is_action_available(action))) - .unwrap_or(false) - } - })); - - platform.on_app_menu_action(Box::new({ - let cx = cx.to_async(); - move |action| { - if let Some(app) = cx.app.upgrade() { - app.borrow_mut().update(|cx| cx.dispatch_action(action)); - } - } - })); -} - -#[cfg(test)] -mod tests { - use crate::Menu; - - #[test] - fn test_menu() { - let menu = Menu::new("App") - .items(vec![ - crate::MenuItem::action("Action 1", gpui::NoAction), - crate::MenuItem::separator(), - ]) - .disabled(true); - - assert_eq!(menu.name.as_ref(), "App"); - assert_eq!(menu.items.len(), 2); - assert!(menu.disabled); - } - - #[test] - fn test_menu_item_builder() { - use super::MenuItem; - - let item = MenuItem::action("Test Action", gpui::NoAction); - assert_eq!( - match &item { - MenuItem::Action { name, .. } => name.as_ref(), - _ => unreachable!(), - }, - "Test Action" - ); - assert!(matches!( - item, - MenuItem::Action { - checked: false, - disabled: false, - .. - } - )); - - assert!( - MenuItem::action("Test Action", gpui::NoAction) - .checked(true) - .is_checked() - ); - assert!( - MenuItem::action("Test Action", gpui::NoAction) - .disabled(true) - .is_disabled() - ); - - let submenu = MenuItem::submenu(super::Menu { - name: "Submenu".into(), - items: vec![], - disabled: true, - }); - assert_eq!( - match &submenu { - MenuItem::Submenu(menu) => menu.name.as_ref(), - _ => unreachable!(), - }, - "Submenu" - ); - assert!(!submenu.is_checked()); - assert!(submenu.is_disabled()); - } -} diff --git a/crates/gpui_pre/src/platform/keyboard.rs b/crates/gpui_pre/src/platform/keyboard.rs deleted file mode 100644 index 10b8620..0000000 --- a/crates/gpui_pre/src/platform/keyboard.rs +++ /dev/null @@ -1,41 +0,0 @@ -use collections::HashMap; - -use crate::{KeybindingKeystroke, Keystroke}; - -/// A trait for platform-specific keyboard layouts -pub trait PlatformKeyboardLayout { - /// Get the keyboard layout ID, which should be unique to the layout - fn id(&self) -> &str; - /// Get the keyboard layout display name - fn name(&self) -> &str; -} - -/// A trait for platform-specific keyboard mappings -pub trait PlatformKeyboardMapper { - /// Map a key equivalent to its platform-specific representation - fn map_key_equivalent( - &self, - keystroke: Keystroke, - use_key_equivalents: bool, - ) -> KeybindingKeystroke; - /// Get the key equivalents for the current keyboard layout, - /// only used on macOS - fn get_key_equivalents(&self) -> Option<&HashMap>; -} - -/// A dummy implementation of the platform keyboard mapper -pub struct DummyKeyboardMapper; - -impl PlatformKeyboardMapper for DummyKeyboardMapper { - fn map_key_equivalent( - &self, - keystroke: Keystroke, - _use_key_equivalents: bool, - ) -> KeybindingKeystroke { - KeybindingKeystroke::from_keystroke(keystroke) - } - - fn get_key_equivalents(&self) -> Option<&HashMap> { - None - } -} diff --git a/crates/gpui_pre/src/platform/keystroke.rs b/crates/gpui_pre/src/platform/keystroke.rs deleted file mode 100644 index c45c7c1..0000000 --- a/crates/gpui_pre/src/platform/keystroke.rs +++ /dev/null @@ -1,776 +0,0 @@ -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::{ - error::Error, - fmt::{Display, Write}, -}; - -use crate::PlatformKeyboardMapper; - -/// This is a helper trait so that we can simplify the implementation of some functions -pub trait AsKeystroke { - /// Returns the GPUI representation of the keystroke. - fn as_keystroke(&self) -> &Keystroke; -} - -/// A keystroke and associated metadata generated by the platform -#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)] -pub struct Keystroke { - /// the state of the modifier keys at the time the keystroke was generated - pub modifiers: Modifiers, - - /// key is the character printed on the key that was pressed - /// e.g. for option-s, key is "s" - /// On layouts that do not have ascii keys (e.g. Thai) - /// this will be the ASCII-equivalent character (q instead of ๆ), - /// and the typed character will be present in key_char. - pub key: String, - - /// key_char is the character that could have been typed when - /// this binding was pressed. - /// e.g. for s this is "s", for option-s "ß", and cmd-s None - pub key_char: Option, -} - -/// Represents a keystroke that can be used in keybindings and displayed to the user. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct KeybindingKeystroke { - /// The GPUI representation of the keystroke. - inner: Keystroke, - /// The modifiers to display. - #[cfg(target_os = "windows")] - display_modifiers: Modifiers, - /// The key to display. - #[cfg(target_os = "windows")] - display_key: String, -} - -/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use -/// markdown to display it. -#[derive(Debug)] -pub struct InvalidKeystrokeError { - /// The invalid keystroke. - pub keystroke: String, -} - -impl Error for InvalidKeystrokeError {} - -impl Display for InvalidKeystrokeError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "Invalid keystroke \"{}\". {}", - self.keystroke, KEYSTROKE_PARSE_EXPECTED_MESSAGE - ) - } -} - -/// Sentence explaining what keystroke parser expects, starting with "Expected ..." -pub const KEYSTROKE_PARSE_EXPECTED_MESSAGE: &str = "Expected a sequence of modifiers \ - (`ctrl`, `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) \ - followed by a key, separated by `-`."; - -impl Keystroke { - /// When matching a key we cannot know whether the user intended to type - /// the key_char or the key itself. On some non-US keyboards keys we use in our - /// bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard), - /// and on some keyboards the IME handler converts a sequence of keys into a - /// specific character (for example `"` is typed as `" space` on a brazilian keyboard). - /// - /// This method assumes that `self` was typed and `target' is in the keymap, and checks - /// both possibilities for self against the target. - pub fn should_match(&self, target: &KeybindingKeystroke) -> bool { - #[cfg(not(target_os = "windows"))] - if let Some(key_char) = self - .key_char - .as_ref() - .filter(|key_char| key_char != &&self.key) - { - let ime_modifiers = Modifiers { - control: self.modifiers.control, - platform: self.modifiers.platform, - ..Default::default() - }; - - if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers { - return true; - } - } - - #[cfg(target_os = "windows")] - if let Some(key_char) = self - .key_char - .as_ref() - .filter(|key_char| key_char != &&self.key) - { - // On Windows, if key_char is set, then the typed keystroke produced the key_char - if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() { - return true; - } - } - - target.inner.modifiers == self.modifiers && target.inner.key == self.key - } - - /// key syntax is: - /// [secondary-][ctrl-][alt-][shift-][cmd-][fn-]key[->key_char] - /// key_char syntax is only used for generating test events, - /// secondary means "cmd" on macOS and "ctrl" on other platforms - /// when matching a key with an key_char set will be matched without it. - pub fn parse(source: &str) -> std::result::Result { - let mut modifiers = Modifiers::none(); - let mut key = None; - let mut key_char = None; - - let mut components = source.split('-').peekable(); - while let Some(component) = components.next() { - if component.eq_ignore_ascii_case("ctrl") { - modifiers.control = true; - continue; - } - if component.eq_ignore_ascii_case("alt") { - modifiers.alt = true; - continue; - } - if component.eq_ignore_ascii_case("shift") { - modifiers.shift = true; - continue; - } - if component.eq_ignore_ascii_case("fn") { - modifiers.function = true; - continue; - } - if component.eq_ignore_ascii_case("secondary") { - if cfg!(target_os = "macos") { - modifiers.platform = true; - } else { - modifiers.control = true; - }; - continue; - } - - let is_platform = component.eq_ignore_ascii_case("cmd") - || component.eq_ignore_ascii_case("super") - || component.eq_ignore_ascii_case("win"); - - if is_platform { - modifiers.platform = true; - continue; - } - - let mut key_str = component.to_string(); - - if let Some(next) = components.peek() { - if next.is_empty() && source.ends_with('-') { - key = Some(String::from("-")); - break; - } else if next.len() > 1 && next.starts_with('>') { - key = Some(key_str); - key_char = Some(String::from(&next[1..])); - components.next(); - } else { - return Err(InvalidKeystrokeError { - keystroke: source.to_owned(), - }); - } - continue; - } - - if component.len() == 1 && component.as_bytes()[0].is_ascii_uppercase() { - // Convert to shift + lowercase char - modifiers.shift = true; - key_str.make_ascii_lowercase(); - } else { - // convert ascii chars to lowercase so that named keys like "tab" and "enter" - // are accepted case insensitively and stored how we expect so they are matched properly - key_str.make_ascii_lowercase() - } - key = Some(key_str); - } - - // Allow for the user to specify a keystroke modifier as the key itself - // This sets the `key` to the modifier, and disables the modifier - key = key.or_else(|| { - use std::mem; - // std::mem::take clears bool incase its true - if mem::take(&mut modifiers.shift) { - Some("shift".to_string()) - } else if mem::take(&mut modifiers.control) { - Some("control".to_string()) - } else if mem::take(&mut modifiers.alt) { - Some("alt".to_string()) - } else if mem::take(&mut modifiers.platform) { - Some("platform".to_string()) - } else if mem::take(&mut modifiers.function) { - Some("function".to_string()) - } else { - None - } - }); - - let key = key.ok_or_else(|| InvalidKeystrokeError { - keystroke: source.to_owned(), - })?; - - Ok(Keystroke { - modifiers, - key, - key_char, - }) - } - - /// Produces a representation of this key that Parse can understand. - pub fn unparse(&self) -> String { - unparse(&self.modifiers, &self.key) - } - - /// Returns true if this keystroke left - /// the ime system in an incomplete state. - pub fn is_ime_in_progress(&self) -> bool { - self.key_char.is_none() - && (is_printable_key(&self.key) || self.key.is_empty()) - && !(self.modifiers.platform - || self.modifiers.control - || self.modifiers.function - || self.modifiers.alt) - } - - /// Returns a new keystroke with the key_char filled. - /// This is used for dispatch_keystroke where we want users to - /// be able to simulate typing "space", etc. - pub fn with_simulated_ime(mut self) -> Self { - if self.key_char.is_none() - && !self.modifiers.platform - && !self.modifiers.control - && !self.modifiers.function - && !self.modifiers.alt - { - self.key_char = match self.key.as_str() { - "space" => Some(" ".into()), - "tab" => Some("\t".into()), - "enter" => Some("\n".into()), - key if !is_printable_key(key) || key.is_empty() => None, - key => { - if self.modifiers.shift { - Some(key.to_uppercase()) - } else { - Some(key.into()) - } - } - } - } - self - } -} - -impl KeybindingKeystroke { - #[cfg(target_os = "windows")] - #[expect(missing_docs)] - pub fn new(inner: Keystroke, display_modifiers: Modifiers, display_key: String) -> Self { - KeybindingKeystroke { - inner, - display_modifiers, - display_key, - } - } - - /// Create a new keybinding keystroke from the given keystroke using the given keyboard mapper. - pub fn new_with_mapper( - inner: Keystroke, - use_key_equivalents: bool, - keyboard_mapper: &dyn PlatformKeyboardMapper, - ) -> Self { - keyboard_mapper.map_key_equivalent(inner, use_key_equivalents) - } - - /// Create a new keybinding keystroke from the given keystroke, without any platform-specific mapping. - pub fn from_keystroke(keystroke: Keystroke) -> Self { - #[cfg(target_os = "windows")] - { - let key = keystroke.key.clone(); - let modifiers = keystroke.modifiers; - KeybindingKeystroke { - inner: keystroke, - display_modifiers: modifiers, - display_key: key, - } - } - #[cfg(not(target_os = "windows"))] - { - KeybindingKeystroke { inner: keystroke } - } - } - - /// Returns the GPUI representation of the keystroke. - pub fn inner(&self) -> &Keystroke { - &self.inner - } - - /// Returns the modifiers. - /// - /// Platform-specific behavior: - /// - On macOS and Linux, this modifiers is the same as `inner.modifiers`, which is the GPUI representation of the keystroke. - /// - On Windows, this modifiers is the display modifiers, for example, a `ctrl-@` keystroke will have `inner.modifiers` as - /// `Modifiers::control()` and `display_modifiers` as `Modifiers::control_shift()`. - pub fn modifiers(&self) -> &Modifiers { - #[cfg(target_os = "windows")] - { - &self.display_modifiers - } - #[cfg(not(target_os = "windows"))] - { - &self.inner.modifiers - } - } - - /// Returns the key. - /// - /// Platform-specific behavior: - /// - On macOS and Linux, this key is the same as `inner.key`, which is the GPUI representation of the keystroke. - /// - On Windows, this key is the display key, for example, a `ctrl-@` keystroke will have `inner.key` as `@` and `display_key` as `2`. - pub fn key(&self) -> &str { - #[cfg(target_os = "windows")] - { - &self.display_key - } - #[cfg(not(target_os = "windows"))] - { - &self.inner.key - } - } - - /// Sets the modifiers. On Windows this modifies both `inner.modifiers` and `display_modifiers`. - pub fn set_modifiers(&mut self, modifiers: Modifiers) { - self.inner.modifiers = modifiers; - #[cfg(target_os = "windows")] - { - self.display_modifiers = modifiers; - } - } - - /// Sets the key. On Windows this modifies both `inner.key` and `display_key`. - pub fn set_key(&mut self, key: String) { - #[cfg(target_os = "windows")] - { - self.display_key = key.clone(); - } - self.inner.key = key; - } - - /// Produces a representation of this key that Parse can understand. - pub fn unparse(&self) -> String { - #[cfg(target_os = "windows")] - { - unparse(&self.display_modifiers, &self.display_key) - } - #[cfg(not(target_os = "windows"))] - { - unparse(&self.inner.modifiers, &self.inner.key) - } - } - - /// Removes the key_char - pub fn remove_key_char(&mut self) { - self.inner.key_char = None; - } -} - -fn is_printable_key(key: &str) -> bool { - !matches!( - key, - "f1" | "f2" - | "f3" - | "f4" - | "f5" - | "f6" - | "f7" - | "f8" - | "f9" - | "f10" - | "f11" - | "f12" - | "f13" - | "f14" - | "f15" - | "f16" - | "f17" - | "f18" - | "f19" - | "f20" - | "f21" - | "f22" - | "f23" - | "f24" - | "f25" - | "f26" - | "f27" - | "f28" - | "f29" - | "f30" - | "f31" - | "f32" - | "f33" - | "f34" - | "f35" - | "backspace" - | "delete" - | "left" - | "right" - | "up" - | "down" - | "pageup" - | "pagedown" - | "insert" - | "home" - | "end" - | "back" - | "forward" - | "escape" - ) -} - -impl std::fmt::Display for Keystroke { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - display_modifiers(&self.modifiers, f)?; - display_key(&self.key, f) - } -} - -impl std::fmt::Display for KeybindingKeystroke { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - display_modifiers(self.modifiers(), f)?; - display_key(self.key(), f) - } -} - -/// The state of the modifier keys at some point in time -#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)] -pub struct Modifiers { - /// The control key - #[serde(default)] - pub control: bool, - - /// The alt key - /// Sometimes also known as the 'meta' key - #[serde(default)] - pub alt: bool, - - /// The shift key - #[serde(default)] - pub shift: bool, - - /// The command key, on macos - /// the windows key, on windows - /// the super key, on linux - #[serde(default)] - pub platform: bool, - - /// The function key - #[serde(default)] - pub function: bool, -} - -impl Modifiers { - /// Returns whether any modifier key is pressed. - pub fn modified(&self) -> bool { - self.control || self.alt || self.shift || self.platform || self.function - } - - /// Whether the semantically 'secondary' modifier key is pressed. - /// - /// On macOS, this is the command key. - /// On Linux and Windows, this is the control key. - pub fn secondary(&self) -> bool { - #[cfg(target_os = "macos")] - { - self.platform - } - - #[cfg(not(target_os = "macos"))] - { - self.control - } - } - - /// Returns how many modifier keys are pressed. - pub fn number_of_modifiers(&self) -> u8 { - self.control as u8 - + self.alt as u8 - + self.shift as u8 - + self.platform as u8 - + self.function as u8 - } - - /// Returns [`Modifiers`] with no modifiers. - pub fn none() -> Modifiers { - Default::default() - } - - /// Returns [`Modifiers`] with just the command key. - pub fn command() -> Modifiers { - Modifiers { - platform: true, - ..Default::default() - } - } - - /// A Returns [`Modifiers`] with just the secondary key pressed. - pub fn secondary_key() -> Modifiers { - #[cfg(target_os = "macos")] - { - Modifiers { - platform: true, - ..Default::default() - } - } - - #[cfg(not(target_os = "macos"))] - { - Modifiers { - control: true, - ..Default::default() - } - } - } - - /// Returns [`Modifiers`] with just the windows key. - pub fn windows() -> Modifiers { - Modifiers { - platform: true, - ..Default::default() - } - } - - /// Returns [`Modifiers`] with just the super key. - pub fn super_key() -> Modifiers { - Modifiers { - platform: true, - ..Default::default() - } - } - - /// Returns [`Modifiers`] with just control. - pub fn control() -> Modifiers { - Modifiers { - control: true, - ..Default::default() - } - } - - /// Returns [`Modifiers`] with just alt. - pub fn alt() -> Modifiers { - Modifiers { - alt: true, - ..Default::default() - } - } - - /// Returns [`Modifiers`] with just shift. - pub fn shift() -> Modifiers { - Modifiers { - shift: true, - ..Default::default() - } - } - - /// Returns [`Modifiers`] with just function. - pub fn function() -> Modifiers { - Modifiers { - function: true, - ..Default::default() - } - } - - /// Returns [`Modifiers`] with command + shift. - pub fn command_shift() -> Modifiers { - Modifiers { - shift: true, - platform: true, - ..Default::default() - } - } - - /// Returns [`Modifiers`] with command + shift. - pub fn control_shift() -> Modifiers { - Modifiers { - shift: true, - control: true, - ..Default::default() - } - } - - /// Checks if this [`Modifiers`] is a subset of another [`Modifiers`]. - pub fn is_subset_of(&self, other: &Modifiers) -> bool { - (*other & *self) == *self - } -} - -impl std::ops::BitOr for Modifiers { - type Output = Self; - - fn bitor(mut self, other: Self) -> Self::Output { - self |= other; - self - } -} - -impl std::ops::BitOrAssign for Modifiers { - fn bitor_assign(&mut self, other: Self) { - self.control |= other.control; - self.alt |= other.alt; - self.shift |= other.shift; - self.platform |= other.platform; - self.function |= other.function; - } -} - -impl std::ops::BitXor for Modifiers { - type Output = Self; - fn bitxor(mut self, rhs: Self) -> Self::Output { - self ^= rhs; - self - } -} - -impl std::ops::BitXorAssign for Modifiers { - fn bitxor_assign(&mut self, other: Self) { - self.control ^= other.control; - self.alt ^= other.alt; - self.shift ^= other.shift; - self.platform ^= other.platform; - self.function ^= other.function; - } -} - -impl std::ops::BitAnd for Modifiers { - type Output = Self; - fn bitand(mut self, rhs: Self) -> Self::Output { - self &= rhs; - self - } -} - -impl std::ops::BitAndAssign for Modifiers { - fn bitand_assign(&mut self, other: Self) { - self.control &= other.control; - self.alt &= other.alt; - self.shift &= other.shift; - self.platform &= other.platform; - self.function &= other.function; - } -} - -/// The state of the capslock key at some point in time -#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)] -pub struct Capslock { - /// The capslock key is on - #[serde(default)] - pub on: bool, -} - -impl AsKeystroke for Keystroke { - fn as_keystroke(&self) -> &Keystroke { - self - } -} - -impl AsKeystroke for KeybindingKeystroke { - fn as_keystroke(&self) -> &Keystroke { - &self.inner - } -} - -fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if modifiers.control { - #[cfg(target_os = "macos")] - f.write_char('^')?; - - #[cfg(not(target_os = "macos"))] - write!(f, "ctrl-")?; - } - if modifiers.alt { - #[cfg(target_os = "macos")] - f.write_char('⌥')?; - - #[cfg(not(target_os = "macos"))] - write!(f, "alt-")?; - } - if modifiers.platform { - #[cfg(target_os = "macos")] - f.write_char('⌘')?; - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - f.write_char('❖')?; - - #[cfg(target_os = "windows")] - f.write_char('⊞')?; - } - if modifiers.shift { - #[cfg(target_os = "macos")] - f.write_char('⇧')?; - - #[cfg(not(target_os = "macos"))] - write!(f, "shift-")?; - } - Ok(()) -} - -fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let key = match key { - #[cfg(target_os = "macos")] - "backspace" => '⌫', - #[cfg(target_os = "macos")] - "up" => '↑', - #[cfg(target_os = "macos")] - "down" => '↓', - #[cfg(target_os = "macos")] - "left" => '←', - #[cfg(target_os = "macos")] - "right" => '→', - #[cfg(target_os = "macos")] - "tab" => '⇥', - #[cfg(target_os = "macos")] - "escape" => '⎋', - #[cfg(target_os = "macos")] - "shift" => '⇧', - #[cfg(target_os = "macos")] - "control" => '⌃', - #[cfg(target_os = "macos")] - "alt" => '⌥', - #[cfg(target_os = "macos")] - "platform" => '⌘', - - key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(), - key => return f.write_str(key), - }; - f.write_char(key) -} - -#[inline] -fn unparse(modifiers: &Modifiers, key: &str) -> String { - let mut result = String::new(); - if modifiers.function { - result.push_str("fn-"); - } - if modifiers.control { - result.push_str("ctrl-"); - } - if modifiers.alt { - result.push_str("alt-"); - } - if modifiers.platform { - #[cfg(target_os = "macos")] - result.push_str("cmd-"); - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - result.push_str("super-"); - - #[cfg(target_os = "windows")] - result.push_str("win-"); - } - if modifiers.shift { - result.push_str("shift-"); - } - result.push_str(&key); - result -} diff --git a/crates/gpui_pre/src/platform/layer_shell.rs b/crates/gpui_pre/src/platform/layer_shell.rs deleted file mode 100644 index 8be1b5f..0000000 --- a/crates/gpui_pre/src/platform/layer_shell.rs +++ /dev/null @@ -1,83 +0,0 @@ -use bitflags::bitflags; -use thiserror::Error; - -use crate::Pixels; - -/// The layer the surface is rendered on. Multiple surfaces can share a layer, and ordering within -/// a single layer is undefined. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -pub enum Layer { - /// The background layer, typically used for wallpapers. - Background, - - /// The bottom layer. - Bottom, - - /// The top layer, typically used for fullscreen windows. - Top, - - /// The overlay layer, used for surfaces that should always be on top. - #[default] - Overlay, -} - -bitflags! { - /// Screen anchor point for layer_shell surfaces. These can be used in any combination, e.g. - /// specifying `Anchor::LEFT | Anchor::RIGHT` will stretch the surface across the width of the - /// screen. - #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] - pub struct Anchor: u32 { - /// Anchor to the top edge of the screen. - const TOP = 1; - /// Anchor to the bottom edge of the screen. - const BOTTOM = 2; - /// Anchor to the left edge of the screen. - const LEFT = 4; - /// Anchor to the right edge of the screen. - const RIGHT = 8; - } -} - -/// Keyboard interactivity mode for the layer_shell surfaces. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -pub enum KeyboardInteractivity { - /// No keyboard inputs will be delivered to the surface and it won't be able to receive - /// keyboard focus. - None, - - /// The surface will receive exclusive keyboard focus as long as it is above the shell surface - /// layer, and no other layer_shell surfaces are above it. - Exclusive, - - /// The surface can be focused similarly to a normal window. - #[default] - OnDemand, -} - -/// Options for creating a layer_shell window. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct LayerShellOptions { - /// The namespace for the surface, mostly used by compositors to apply rules, can not be - /// changed after the surface is created. - pub namespace: String, - /// The layer the surface is rendered on. - pub layer: Layer, - /// The anchor point of the surface. - pub anchor: Anchor, - /// Requests that the compositor avoids occluding an area with other surfaces. - pub exclusive_zone: Option, - /// The anchor point of the exclusive zone, will be determined using the anchor if left - /// unspecified. - pub exclusive_edge: Option, - /// Margins between the surface and its anchor point(s). - /// Specified in CSS order: top, right, bottom, left. - pub margin: Option<(Pixels, Pixels, Pixels, Pixels)>, - /// How keyboard events should be delivered to the surface. - pub keyboard_interactivity: KeyboardInteractivity, -} - -/// An error indicating that an action failed because the compositor doesn't support the required -/// layer_shell protocol. -#[derive(Debug, Error)] -#[error("Compositor doesn't support zwlr_layer_shell_v1")] -pub struct LayerShellNotSupportedError; diff --git a/crates/gpui_pre/src/platform/popup.rs b/crates/gpui_pre/src/platform/popup.rs deleted file mode 100644 index a1f8d7e..0000000 --- a/crates/gpui_pre/src/platform/popup.rs +++ /dev/null @@ -1,134 +0,0 @@ -use bitflags::bitflags; -use thiserror::Error; - -use crate::{AnyWindowHandle, Bounds, Pixels, Point}; - -/// Options for a parent-anchored popup window such as a menu, dropdown, context menu or tooltip. -/// -/// A popup is placed relative to an anchor rectangle on its parent window rather than at an -/// absolute screen position. The platform resolves the final position, so this works both on -/// systems where the compositor owns window placement (Wayland) and on platforms with absolute -/// coordinates. -/// -/// The popup's size comes from [`WindowOptions::window_bounds`](crate::WindowOptions), whose -/// origin is ignored. All coordinates are in logical pixels. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct PopupOptions { - /// The window the popup is anchored to. - pub parent: AnyWindowHandle, - - /// The rectangle the popup is positioned relative to, in the parent window's coordinate - /// space (the same space element bounds are in). For example, a dropdown menu uses the - /// bounds of the button that opened it. - pub anchor_rect: Bounds, - - /// Which point of [`Self::anchor_rect`] the popup is anchored to. - pub anchor: PopupAnchor, - - /// The direction in which the popup extends away from the anchor point. A dropdown that - /// drops below its button anchors to [`PopupAnchor::BottomLeft`] with a gravity of - /// [`PopupGravity::BottomRight`] so it grows down and to the right. - pub gravity: PopupGravity, - - /// How the platform may adjust the popup if the requested placement would put it off-screen. - pub constraint_adjustment: PopupConstraintAdjustment, - - /// An additional offset applied to the popup after anchoring. - pub offset: Point, - - /// Whether the popup should take an explicit input grab. - /// - /// Grabbing popups behave like menus: they take keyboard focus and are dismissed when the - /// user clicks outside of them or presses a dismissing key. Use it for menus and comboboxes, - /// not for tooltips or other passive popups. - /// - /// A grab must be requested while the triggering input is still active, in practice the - /// press of the mouse button that opens the popup. Open grabbing popups from a mouse-down - /// handler rather than a click handler, otherwise the grab is refused. - /// - /// Automatic dismissal only covers input aimed at other applications. A click elsewhere in - /// your own application still reaches it as usual, so closing the popup in that case is up - /// to you. Nested grabbing popups must be closed in the reverse order they were opened. - pub grab: bool, -} - -/// The point of the anchor rectangle that a popup is anchored to. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -pub enum PopupAnchor { - /// Anchor to the center of the anchor rectangle. - #[default] - Center, - /// Anchor to the center of the top edge. - Top, - /// Anchor to the center of the bottom edge. - Bottom, - /// Anchor to the center of the left edge. - Left, - /// Anchor to the center of the right edge. - Right, - /// Anchor to the top-left corner. - TopLeft, - /// Anchor to the bottom-left corner. - BottomLeft, - /// Anchor to the top-right corner. - TopRight, - /// Anchor to the bottom-right corner. - BottomRight, -} - -/// The direction in which a popup extends away from its anchor point. -/// -/// For instance, a gravity of [`PopupGravity::BottomRight`] places the popup below and to the -/// right of the anchor point. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -pub enum PopupGravity { - /// The popup is centered over the anchor point. - #[default] - Center, - /// The popup extends upwards from the anchor point. - Top, - /// The popup extends downwards from the anchor point. - Bottom, - /// The popup extends to the left of the anchor point. - Left, - /// The popup extends to the right of the anchor point. - Right, - /// The popup extends up and to the left of the anchor point. - TopLeft, - /// The popup extends down and to the left of the anchor point. - BottomLeft, - /// The popup extends up and to the right of the anchor point. - TopRight, - /// The popup extends down and to the right of the anchor point. - BottomRight, -} - -bitflags! { - /// How a popup may be adjusted by the platform if the requested placement would put it - /// off-screen. If no flags are set, the popup is placed exactly as requested and may be - /// clipped. - #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] - pub struct PopupConstraintAdjustment: u32 { - /// The popup may be slid horizontally to stay on-screen. - const SLIDE_X = 1; - /// The popup may be slid vertically to stay on-screen. - const SLIDE_Y = 2; - /// The popup's anchor and gravity may be flipped horizontally to stay on-screen. - const FLIP_X = 4; - /// The popup's anchor and gravity may be flipped vertically to stay on-screen. - const FLIP_Y = 8; - /// The popup may be shrunk horizontally to stay on-screen. - const RESIZE_X = 16; - /// The popup may be shrunk vertically to stay on-screen. - const RESIZE_Y = 32; - } -} - -/// Returned when the current platform has no native popup implementation yet. -/// -/// Native popups are separate from gpui's in-window popovers, which are drawn as elements inside -/// an existing window. A caller that wants a popup on every platform should treat this error as -/// a cue to fall back to that in-window rendering. -#[derive(Debug, Error)] -#[error("popups are not supported on this platform")] -pub struct PopupNotSupportedError; diff --git a/crates/gpui_pre/src/platform/scap_screen_capture.rs b/crates/gpui_pre/src/platform/scap_screen_capture.rs deleted file mode 100644 index 797e19b..0000000 --- a/crates/gpui_pre/src/platform/scap_screen_capture.rs +++ /dev/null @@ -1,325 +0,0 @@ -//! Screen capture for Linux and Windows -use crate::{ - DevicePixels, ForegroundExecutor, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, - Size, SourceMetadata, size, -}; -use anyhow::{Context as _, Result, anyhow}; -use futures::channel::oneshot; -use scap::Target; -use std::rc::Rc; -use std::sync::Arc; -use std::sync::atomic::{self, AtomicBool}; - -/// Populates the receiver with the screens that can be captured. -/// -/// `scap_default_target_source` should be used instead on Wayland, since `scap_screen_sources` -/// won't return any results. -#[allow(dead_code)] -pub fn scap_screen_sources( - foreground_executor: &ForegroundExecutor, -) -> oneshot::Receiver>>> { - let (sources_tx, sources_rx) = oneshot::channel(); - get_screen_targets(sources_tx); - to_dyn_screen_capture_sources(sources_rx, foreground_executor) -} - -/// Starts screen capture for the default target, and populates the receiver with a single source -/// for it. The first frame of the screen capture is used to determine the size of the stream. -/// -/// On Wayland (Linux), prompts the user to select a target, and populates the receiver with a -/// single screen capture source for their selection. -#[allow(dead_code)] -pub(crate) fn start_scap_default_target_source( - foreground_executor: &ForegroundExecutor, -) -> oneshot::Receiver>>> { - let (sources_tx, sources_rx) = oneshot::channel(); - start_default_target_screen_capture(sources_tx); - to_dyn_screen_capture_sources(sources_rx, foreground_executor) -} - -struct ScapCaptureSource { - target: scap::Display, - size: Size, -} - -/// Populates the sender with the screens available for capture. -fn get_screen_targets(sources_tx: oneshot::Sender>>) { - // Due to use of blocking APIs, a new thread is used. - std::thread::spawn(|| { - let targets = match scap::get_all_targets() { - Ok(targets) => targets, - Err(err) => { - sources_tx.send(Err(err)).ok(); - return; - } - }; - let sources = targets - .into_iter() - .filter_map(|target| match target { - scap::Target::Display(display) => { - let size = Size { - width: DevicePixels(display.width as i32), - height: DevicePixels(display.height as i32), - }; - Some(ScapCaptureSource { - target: display, - size, - }) - } - scap::Target::Window(_) => None, - }) - .collect::>(); - sources_tx.send(Ok(sources)).ok(); - }); -} - -impl ScreenCaptureSource for ScapCaptureSource { - fn metadata(&self) -> Result { - Ok(SourceMetadata { - resolution: self.size, - label: Some(self.target.title.clone().into()), - is_main: None, - id: self.target.id as u64, - }) - } - - fn stream( - &self, - foreground_executor: &ForegroundExecutor, - frame_callback: Box, - ) -> oneshot::Receiver>> { - let (stream_tx, stream_rx) = oneshot::channel(); - let target = self.target.clone(); - - // Due to use of blocking APIs, a dedicated thread is used. - std::thread::spawn(move || { - match new_scap_capturer(Some(scap::Target::Display(target.clone()))) { - Ok(mut capturer) => { - capturer.start_capture(); - run_capture(capturer, target.clone(), frame_callback, stream_tx); - } - Err(e) => { - stream_tx.send(Err(e)).ok(); - } - } - }); - - to_dyn_screen_capture_stream(stream_rx, foreground_executor) - } -} - -struct ScapDefaultTargetCaptureSource { - // Sender populated by single call to `ScreenCaptureSource::stream`. - stream_call_tx: std::sync::mpsc::SyncSender<( - // Provides the result of `ScreenCaptureSource::stream`. - oneshot::Sender>, - // Callback for frames. - Box, - )>, - target: scap::Display, - size: Size, -} - -/// Starts screen capture on the default capture target, and populates the sender with the source. -fn start_default_target_screen_capture( - sources_tx: oneshot::Sender>>, -) { - // Due to use of blocking APIs, a dedicated thread is used. - std::thread::spawn(|| { - let start_result = gpui_util::maybe!({ - let mut capturer = new_scap_capturer(None)?; - capturer.start_capture(); - let first_frame = capturer - .get_next_frame() - .context("Failed to get first frame of screenshare to get the size.")?; - let size = frame_size(&first_frame); - let target = capturer - .target() - .context("Unable to determine the target display.")?; - let target = target.clone(); - Ok((capturer, size, target)) - }); - - match start_result { - Ok((capturer, size, Target::Display(display))) => { - let (stream_call_tx, stream_rx) = std::sync::mpsc::sync_channel(1); - sources_tx - .send(Ok(vec![ScapDefaultTargetCaptureSource { - stream_call_tx, - size, - target: display.clone(), - }])) - .ok(); - let Ok((stream_tx, frame_callback)) = stream_rx.recv() else { - return; - }; - run_capture(capturer, display, frame_callback, stream_tx); - } - Err(e) => { - sources_tx.send(Err(e)).ok(); - } - _ => { - sources_tx - .send(Err(anyhow!("The screen capture source is not a display"))) - .ok(); - } - } - }); -} - -impl ScreenCaptureSource for ScapDefaultTargetCaptureSource { - fn metadata(&self) -> Result { - Ok(SourceMetadata { - resolution: self.size, - label: None, - is_main: None, - id: self.target.id as u64, - }) - } - - fn stream( - &self, - foreground_executor: &ForegroundExecutor, - frame_callback: Box, - ) -> oneshot::Receiver>> { - let (tx, rx) = oneshot::channel(); - match self.stream_call_tx.try_send((tx, frame_callback)) { - Ok(()) => {} - Err(std::sync::mpsc::TrySendError::Full((tx, _))) - | Err(std::sync::mpsc::TrySendError::Disconnected((tx, _))) => { - // Note: support could be added for being called again after end of prior stream. - tx.send(Err(anyhow!( - "Can't call ScapDefaultTargetCaptureSource::stream multiple times." - ))) - .ok(); - } - } - to_dyn_screen_capture_stream(rx, foreground_executor) - } -} - -fn new_scap_capturer(target: Option) -> Result { - scap::capturer::Capturer::build(scap::capturer::Options { - fps: 60, - show_cursor: true, - show_highlight: true, - // Note that the actual frame output type may differ. - output_type: scap::frame::FrameType::YUVFrame, - output_resolution: scap::capturer::Resolution::Captured, - crop_area: None, - target, - excluded_targets: None, - }) -} - -fn run_capture( - mut capturer: scap::capturer::Capturer, - display: scap::Display, - frame_callback: Box, - stream_tx: oneshot::Sender>, -) { - let cancel_stream = Arc::new(AtomicBool::new(false)); - let size = Size { - width: DevicePixels(display.width as i32), - height: DevicePixels(display.height as i32), - }; - let stream_send_result = stream_tx.send(Ok(ScapStream { - cancel_stream: cancel_stream.clone(), - display, - size, - })); - if stream_send_result.is_err() { - return; - } - while !cancel_stream.load(std::sync::atomic::Ordering::SeqCst) { - match capturer.get_next_frame() { - Ok(frame) => frame_callback(ScreenCaptureFrame(frame)), - Err(err) => { - log::error!("Halting screen capture due to error: {err}"); - break; - } - } - } - capturer.stop_capture(); -} - -struct ScapStream { - cancel_stream: Arc, - display: scap::Display, - size: Size, -} - -impl ScreenCaptureStream for ScapStream { - fn metadata(&self) -> Result { - Ok(SourceMetadata { - resolution: self.size, - label: Some(self.display.title.clone().into()), - is_main: None, - id: self.display.id as u64, - }) - } -} - -impl Drop for ScapStream { - fn drop(&mut self) { - self.cancel_stream.store(true, atomic::Ordering::SeqCst); - } -} - -fn frame_size(frame: &scap::frame::Frame) -> Size { - let (width, height) = match frame { - scap::frame::Frame::YUVFrame(frame) => (frame.width, frame.height), - scap::frame::Frame::RGB(frame) => (frame.width, frame.height), - scap::frame::Frame::RGBx(frame) => (frame.width, frame.height), - scap::frame::Frame::XBGR(frame) => (frame.width, frame.height), - scap::frame::Frame::BGRx(frame) => (frame.width, frame.height), - scap::frame::Frame::BGR0(frame) => (frame.width, frame.height), - scap::frame::Frame::BGRA(frame) => (frame.width, frame.height), - }; - size(DevicePixels(width), DevicePixels(height)) -} - -/// This is used by `get_screen_targets` and `start_default_target_screen_capture` to turn their -/// results into `Rc`. They need to `Send` their capture source, and so -/// the capture source structs are used as `Rc` is not `Send`. -fn to_dyn_screen_capture_sources( - sources_rx: oneshot::Receiver>>, - foreground_executor: &ForegroundExecutor, -) -> oneshot::Receiver>>> { - let (dyn_sources_tx, dyn_sources_rx) = oneshot::channel(); - foreground_executor - .spawn(async move { - match sources_rx.await { - Ok(Ok(results)) => dyn_sources_tx - .send(Ok(results - .into_iter() - .map(|source| Rc::new(source) as Rc) - .collect::>())) - .ok(), - Ok(Err(err)) => dyn_sources_tx.send(Err(err)).ok(), - Err(oneshot::Canceled) => None, - } - }) - .detach(); - dyn_sources_rx -} - -/// Same motivation as `to_dyn_screen_capture_sources` above. -fn to_dyn_screen_capture_stream( - sources_rx: oneshot::Receiver>, - foreground_executor: &ForegroundExecutor, -) -> oneshot::Receiver>> { - let (dyn_sources_tx, dyn_sources_rx) = oneshot::channel(); - foreground_executor - .spawn(async move { - match sources_rx.await { - Ok(Ok(stream)) => dyn_sources_tx - .send(Ok(Box::new(stream) as Box)) - .ok(), - Ok(Err(err)) => dyn_sources_tx.send(Err(err)).ok(), - Err(oneshot::Canceled) => None, - } - }) - .detach(); - dyn_sources_rx -} diff --git a/crates/gpui_pre/src/platform/test.rs b/crates/gpui_pre/src/platform/test.rs deleted file mode 100644 index a327d8f..0000000 --- a/crates/gpui_pre/src/platform/test.rs +++ /dev/null @@ -1,12 +0,0 @@ -mod dispatcher; -mod display; -mod platform; -mod window; - -pub use dispatcher::*; -pub(crate) use display::*; -pub(crate) use platform::*; -pub(crate) use window::*; - -#[cfg(any(test, feature = "test-support"))] -pub use platform::{TestScreenCaptureSource, TestScreenCaptureStream}; diff --git a/crates/gpui_pre/src/platform/test/dispatcher.rs b/crates/gpui_pre/src/platform/test/dispatcher.rs deleted file mode 100644 index c48e491..0000000 --- a/crates/gpui_pre/src/platform/test/dispatcher.rs +++ /dev/null @@ -1,148 +0,0 @@ -use crate::{PlatformDispatcher, Priority, RunnableVariant}; -use scheduler::Instant; -use scheduler::{Clock, Scheduler, SessionId, TestScheduler, TestSchedulerConfig, Yield}; -use std::{ - sync::{ - Arc, - atomic::{AtomicUsize, Ordering}, - }, - time::Duration, -}; - -/// TestDispatcher provides deterministic async execution for tests. -/// -/// This implementation delegates task scheduling to the scheduler crate's `TestScheduler`. -/// Access the scheduler directly via `scheduler()` for clock, rng, and parking control. -#[doc(hidden)] -pub struct TestDispatcher { - session_id: SessionId, - scheduler: Arc, - num_cpus_override: Arc, -} - -impl TestDispatcher { - pub fn new(seed: u64) -> Self { - let scheduler = Arc::new(TestScheduler::new(TestSchedulerConfig { - seed, - randomize_order: true, - allow_parking: false, - capture_pending_traces: std::env::var("PENDING_TRACES") - .map_or(false, |var| var == "1" || var == "true"), - timeout_ticks: 0..=1000, - })); - Self::from_scheduler(scheduler) - } - - pub fn from_scheduler(scheduler: Arc) -> Self { - TestDispatcher { - session_id: scheduler.allocate_session_id(), - scheduler, - num_cpus_override: Arc::new(AtomicUsize::new(0)), - } - } - - pub fn scheduler(&self) -> &Arc { - &self.scheduler - } - - pub fn session_id(&self) -> SessionId { - self.session_id - } - - pub fn drain_tasks(&self) { - self.scheduler.drain_tasks(); - } - - pub fn advance_clock(&self, by: Duration) { - self.scheduler.advance_clock(by); - } - - pub fn advance_clock_to_next_timer(&self) -> bool { - self.scheduler.advance_clock_to_next_timer() - } - - pub fn simulate_random_delay(&self) -> Yield { - self.scheduler.yield_random() - } - - pub fn tick(&self, background_only: bool) -> bool { - if background_only { - self.scheduler.tick_background_only() - } else { - self.scheduler.tick() - } - } - - pub fn run_until_parked(&self) { - while self.tick(false) {} - } - - pub fn allow_parking(&self) { - self.scheduler.allow_parking(); - } - - pub fn forbid_parking(&self) { - self.scheduler.forbid_parking(); - } - - /// Override the value returned by `BackgroundExecutor::num_cpus()` in tests. - /// A value of 0 means no override (the default of 4 is used). - pub fn set_num_cpus(&self, count: usize) { - self.num_cpus_override.store(count, Ordering::SeqCst); - } - - /// Returns the overridden CPU count, or `None` if no override is set. - pub fn num_cpus_override(&self) -> Option { - match self.num_cpus_override.load(Ordering::SeqCst) { - 0 => None, - n => Some(n), - } - } -} - -impl Clone for TestDispatcher { - fn clone(&self) -> Self { - let session_id = self.scheduler.allocate_session_id(); - Self { - session_id, - scheduler: self.scheduler.clone(), - num_cpus_override: self.num_cpus_override.clone(), - } - } -} - -impl PlatformDispatcher for TestDispatcher { - fn is_main_thread(&self) -> bool { - self.scheduler.is_main_thread() - } - - fn now(&self) -> Instant { - self.scheduler.clock().now() - } - - fn dispatch(&self, runnable: RunnableVariant, priority: Priority) { - self.scheduler - .schedule_background_with_priority(runnable, priority); - } - - fn dispatch_on_main_thread(&self, runnable: RunnableVariant, _priority: Priority) { - self.scheduler.schedule_local(self.session_id, runnable); - } - - fn dispatch_after(&self, _duration: Duration, _runnable: RunnableVariant) { - panic!( - "dispatch_after should not be called in tests. \ - Use BackgroundExecutor::timer() which uses the scheduler's native timer." - ); - } - - fn as_test(&self) -> Option<&TestDispatcher> { - Some(self) - } - - fn spawn_realtime(&self, f: Box) { - std::thread::spawn(move || { - f(); - }); - } -} diff --git a/crates/gpui_pre/src/platform/test/display.rs b/crates/gpui_pre/src/platform/test/display.rs deleted file mode 100644 index c4adb01..0000000 --- a/crates/gpui_pre/src/platform/test/display.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::{Bounds, DisplayId, Pixels, PlatformDisplay, Point, px}; -use anyhow::{Ok, Result}; - -#[derive(Debug)] -pub(crate) struct TestDisplay { - id: DisplayId, - uuid: uuid::Uuid, - bounds: Bounds, -} - -impl TestDisplay { - pub fn new() -> Self { - TestDisplay { - id: DisplayId(1), - uuid: uuid::Uuid::new_v4(), - bounds: Bounds::from_corners(Point::default(), Point::new(px(1920.), px(1080.))), - } - } -} - -impl PlatformDisplay for TestDisplay { - fn id(&self) -> crate::DisplayId { - self.id - } - - fn uuid(&self) -> Result { - Ok(self.uuid) - } - - fn bounds(&self) -> crate::Bounds { - self.bounds - } -} diff --git a/crates/gpui_pre/src/platform/test/platform.rs b/crates/gpui_pre/src/platform/test/platform.rs deleted file mode 100644 index 5e82fbe..0000000 --- a/crates/gpui_pre/src/platform/test/platform.rs +++ /dev/null @@ -1,676 +0,0 @@ -#[cfg(any(test, feature = "test-support"))] -use crate::NoopTextSystem; -#[cfg(any(test, feature = "test-support"))] -use crate::PathPromptOptions; -use crate::{ - AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels, - DummyKeyboardMapper, ForegroundExecutor, Keymap, Platform, PlatformDisplay, - PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, - PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SharedString, - SourceMetadata, SystemNotification, SystemNotificationResponse, Task, TestDisplay, TestWindow, - ThermalState, WindowAppearance, WindowParams, size, -}; -use anyhow::Result; -#[cfg(any(test, feature = "test-support"))] -use collections::VecDeque; -use futures::channel::oneshot; -use parking_lot::Mutex; -use std::{ - cell::RefCell, - path::{Path, PathBuf}, - rc::{Rc, Weak}, - sync::Arc, -}; - -/// TestPlatform implements the Platform trait for use in tests. -pub(crate) struct TestPlatform { - background_executor: BackgroundExecutor, - foreground_executor: ForegroundExecutor, - - pub(crate) active_window: RefCell>, - active_display: Rc, - active_cursor: Mutex, - current_clipboard_item: Mutex>, - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - current_primary_item: Mutex>, - #[cfg(target_os = "macos")] - current_find_pasteboard_item: Mutex>, - #[cfg(any(test, feature = "test-support"))] - pub(crate) prompts: RefCell, - screen_capture_sources: RefCell>, - pub opened_url: RefCell>, - pub(crate) system_notifications: RefCell, - pub text_system: Arc, - pub expect_restart: - RefCell, Vec)>>>, - headless_renderer_factory: Option Option>>>, - weak: Weak, -} - -#[derive(Clone)] -/// A fake screen capture source, used for testing. -pub struct TestScreenCaptureSource {} - -/// A fake screen capture stream, used for testing. -pub struct TestScreenCaptureStream {} - -impl ScreenCaptureSource for TestScreenCaptureSource { - fn metadata(&self) -> Result { - Ok(SourceMetadata { - id: 0, - is_main: None, - label: None, - resolution: size(DevicePixels(1), DevicePixels(1)), - }) - } - - fn stream( - &self, - _foreground_executor: &ForegroundExecutor, - _frame_callback: Box, - ) -> oneshot::Receiver>> { - let (mut tx, rx) = oneshot::channel(); - let stream = TestScreenCaptureStream {}; - tx.send(Ok(Box::new(stream) as Box)) - .ok(); - rx - } -} - -impl ScreenCaptureStream for TestScreenCaptureStream { - fn metadata(&self) -> Result { - TestScreenCaptureSource {}.metadata() - } -} - -#[cfg(any(test, feature = "test-support"))] -struct TestPrompt { - msg: String, - detail: Option, - answers: Vec, - tx: oneshot::Sender, -} - -#[derive(Default)] -pub(crate) struct TestSystemNotifications { - pub(crate) app_identity: Option<(SharedString, SharedString)>, - pub(crate) shown: Vec, - pub(crate) delivered: Vec, - pub(crate) dismissed: Vec, - response_callback: Option>, -} - -#[cfg(any(test, feature = "test-support"))] -#[derive(Default)] -pub(crate) struct TestPrompts { - multiple_choice: VecDeque, - new_path: VecDeque<(PathBuf, oneshot::Sender>>)>, - paths: VecDeque<( - PathPromptOptions, - oneshot::Sender>>>, - )>, -} - -impl TestPlatform { - #[cfg(any(test, feature = "test-support"))] - pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Rc { - Self::with_platform( - executor, - foreground_executor, - Arc::new(NoopTextSystem), - None, - ) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn with_text_system( - executor: BackgroundExecutor, - foreground_executor: ForegroundExecutor, - text_system: Arc, - ) -> Rc { - Self::with_platform(executor, foreground_executor, text_system, None) - } - - pub fn with_platform( - executor: BackgroundExecutor, - foreground_executor: ForegroundExecutor, - text_system: Arc, - headless_renderer_factory: Option< - Box Option>>, - >, - ) -> Rc { - Rc::new_cyclic(|weak| TestPlatform { - background_executor: executor, - foreground_executor, - #[cfg(any(test, feature = "test-support"))] - prompts: Default::default(), - screen_capture_sources: Default::default(), - active_cursor: Default::default(), - active_display: Rc::new(TestDisplay::new()), - active_window: Default::default(), - expect_restart: Default::default(), - current_clipboard_item: Mutex::new(None), - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - current_primary_item: Mutex::new(None), - #[cfg(target_os = "macos")] - current_find_pasteboard_item: Mutex::new(None), - weak: weak.clone(), - opened_url: Default::default(), - system_notifications: Default::default(), - text_system, - headless_renderer_factory, - }) - } - - #[cfg(any(test, feature = "test-support"))] - pub(crate) fn simulate_new_path_selection( - &self, - select_path: impl FnOnce(&std::path::Path) -> Option, - ) { - let (path, tx) = self - .prompts - .borrow_mut() - .new_path - .pop_front() - .expect("no pending new path prompt"); - tx.send(Ok(select_path(&path))).ok(); - } - - #[cfg(any(test, feature = "test-support"))] - pub(crate) fn simulate_path_prompt_response( - &self, - select_paths: impl FnOnce(&PathPromptOptions) -> Option>, - ) { - let (options, tx) = self - .prompts - .borrow_mut() - .paths - .pop_front() - .expect("no pending paths prompt"); - let selection = select_paths(&options); - if let Some(paths) = &selection - && !options.multiple - && paths.len() > 1 - { - panic!( - "selected {} paths for a prompt that does not allow multiple selection", - paths.len() - ); - } - tx.send(Ok(selection)).ok(); - } - - #[cfg(any(test, feature = "test-support"))] - pub(crate) fn did_prompt_for_paths(&self) -> bool { - !self.prompts.borrow().paths.is_empty() - } - - #[cfg(any(test, feature = "test-support"))] - #[track_caller] - pub(crate) fn simulate_prompt_answer(&self, response: &str) { - let prompt = self - .prompts - .borrow_mut() - .multiple_choice - .pop_front() - .expect("no pending multiple choice prompt"); - let Some(ix) = prompt.answers.iter().position(|a| a == response) else { - panic!( - "PROMPT: {}\n{:?}\n{:?}\nCannot respond with {}", - prompt.msg, prompt.detail, prompt.answers, response - ) - }; - prompt.tx.send(ix).ok(); - } - - #[cfg(any(test, feature = "test-support"))] - pub(crate) fn has_pending_prompt(&self) -> bool { - !self.prompts.borrow().multiple_choice.is_empty() - } - - #[cfg(any(test, feature = "test-support"))] - pub(crate) fn pending_prompt(&self) -> Option<(String, String)> { - let prompts = self.prompts.borrow(); - let prompt = prompts.multiple_choice.front()?; - Some(( - prompt.msg.clone(), - prompt.detail.clone().unwrap_or_default(), - )) - } - - #[cfg(any(test, feature = "test-support"))] - pub(crate) fn set_screen_capture_sources(&self, sources: Vec) { - *self.screen_capture_sources.borrow_mut() = sources; - } - - /// Queues the prompt so a test can later inspect or answer it through - /// [`Self::pending_prompt`] and [`Self::simulate_prompt_answer`]. - #[cfg(any(test, feature = "test-support"))] - pub(crate) fn prompt( - &self, - msg: &str, - detail: Option<&str>, - answers: &[PromptButton], - ) -> oneshot::Receiver { - let (tx, rx) = oneshot::channel(); - let answers: Vec = answers.iter().map(|s| s.label().to_string()).collect(); - self.prompts - .borrow_mut() - .multiple_choice - .push_back(TestPrompt { - msg: msg.to_string(), - detail: detail.map(|s| s.to_string()), - answers, - tx, - }); - rx - } - - /// Benchmarks have no API to answer a prompt, so this doesn't retain it - /// for later inspection; dropping the sender immediately cancels the - /// returned receiver instead of leaving it pending indefinitely. - #[cfg(not(any(test, feature = "test-support")))] - pub(crate) fn prompt( - &self, - _msg: &str, - _detail: Option<&str>, - _answers: &[PromptButton], - ) -> oneshot::Receiver { - oneshot::channel().1 - } - - pub(crate) fn set_active_window(&self, window: Option) { - let executor = self.foreground_executor(); - let previous_window = self.active_window.borrow_mut().take(); - self.active_window.borrow_mut().clone_from(&window); - - executor - .spawn(async move { - if let Some(previous_window) = previous_window { - if let Some(window) = window.as_ref() - && Rc::ptr_eq(&previous_window.0, &window.0) - { - return; - } - previous_window.simulate_active_status_change(false); - } - if let Some(window) = window { - window.simulate_active_status_change(true); - } - }) - .detach(); - } - - #[cfg(any(test, feature = "test-support"))] - pub(crate) fn did_prompt_for_new_path(&self) -> bool { - !self.prompts.borrow().new_path.is_empty() - } - - #[cfg(any(test, feature = "test-support"))] - pub(crate) fn app_identity(&self) -> Option<(SharedString, SharedString)> { - self.system_notifications.borrow().app_identity.clone() - } - - #[cfg(any(test, feature = "test-support"))] - pub(crate) fn shown_system_notifications(&self) -> Vec { - self.system_notifications.borrow().shown.clone() - } - - #[cfg(any(test, feature = "test-support"))] - pub(crate) fn delivered_system_notifications(&self) -> Vec { - self.system_notifications.borrow().delivered.clone() - } - - #[cfg(any(test, feature = "test-support"))] - pub(crate) fn dismissed_system_notifications(&self) -> Vec { - self.system_notifications.borrow().dismissed.clone() - } - - #[cfg(any(test, feature = "test-support"))] - pub(crate) fn simulate_system_notification_response( - &self, - response: SystemNotificationResponse, - ) { - let callback = self - .system_notifications - .borrow_mut() - .response_callback - .take(); - if let Some(mut callback) = callback { - callback(response); - self.system_notifications - .borrow_mut() - .response_callback - .get_or_insert(callback); - } - } -} - -impl Platform for TestPlatform { - fn background_executor(&self) -> BackgroundExecutor { - self.background_executor.clone() - } - - fn foreground_executor(&self) -> ForegroundExecutor { - self.foreground_executor.clone() - } - - fn text_system(&self) -> Arc { - self.text_system.clone() - } - - fn keyboard_layout(&self) -> Box { - Box::new(TestKeyboardLayout) - } - - fn keyboard_mapper(&self) -> Rc { - Rc::new(DummyKeyboardMapper) - } - - fn on_keyboard_layout_change(&self, _: Box) {} - - fn on_thermal_state_change(&self, _: Box) {} - - fn thermal_state(&self) -> ThermalState { - ThermalState::Nominal - } - - fn run(&self, _on_finish_launching: Box) { - unimplemented!() - } - - fn quit(&self) {} - - fn restart(&self, path: Option, arguments: Vec) { - if let Some(tx) = self.expect_restart.take() { - tx.send((path, arguments)).unwrap(); - } - } - - fn activate(&self, _ignoring_other_apps: bool) { - // - } - - fn hide(&self) { - unimplemented!() - } - - fn hide_other_apps(&self) { - unimplemented!() - } - - fn unhide_other_apps(&self) { - unimplemented!() - } - - fn displays(&self) -> Vec> { - vec![self.active_display.clone()] - } - - fn primary_display(&self) -> Option> { - Some(self.active_display.clone()) - } - - fn is_screen_capture_supported(&self) -> bool { - true - } - - fn screen_capture_sources( - &self, - ) -> oneshot::Receiver>>> { - let (mut tx, rx) = oneshot::channel(); - tx.send(Ok(self - .screen_capture_sources - .borrow() - .iter() - .map(|source| Rc::new(source.clone()) as Rc) - .collect())) - .ok(); - rx - } - - fn active_window(&self) -> Option { - self.active_window - .borrow() - .as_ref() - .map(|window| window.0.lock().handle) - } - - fn open_window( - &self, - handle: AnyWindowHandle, - params: WindowParams, - ) -> anyhow::Result> { - let renderer = self.headless_renderer_factory.as_ref().and_then(|f| f()); - let window = TestWindow::new( - handle, - params, - self.weak.clone(), - self.active_display.clone(), - renderer, - ); - Ok(Box::new(window)) - } - - fn window_appearance(&self) -> WindowAppearance { - WindowAppearance::Light - } - - fn open_url(&self, url: &str) { - *self.opened_url.borrow_mut() = Some(url.to_string()) - } - - fn on_open_urls(&self, _callback: Box)>) { - unimplemented!() - } - - /// Queues the prompt so a test can later answer it through - /// [`Self::simulate_path_prompt_response`]. - #[cfg(any(test, feature = "test-support"))] - fn prompt_for_paths( - &self, - options: crate::PathPromptOptions, - ) -> oneshot::Receiver>>> { - let (tx, rx) = oneshot::channel(); - self.prompts.borrow_mut().paths.push_back((options, tx)); - rx - } - - /// Benchmarks have no API to answer a path prompt, so this doesn't - /// retain it for later inspection; dropping the sender immediately - /// cancels the returned receiver instead of leaving it pending - /// indefinitely. - #[cfg(not(any(test, feature = "test-support")))] - fn prompt_for_paths( - &self, - _options: crate::PathPromptOptions, - ) -> oneshot::Receiver>>> { - oneshot::channel().1 - } - - /// Queues the prompt so a test can later answer it through - /// [`Self::simulate_new_path_selection`]. - #[cfg(any(test, feature = "test-support"))] - fn prompt_for_new_path( - &self, - directory: &std::path::Path, - _suggested_name: Option<&str>, - ) -> oneshot::Receiver>> { - let (tx, rx) = oneshot::channel(); - self.prompts - .borrow_mut() - .new_path - .push_back((directory.to_path_buf(), tx)); - rx - } - - /// Benchmarks have no API to answer a new-path prompt, so this doesn't - /// retain it for later inspection; dropping the sender immediately - /// cancels the returned receiver instead of leaving it pending - /// indefinitely. - #[cfg(not(any(test, feature = "test-support")))] - fn prompt_for_new_path( - &self, - _directory: &std::path::Path, - _suggested_name: Option<&str>, - ) -> oneshot::Receiver>> { - oneshot::channel().1 - } - - fn can_select_mixed_files_and_dirs(&self) -> bool { - true - } - - fn reveal_path(&self, _path: &std::path::Path) { - unimplemented!() - } - - fn on_quit(&self, _callback: Box bool>) {} - - fn on_reopen(&self, _callback: Box) { - unimplemented!() - } - - fn on_system_wake(&self, _callback: Box) {} - - fn set_app_identity(&self, identifier: &str, name: &str) { - self.system_notifications.borrow_mut().app_identity = - Some((identifier.to_string().into(), name.to_string().into())); - } - - fn show_system_notification(&self, notification: SystemNotification) { - let mut system_notifications = self.system_notifications.borrow_mut(); - if system_notifications.app_identity.is_none() { - return; - } - - let delivered = system_notifications - .delivered - .iter_mut() - .find(|delivered| delivered.tag == notification.tag); - if let Some(delivered) = delivered { - *delivered = notification.clone(); - } else { - system_notifications.delivered.push(notification.clone()); - } - system_notifications.shown.push(notification); - } - - fn dismiss_system_notification(&self, tag: &str) { - let mut system_notifications = self.system_notifications.borrow_mut(); - system_notifications - .delivered - .retain(|notification| notification.tag != tag); - system_notifications - .dismissed - .push(SharedString::from(tag.to_string())); - } - - fn on_system_notification_response( - &self, - callback: Box, - ) { - self.system_notifications.borrow_mut().response_callback = Some(callback); - } - - fn set_menus(&self, _menus: Vec, _keymap: &Keymap) {} - fn set_dock_menu(&self, _menu: Vec, _keymap: &Keymap) {} - - fn add_recent_document(&self, _paths: &Path) {} - - fn on_app_menu_action(&self, _callback: Box) {} - - fn on_will_open_app_menu(&self, _callback: Box) {} - - fn on_validate_app_menu_command(&self, _callback: Box bool>) {} - - fn app_path(&self) -> Result { - unimplemented!() - } - - fn path_for_auxiliary_executable(&self, _name: &str) -> Result { - unimplemented!() - } - - fn set_cursor_style(&self, style: crate::CursorStyle) { - *self.active_cursor.lock() = style; - } - - fn hide_cursor_until_mouse_moves(&self) {} - - fn is_cursor_visible(&self) -> bool { - true - } - - fn should_auto_hide_scrollbars(&self) -> bool { - false - } - - fn read_from_clipboard(&self) -> Option { - self.current_clipboard_item.lock().clone() - } - - fn write_to_clipboard(&self, item: ClipboardItem) { - *self.current_clipboard_item.lock() = Some(item); - } - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - fn read_from_primary(&self) -> Option { - self.current_primary_item.lock().clone() - } - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - fn write_to_primary(&self, item: ClipboardItem) { - *self.current_primary_item.lock() = Some(item); - } - - #[cfg(target_os = "macos")] - fn read_from_find_pasteboard(&self) -> Option { - self.current_find_pasteboard_item.lock().clone() - } - - #[cfg(target_os = "macos")] - fn write_to_find_pasteboard(&self, item: ClipboardItem) { - *self.current_find_pasteboard_item.lock() = Some(item); - } - - fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task> { - Task::ready(Ok(())) - } - - fn read_credentials(&self, _url: &str) -> Task)>>> { - Task::ready(Ok(None)) - } - - fn delete_credentials(&self, _url: &str) -> Task> { - Task::ready(Ok(())) - } - - fn register_url_scheme(&self, _: &str) -> Task> { - unimplemented!() - } - - fn open_with_system(&self, _path: &Path) { - unimplemented!() - } -} - -impl TestScreenCaptureSource { - /// Create a fake screen capture source, for testing. - #[cfg(any(test, feature = "test-support"))] - pub fn new() -> Self { - Self {} - } -} - -struct TestKeyboardLayout; - -impl PlatformKeyboardLayout for TestKeyboardLayout { - fn id(&self) -> &str { - "zed.keyboard.example" - } - - fn name(&self) -> &str { - "zed.keyboard.example" - } -} diff --git a/crates/gpui_pre/src/platform/test/window.rs b/crates/gpui_pre/src/platform/test/window.rs deleted file mode 100644 index 38926b6..0000000 --- a/crates/gpui_pre/src/platform/test/window.rs +++ /dev/null @@ -1,550 +0,0 @@ -use crate::{ - AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, DevicePixels, - DispatchEventResult, GpuSpecs, Pixels, PlatformAtlas, PlatformDisplay, - PlatformHeadlessRenderer, PlatformInput, PlatformInputHandler, PlatformWindow, Point, - PromptButton, RequestFrameOptions, Scene, Size, TestPlatform, TextInputConfiguration, - TextInputStateChange, TileId, WindowAppearance, WindowBackgroundAppearance, WindowBounds, - WindowControlArea, WindowParams, -}; -use collections::HashMap; -use gpui_util::ResultExt as _; -#[cfg(any(test, feature = "test-support"))] -use image::RgbaImage; -use parking_lot::Mutex; -use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; -use std::{ - cell::Cell, - path::PathBuf, - rc::{Rc, Weak}, - sync::{self, Arc}, -}; - -pub(crate) struct TestWindowState { - pub(crate) bounds: Bounds, - pub(crate) handle: AnyWindowHandle, - display: Rc, - pub(crate) title: Option, - pub(crate) edited: bool, - pub(crate) document_path: Option, - platform: Weak, - // TODO: Replace with `Rc` - sprite_atlas: Arc, - renderer: Option>, - pub(crate) should_close_handler: Option bool>>, - hit_test_window_control_callback: Option Option>>, - input_callback: Option DispatchEventResult>>, - active_status_change_callback: Option>, - hover_status_change_callback: Option>, - resize_callback: Option, f32)>>, - moved_callback: Option>, - appearance_change_callback: Option>, - request_frame_callback: Option>, - frame_wake_count: Rc>, - frame_scheduled: bool, - frame_callback_pending: bool, - input_handler: Option, - text_input_configurations: Vec, - text_input_state_changes: Vec, - is_fullscreen: bool, - appearance: WindowAppearance, - external_drag_files: Vec<(PathBuf, bool)>, - start_external_drag_result: bool, -} - -#[derive(Clone)] -pub struct TestWindow(pub(crate) Rc>); - -// Test windows are not backed by a real platform window, so there is no raw -// handle to report; `NotSupported` is `raw_window_handle`'s variant for exactly this. -impl HasWindowHandle for TestWindow { - fn window_handle( - &self, - ) -> Result, raw_window_handle::HandleError> { - Err(raw_window_handle::HandleError::NotSupported) - } -} - -impl HasDisplayHandle for TestWindow { - fn display_handle( - &self, - ) -> Result, raw_window_handle::HandleError> { - Err(raw_window_handle::HandleError::NotSupported) - } -} - -impl TestWindow { - pub(crate) fn new( - handle: AnyWindowHandle, - params: WindowParams, - platform: Weak, - display: Rc, - renderer: Option>, - ) -> Self { - let sprite_atlas: Arc = match &renderer { - Some(r) => r.sprite_atlas(), - None => Arc::new(TestAtlas::new()), - }; - Self(Rc::new(Mutex::new(TestWindowState { - bounds: params.bounds, - display, - platform, - handle, - sprite_atlas, - renderer, - title: Default::default(), - edited: false, - document_path: None, - should_close_handler: None, - hit_test_window_control_callback: None, - input_callback: None, - active_status_change_callback: None, - hover_status_change_callback: None, - resize_callback: None, - moved_callback: None, - appearance_change_callback: None, - request_frame_callback: None, - frame_wake_count: Rc::new(Cell::new(0)), - frame_scheduled: false, - frame_callback_pending: false, - input_handler: None, - text_input_configurations: Vec::new(), - text_input_state_changes: Vec::new(), - is_fullscreen: false, - appearance: WindowAppearance::Light, - external_drag_files: Vec::new(), - start_external_drag_result: false, - }))) - } - pub fn simulate_scheduled_frame(&self) -> bool { - let callback = { - let mut state = self.0.lock(); - if !std::mem::take(&mut state.frame_scheduled) { - return false; - } - state.frame_callback_pending = false; - state.request_frame_callback.take() - }; - let Some(mut callback) = callback else { - self.0.lock().frame_scheduled = true; - return false; - }; - - callback(RequestFrameOptions::default()); - self.0.lock().request_frame_callback = Some(callback); - true - } - - pub fn frame_scheduled(&self) -> bool { - self.0.lock().frame_scheduled - } - - /// Every [`TextInputConfiguration`] forwarded to this window, in order. - pub fn text_input_configurations(&self) -> Vec { - self.0.lock().text_input_configurations.clone() - } - - pub fn text_input_state_changes(&self) -> Vec { - self.0.lock().text_input_state_changes.clone() - } - - pub fn simulate_resize(&mut self, size: Size) { - let scale_factor = self.scale_factor(); - let mut lock = self.0.lock(); - // Always update bounds, even if no callback is registered - lock.bounds.size = size; - let Some(mut callback) = lock.resize_callback.take() else { - return; - }; - drop(lock); - callback(size, scale_factor); - self.0.lock().resize_callback = Some(callback); - } - - pub(crate) fn simulate_active_status_change(&self, active: bool) { - let mut lock = self.0.lock(); - let Some(mut callback) = lock.active_status_change_callback.take() else { - return; - }; - drop(lock); - callback(active); - self.0.lock().active_status_change_callback = Some(callback); - } - - pub fn simulate_appearance_change(&self, appearance: WindowAppearance) { - let mut lock = self.0.lock(); - lock.appearance = appearance; - let Some(mut callback) = lock.appearance_change_callback.take() else { - return; - }; - drop(lock); - callback(); - self.0.lock().appearance_change_callback = Some(callback); - } - - /// Returns how many times this window's frame waker has been invoked. - pub fn frame_wake_count(&self) -> usize { - self.0.lock().frame_wake_count.get() - } - - /// Delivers a frame request to the window, as the platform's frame source - /// would. - pub fn simulate_frame_request(&self, options: RequestFrameOptions) { - let mut lock = self.0.lock(); - let Some(mut callback) = lock.request_frame_callback.take() else { - return; - }; - drop(lock); - callback(options); - self.0.lock().request_frame_callback = Some(callback); - } - - pub fn simulate_input(&mut self, event: PlatformInput) -> bool { - let mut lock = self.0.lock(); - let Some(mut callback) = lock.input_callback.take() else { - return false; - }; - drop(lock); - let result = callback(event); - self.0.lock().input_callback = Some(callback); - !result.propagate - } - - pub fn external_drag_files(&self) -> Vec<(PathBuf, bool)> { - self.0.lock().external_drag_files.clone() - } - - pub fn set_start_external_drag_result(&self, result: bool) { - self.0.lock().start_external_drag_result = result; - } -} - -impl PlatformWindow for TestWindow { - fn bounds(&self) -> Bounds { - self.0.lock().bounds - } - - fn window_bounds(&self) -> WindowBounds { - WindowBounds::Windowed(self.bounds()) - } - - fn is_maximized(&self) -> bool { - false - } - - fn content_size(&self) -> Size { - self.bounds().size - } - - fn resize(&mut self, size: Size) { - let mut lock = self.0.lock(); - lock.bounds.size = size; - } - - fn scale_factor(&self) -> f32 { - 2.0 - } - - fn appearance(&self) -> WindowAppearance { - self.0.lock().appearance - } - - fn display(&self) -> Option> { - Some(self.0.lock().display.clone()) - } - - fn mouse_position(&self) -> Point { - Point::default() - } - - fn modifiers(&self) -> crate::Modifiers { - crate::Modifiers::default() - } - - fn capslock(&self) -> crate::Capslock { - crate::Capslock::default() - } - - fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { - self.0.lock().input_handler = Some(input_handler); - } - - fn take_input_handler(&mut self) -> Option { - self.0.lock().input_handler.take() - } - - fn set_text_input_configuration(&mut self, configuration: TextInputConfiguration) { - self.0.lock().text_input_configurations.push(configuration); - } - - fn text_input_state_changed(&self, change: TextInputStateChange) { - self.0.lock().text_input_state_changes.push(change); - } - - fn prompt( - &self, - _level: crate::PromptLevel, - msg: &str, - detail: Option<&str>, - answers: &[PromptButton], - ) -> Option> { - Some( - self.0 - .lock() - .platform - .upgrade() - .expect("platform dropped") - .prompt(msg, detail, answers), - ) - } - - fn activate(&self) { - self.0 - .lock() - .platform - .upgrade() - .unwrap() - .set_active_window(Some(self.clone())) - } - - fn is_active(&self) -> bool { - false - } - - fn is_hovered(&self) -> bool { - false - } - - fn background_appearance(&self) -> WindowBackgroundAppearance { - WindowBackgroundAppearance::Opaque - } - - fn is_subpixel_rendering_supported(&self) -> bool { - false - } - - fn set_title(&mut self, title: &str) { - self.0.lock().title = Some(title.to_owned()); - } - - fn set_app_id(&mut self, _app_id: &str) {} - - fn set_background_appearance(&self, _background: WindowBackgroundAppearance) {} - - fn set_edited(&mut self, edited: bool) { - self.0.lock().edited = edited; - } - - fn set_document_path(&self, path: Option<&std::path::Path>) { - self.0.lock().document_path = path.map(|p| p.to_path_buf()); - } - - fn show_character_palette(&self) { - unimplemented!() - } - - fn minimize(&self) { - unimplemented!() - } - - fn zoom(&self) { - unimplemented!() - } - - fn toggle_fullscreen(&self) { - let mut lock = self.0.lock(); - lock.is_fullscreen = !lock.is_fullscreen; - } - - fn is_fullscreen(&self) -> bool { - self.0.lock().is_fullscreen - } - - fn frame_waker(&self) -> Option> { - // Recording invocations (rather than delivering a frame) lets tests - // assert the wake protocol without coupling to frame timing; tests - // deliver frames explicitly via `simulate_frame_request`. - let frame_wake_count = self.0.lock().frame_wake_count.clone(); - Some(Rc::new(move || { - frame_wake_count.set(frame_wake_count.get() + 1); - })) - } - - fn on_request_frame(&self, callback: Box) { - self.0.lock().request_frame_callback = Some(callback); - } - - fn schedule_frame(&self) { - let mut state = self.0.lock(); - if !state.frame_callback_pending { - state.frame_scheduled = true; - } - } - - fn on_input(&self, callback: Box DispatchEventResult>) { - self.0.lock().input_callback = Some(callback) - } - - fn on_active_status_change(&self, callback: Box) { - self.0.lock().active_status_change_callback = Some(callback) - } - - fn on_hover_status_change(&self, callback: Box) { - self.0.lock().hover_status_change_callback = Some(callback) - } - - fn on_resize(&self, callback: Box, f32)>) { - self.0.lock().resize_callback = Some(callback) - } - - fn on_moved(&self, callback: Box) { - self.0.lock().moved_callback = Some(callback) - } - - fn on_should_close(&self, callback: Box bool>) { - self.0.lock().should_close_handler = Some(callback); - } - - fn on_close(&self, _callback: Box) {} - - fn on_hit_test_window_control(&self, callback: Box Option>) { - self.0.lock().hit_test_window_control_callback = Some(callback); - } - - fn on_appearance_changed(&self, callback: Box) { - self.0.lock().appearance_change_callback = Some(callback); - } - - fn draw(&self, scene: &Scene) { - let scale_factor = self.scale_factor(); - let mut state = self.0.lock(); - state.frame_callback_pending = true; - state.frame_scheduled = true; - let device_size: Size = state.bounds.size.to_device_pixels(scale_factor); - if let Some(renderer) = &mut state.renderer { - renderer.render_scene(scene, device_size).warn_on_err(); - } - } - - fn sprite_atlas(&self) -> sync::Arc { - self.0.lock().sprite_atlas.clone() - } - - #[cfg(any(test, feature = "test-support"))] - fn render_to_image(&self, scene: &Scene) -> anyhow::Result { - let scale_factor = self.scale_factor(); - let mut state = self.0.lock(); - let size = state.bounds.size; - if let Some(renderer) = &mut state.renderer { - let device_size: Size = size.to_device_pixels(scale_factor); - renderer.render_scene_to_image(scene, device_size) - } else { - anyhow::bail!("render_to_image not available: no HeadlessRenderer configured") - } - } - - fn as_test(&mut self) -> Option<&mut TestWindow> { - Some(self) - } - - #[cfg(target_os = "windows")] - fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND { - unimplemented!() - } - - fn show_window_menu(&self, _position: Point) { - unimplemented!() - } - - fn start_window_move(&self) { - unimplemented!() - } - - fn can_start_external_drag(&self) -> bool { - true - } - - fn start_external_drag(&self, payload: &crate::ExternalDragPayload) -> bool { - let mut state = self.0.lock(); - match payload { - crate::ExternalDragPayload::Files(paths) => { - state.external_drag_files.extend_from_slice(paths.entries()); - } - } - state.start_external_drag_result - } - - fn update_ime_position(&self, _bounds: Bounds) {} - - fn gpu_specs(&self) -> Option { - None - } -} - -pub(crate) struct TestAtlasState { - next_id: u32, - tiles: HashMap, -} - -pub(crate) struct TestAtlas(Mutex); - -impl TestAtlas { - pub fn new() -> Self { - TestAtlas(Mutex::new(TestAtlasState { - next_id: 0, - tiles: HashMap::default(), - })) - } -} - -impl PlatformAtlas for TestAtlas { - fn get_or_insert_with<'a>( - &self, - key: &crate::AtlasKey, - build: &mut dyn FnMut() -> anyhow::Result< - Option<(Size, std::borrow::Cow<'a, [u8]>)>, - >, - ) -> anyhow::Result> { - let mut state = self.0.lock(); - if let Some(&tile) = state.tiles.get(key) { - return Ok(Some(tile)); - } - drop(state); - - let Some((size, _)) = build()? else { - return Ok(None); - }; - - let mut state = self.0.lock(); - state.next_id += 1; - let texture_id = state.next_id; - state.next_id += 1; - let tile_id = state.next_id; - - state.tiles.insert( - key.clone(), - crate::AtlasTile { - texture_id: AtlasTextureId { - index: texture_id, - kind: crate::AtlasTextureKind::Monochrome, - }, - tile_id: TileId(tile_id), - padding: 0, - bounds: crate::Bounds { - origin: Point::default(), - size, - }, - }, - ); - - Ok(Some(state.tiles[key])) - } - - fn remove(&self, key: &AtlasKey) { - let mut state = self.0.lock(); - state.tiles.remove(key); - } - - fn contains(&self, key: &AtlasKey) -> bool { - self.0.lock().tiles.contains_key(key) - } -} diff --git a/crates/gpui_pre/src/platform/threaded_dispatcher.rs b/crates/gpui_pre/src/platform/threaded_dispatcher.rs deleted file mode 100644 index 762464a..0000000 --- a/crates/gpui_pre/src/platform/threaded_dispatcher.rs +++ /dev/null @@ -1,720 +0,0 @@ -use std::{ - collections::BinaryHeap, - sync::Arc, - thread, - time::{Duration, Instant}, -}; - -use parking_lot::{Condvar, Mutex}; - -use crate::{ - PlatformDispatcher, Priority, RunnableVariant, profiler, - queue::{PriorityQueueReceiver, PriorityQueueSender}, -}; - -const MIN_THREADS: usize = 2; - -/// A multithreaded [`PlatformDispatcher`] for tests and benchmarks. -/// -/// Background tasks run in parallel on a pool of worker threads and timers fire -/// in real time on a dedicated timer thread, mirroring the production -/// dispatchers (see `LinuxDispatcher`). Main-thread tasks are queued until the -/// creating thread drains them via [`Self::run_until_idle`], since there is no -/// platform run loop pumping them. -/// -/// Unlike [`TestDispatcher`](crate::TestDispatcher), which runs everything on a -/// single thread with a virtual clock, work dispatched through this dispatcher -/// executes with production concurrency. -pub struct ThreadedDispatcher { - background_sender: PriorityQueueSender, - main_sender: PriorityQueueSender, - main_receiver: Mutex>, - timers: Arc, - idle: Arc, - main_thread_id: thread::ThreadId, -} - -/// Tracks how many background and timer runnables are queued or running so -/// [`ThreadedDispatcher::run_until_idle`] knows when to stop waiting. -#[derive(Default)] -struct IdleTracker { - inflight: Mutex, - condvar: Condvar, -} - -impl IdleTracker { - fn increment(&self) { - *self.inflight.lock() += 1; - } - - fn decrement(&self) { - let mut inflight = self.inflight.lock(); - *inflight -= 1; - if *inflight == 0 { - self.condvar.notify_all(); - } - } - - /// Returns a guard that decrements the in-flight count when dropped, so - /// the count stays correct even if the runnable being executed panics. - fn decrement_on_drop(&self) -> impl Drop + '_ { - gpui_util::defer(|| self.decrement()) - } - - /// Notifies waiters while holding the in-flight lock. `run_until_idle` - /// re-checks its wake conditions under this lock before waiting, so the - /// notification can't slip between its check and its wait and be lost. - fn notify_under_lock(&self) { - let _inflight = self.inflight.lock(); - self.condvar.notify_all(); - } -} - -struct TimerQueue { - state: Mutex, - condvar: Condvar, -} - -struct TimerQueueState { - heap: BinaryHeap, - next_seq: u64, -} - -struct TimerEntry { - due: Instant, - seq: u64, - runnable: RunnableVariant, -} - -impl PartialEq for TimerEntry { - fn eq(&self, other: &Self) -> bool { - self.due == other.due && self.seq == other.seq - } -} - -impl Eq for TimerEntry {} - -impl PartialOrd for TimerEntry { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for TimerEntry { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - // Reversed so that the entry with the earliest due time (breaking ties - // by insertion order) is at the top of the max-heap. - other - .due - .cmp(&self.due) - .then_with(|| other.seq.cmp(&self.seq)) - } -} - -impl Default for ThreadedDispatcher { - fn default() -> Self { - Self::new() - } -} - -impl ThreadedDispatcher { - /// Creates a dispatcher whose main thread is the calling thread. - /// - /// Worker and timer threads live for the lifetime of the process; the - /// dispatcher is expected to be created once and reused. - pub fn new() -> Self { - let (background_sender, background_receiver) = PriorityQueueReceiver::new(); - let (main_sender, main_receiver) = PriorityQueueReceiver::new(); - let idle = Arc::new(IdleTracker::default()); - - let thread_count = - thread::available_parallelism().map_or(MIN_THREADS, |i| i.get().max(MIN_THREADS)); - for i in 0..thread_count { - let mut receiver: PriorityQueueReceiver = background_receiver.clone(); - let idle = idle.clone(); - thread::Builder::new() - .name(format!("ThreadedDispatcherWorker-{i}")) - .spawn(move || { - while let Ok(runnable) = receiver.pop() { - let _decrement = idle.decrement_on_drop(); - let location = runnable.metadata().location; - let spawned = runnable.metadata().spawned; - profiler::update_running_task(spawned, location); - runnable.run(); - profiler::save_task_timing(); - } - }) - .expect("failed to spawn threaded dispatcher worker"); - } - drop(background_receiver); - - let timers = Arc::new(TimerQueue { - state: Mutex::new(TimerQueueState { - heap: BinaryHeap::new(), - next_seq: 0, - }), - condvar: Condvar::new(), - }); - { - let timers = timers.clone(); - let idle = idle.clone(); - thread::Builder::new() - .name("ThreadedDispatcherTimer".to_owned()) - .spawn(move || { - let mut state = timers.state.lock(); - loop { - let Some(entry) = state.heap.peek() else { - timers.condvar.wait(&mut state); - continue; - }; - let due = entry.due; - if due > Instant::now() { - timers.condvar.wait_until(&mut state, due); - continue; - } - let Some(entry) = state.heap.pop() else { - continue; - }; - // Count the firing timer as in-flight before releasing - // the lock so it can spawn follow-up work that - // `run_until_idle` will wait for. Lock order is always - // timer state, then in-flight count; `run_until_idle` - // never takes them in the opposite order. - idle.increment(); - drop(state); - - { - let _decrement = idle.decrement_on_drop(); - let location = entry.runnable.metadata().location; - let spawned = entry.runnable.metadata().spawned; - profiler::update_running_task(spawned, location); - entry.runnable.run(); - profiler::save_task_timing(); - } - - state = timers.state.lock(); - } - }) - .expect("failed to spawn threaded dispatcher timer"); - } - - Self { - background_sender, - main_sender, - main_receiver: Mutex::new(main_receiver), - timers, - idle, - main_thread_id: thread::current().id(), - } - } - - /// Runs queued main thread tasks and waits until no background or timer - /// work is queued, running, or already due. - /// - /// Timers that haven't reached their due time yet are *not* waited for: - /// the dispatcher runs in real time and cannot skip ahead like the - /// `TestDispatcher`'s virtual clock, so waiting on a future timer would - /// block for its full real duration. Tasks sleeping on such timers are - /// considered idle. Must be called on the thread that created this - /// dispatcher. - pub fn run_until_idle(&self) { - assert!( - self.is_main_thread(), - "run_until_idle must be called on the threaded dispatcher's main thread" - ); - loop { - if self.drain_main_queue() { - continue; - } - - // Checked before taking the in-flight lock; the timer thread - // locks them in the opposite order, so nesting would deadlock. - if self.has_due_timer() { - // Poll briefly: a firing timer leaves the heap just before it - // registers as in-flight. - let mut inflight = self.idle.inflight.lock(); - self.idle - .condvar - .wait_for(&mut inflight, Duration::from_millis(1)); - continue; - } - - let mut inflight = self.idle.inflight.lock(); - // Re-checked under the lock that `dispatch_on_main_thread` - // notifies under, so the notification can't be lost. - if self.main_queue_has_work() { - continue; - } - if *inflight == 0 { - // Main-thread sends happen before in-flight decrements, and - // decrements happen under this lock, so the check above - // observed all completed work. - return; - } - // Woken when main-thread work arrives or the in-flight count - // reaches zero; both notify under this lock. - self.idle.condvar.wait(&mut inflight); - } - } - - /// Drives main-thread work until `ready` returns a value. - /// - /// Unlike [`Self::run_until_idle`], this waits across temporary quiescence. - /// This is required when completion can arrive from an external worker that - /// is not represented in the dispatcher's in-flight count. - /// - /// Readiness is checked before every main-thread runnable, so this returns - /// as soon as `ready` observes completion rather than after the queue - /// drains — deferred work that re-queues itself (idle sweeps, pollers) - /// must not extend a benchmark's measured interval past the completion it - /// awaits. - #[cfg(any(test, feature = "bench-support"))] - pub(crate) fn run_until(&self, mut ready: impl FnMut() -> Option) -> R { - assert!( - self.is_main_thread(), - "run_until must be called on the threaded dispatcher's main thread" - ); - loop { - if let Some(result) = ready() { - return result; - } - if self.run_one_main_task() { - continue; - } - - let mut inflight = self.idle.inflight.lock(); - if self.main_queue_has_work() { - continue; - } - self.idle.condvar.wait(&mut inflight); - } - } - - /// Runs at most one queued main-thread task, returning whether one ran. - /// - /// [`Self::run_until`] steps tasks one at a time so it can observe - /// readiness between them: a task that perpetually re-queues itself (like - /// an idle-time sweep) would otherwise keep [`Self::drain_main_queue`] - /// looping past the completion the caller is waiting for. - #[cfg(any(test, feature = "bench-support"))] - fn run_one_main_task(&self) -> bool { - let runnable = self.main_receiver.lock().try_pop(); - match runnable { - Ok(Some(runnable)) => { - let location = runnable.metadata().location; - let spawned = runnable.metadata().spawned; - profiler::update_running_task(spawned, location); - runnable.run(); - profiler::save_task_timing(); - true - } - Ok(None) | Err(_) => false, - } - } - - /// Runs the main-thread tasks that were queued when the call began, - /// returning whether any ran. Tasks dispatched while running (e.g. a task - /// re-queuing itself after yielding) are left for the next call, as on - /// the platform run loops. - pub fn run_ready_main_tasks(&self) -> bool { - assert!( - self.is_main_thread(), - "run_ready_main_tasks must be called on the threaded dispatcher's main thread" - ); - let pending = self.main_receiver.lock().len(); - let mut ran_any = false; - for _ in 0..pending { - let runnable = self.main_receiver.lock().try_pop(); - match runnable { - Ok(Some(runnable)) => { - let location = runnable.metadata().location; - let spawned = runnable.metadata().spawned; - profiler::update_running_task(spawned, location); - runnable.run(); - profiler::save_task_timing(); - ran_any = true; - } - Ok(None) | Err(_) => break, - } - } - ran_any - } - - /// Cancels all pending timers so timers armed by one workload can't fire - /// during a later workload sharing this process-lifetime dispatcher. - /// - /// Dropping a timer runnable drops its completion sender, waking the task - /// awaiting the timer. Call [`Self::run_until_idle`] after this method to - /// drain any work that cancellation unblocks. - pub fn cancel_pending_timers(&self) -> usize { - let timers = { - let mut state = self.timers.state.lock(); - let timers: Vec<_> = state.heap.drain().collect(); - self.timers.condvar.notify_all(); - timers - }; - let canceled = timers.len(); - drop(timers); - canceled - } - - /// Describes the dispatcher's idle-tracking state, for diagnosing - /// workloads that fail to reach quiescence. - pub fn debug_state(&self) -> String { - let inflight = *self.idle.inflight.lock(); - let timers = self.timers.state.lock().heap.len(); - let main_queue_has_work = self.main_queue_has_work(); - format!( - "ThreadedDispatcher {{ inflight: {inflight}, pending_timers: {timers}, \ - main_queue_has_work: {main_queue_has_work} }}" - ) - } - - /// Whether no main-thread work is queued, no background or timer - /// runnables are queued or running, and no armed timer is due. Timers - /// that aren't due yet are ignored, as in [`Self::run_until_idle`]. - #[cfg(any(test, feature = "bench-support"))] - pub(crate) fn is_idle(&self) -> bool { - !self.main_queue_has_work() && !self.has_due_timer() && *self.idle.inflight.lock() == 0 - } - - fn has_due_timer(&self) -> bool { - let state = self.timers.state.lock(); - state - .heap - .peek() - .is_some_and(|entry| entry.due <= Instant::now()) - } - - fn main_queue_has_work(&self) -> bool { - !self.main_receiver.lock().is_empty() - } - - fn drain_main_queue(&self) -> bool { - let mut ran_any = false; - loop { - // Lock only around the pop so runnables can re-entrantly dispatch - // more main-thread work through the sender while they run. - let runnable = self.main_receiver.lock().try_pop(); - match runnable { - Ok(Some(runnable)) => { - let location = runnable.metadata().location; - let spawned = runnable.metadata().spawned; - profiler::update_running_task(spawned, location); - runnable.run(); - profiler::save_task_timing(); - ran_any = true; - } - Ok(None) | Err(_) => return ran_any, - } - } - } -} - -impl PlatformDispatcher for ThreadedDispatcher { - fn is_main_thread(&self) -> bool { - thread::current().id() == self.main_thread_id - } - - fn dispatch(&self, runnable: RunnableVariant, priority: Priority) { - self.idle.increment(); - self.background_sender - .send(priority, runnable) - .unwrap_or_else(|_| panic!("threaded dispatcher workers are no longer running")); - } - - fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority) { - if let Err(error) = self.main_sender.send(priority, runnable) { - // The main receiver lives as long as this dispatcher, so a failed - // send means we're mid-teardown. The runnable may wrap a !Send - // future, so forget it rather than dropping it on this thread - // (mirrors LinuxDispatcher). - std::mem::forget(error); - return; - } - // Wake `run_until_idle` if it's waiting for main-thread work. - self.idle.notify_under_lock(); - } - - fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant) { - let mut state = self.timers.state.lock(); - let seq = state.next_seq; - state.next_seq += 1; - state.heap.push(TimerEntry { - due: Instant::now() + duration, - seq, - runnable, - }); - self.timers.condvar.notify_one(); - } - - fn spawn_realtime(&self, f: Box) { - // This dispatcher does not need realtime scheduling priority; a plain - // thread keeps it portable. - thread::Builder::new() - .name("ThreadedDispatcherRealtime".to_owned()) - .spawn(f) - .expect("failed to spawn threaded dispatcher realtime thread"); - } - - fn as_threaded(&self) -> Option<&ThreadedDispatcher> { - Some(self) - } -} - -#[cfg(test)] -mod tests { - use std::future::Future; - use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; - - use super::*; - use crate::{BackgroundExecutor, ForegroundExecutor}; - - #[test] - fn is_idle_tracks_queued_work_but_ignores_undue_timers() { - let dispatcher = Arc::new(ThreadedDispatcher::new()); - let foreground = ForegroundExecutor::new(dispatcher.clone()); - assert!(dispatcher.is_idle()); - - foreground.spawn(async {}).detach(); - assert!(!dispatcher.is_idle()); - dispatcher.run_until_idle(); - assert!(dispatcher.is_idle()); - - let background = BackgroundExecutor::new(dispatcher.clone()); - let timer = background.timer(Duration::from_secs(60)); - // The timer future's initial poll runs on a worker thread; wait for - // it so only the armed, not-yet-due timer remains. - dispatcher.run_until_idle(); - assert!( - dispatcher.is_idle(), - "a timer that is not due yet should not count as pending work" - ); - drop(timer); - dispatcher.cancel_pending_timers(); - dispatcher.run_until_idle(); - assert!(dispatcher.is_idle()); - } - - #[test] - fn run_ready_main_tasks_does_not_wait_for_background_handoffs() { - let dispatcher = Arc::new(ThreadedDispatcher::new()); - let background = BackgroundExecutor::new(dispatcher.clone()); - let foreground = ForegroundExecutor::new(dispatcher.clone()); - - let (sender, receiver) = futures::channel::oneshot::channel(); - background - .spawn(async move { - thread::sleep(Duration::from_millis(10)); - sender.send(()).ok(); - }) - .detach(); - - let completed = Arc::new(AtomicBool::new(false)); - foreground - .spawn({ - let completed = completed.clone(); - async move { - receiver.await.ok(); - completed.store(true, Ordering::SeqCst); - } - }) - .detach(); - - assert!(dispatcher.run_ready_main_tasks()); - assert!(!completed.load(Ordering::SeqCst)); - - dispatcher.run_until_idle(); - assert!(completed.load(Ordering::SeqCst)); - } - - #[test] - fn run_until_idle_completes_background_to_main_handoffs() { - let dispatcher = Arc::new(ThreadedDispatcher::new()); - let background = BackgroundExecutor::new(dispatcher.clone()); - let foreground = ForegroundExecutor::new(dispatcher.clone()); - - let (sender, receiver) = futures::channel::oneshot::channel(); - background - .spawn(async move { - thread::sleep(Duration::from_millis(10)); - sender.send(()).ok(); - }) - .detach(); - - let completed = Arc::new(AtomicBool::new(false)); - foreground - .spawn({ - let completed = completed.clone(); - async move { - receiver.await.ok(); - completed.store(true, Ordering::SeqCst); - } - }) - .detach(); - - dispatcher.run_until_idle(); - assert!(completed.load(Ordering::SeqCst)); - } - - #[test] - fn run_until_waits_for_untracked_external_wakes() { - let dispatcher = Arc::new(ThreadedDispatcher::new()); - let foreground = ForegroundExecutor::new(dispatcher.clone()); - let (sender, receiver) = futures::channel::oneshot::channel(); - let sender_thread = thread::spawn(move || { - thread::sleep(Duration::from_millis(10)); - sender - .send(()) - .expect("foreground receiver should remain alive"); - }); - - let completed = Arc::new(AtomicBool::new(false)); - foreground - .spawn({ - let completed = completed.clone(); - async move { - receiver - .await - .expect("external sender should deliver its wake"); - completed.store(true, Ordering::SeqCst); - } - }) - .detach(); - - dispatcher.run_until(|| completed.load(Ordering::SeqCst).then_some(())); - sender_thread.join().expect("sender thread should finish"); - assert!(completed.load(Ordering::SeqCst)); - } - - #[test] - fn run_until_returns_at_readiness_despite_requeuing_main_work() { - const REQUEUE_LIMIT: usize = 10_000; - - let dispatcher = Arc::new(ThreadedDispatcher::new()); - let foreground = ForegroundExecutor::new(dispatcher.clone()); - - // Mirrors main-thread work that yields and immediately re-queues - // itself (e.g. an idle-time sweep): the main queue never drains until - // such work finishes every iteration, so readiness must be observed - // between runnables rather than only at quiescence. - let iterations = Arc::new(AtomicUsize::new(0)); - foreground - .spawn({ - let iterations = iterations.clone(); - async move { - for _ in 0..REQUEUE_LIMIT { - iterations.fetch_add(1, Ordering::SeqCst); - yield_once().await; - } - } - }) - .detach(); - - let completed = Arc::new(AtomicBool::new(false)); - foreground - .spawn({ - let completed = completed.clone(); - async move { - completed.store(true, Ordering::SeqCst); - } - }) - .detach(); - - dispatcher.run_until(|| completed.load(Ordering::SeqCst).then_some(())); - assert!( - iterations.load(Ordering::SeqCst) < REQUEUE_LIMIT, - "run_until should return at readiness instead of draining re-queued main work" - ); - } - - /// Completes after one re-schedule: the poll returns `Pending` and wakes - /// immediately, so the runnable re-enters the main queue. - fn yield_once() -> impl Future { - let mut yielded = false; - std::future::poll_fn(move |poll_context| { - if yielded { - std::task::Poll::Ready(()) - } else { - yielded = true; - poll_context.waker().wake_by_ref(); - std::task::Poll::Pending - } - }) - } - - #[test] - fn run_ready_main_tasks_advances_requeuing_work_one_batch_per_call() { - const REQUEUE_LIMIT: usize = 10_000; - - let dispatcher = Arc::new(ThreadedDispatcher::new()); - let foreground = ForegroundExecutor::new(dispatcher.clone()); - - let iterations = Arc::new(AtomicUsize::new(0)); - foreground - .spawn({ - let iterations = iterations.clone(); - async move { - for _ in 0..REQUEUE_LIMIT { - iterations.fetch_add(1, Ordering::SeqCst); - yield_once().await; - } - } - }) - .detach(); - - assert!(dispatcher.run_ready_main_tasks()); - assert_eq!(iterations.load(Ordering::SeqCst), 1); - assert!(dispatcher.run_ready_main_tasks()); - assert_eq!(iterations.load(Ordering::SeqCst), 2); - } - - #[test] - fn timers_fire_in_real_time() { - let dispatcher = Arc::new(ThreadedDispatcher::new()); - let background = BackgroundExecutor::new(dispatcher); - - let fired = Arc::new(AtomicBool::new(false)); - let timer = background.timer(Duration::from_millis(10)); - background - .spawn({ - let fired = fired.clone(); - async move { - timer.await; - fired.store(true, Ordering::SeqCst); - } - }) - .detach(); - - let deadline = Instant::now() + Duration::from_secs(10); - while !fired.load(Ordering::SeqCst) && Instant::now() < deadline { - thread::sleep(Duration::from_millis(1)); - } - assert!(fired.load(Ordering::SeqCst)); - } - - #[test] - fn cancel_pending_timers_wakes_waiters_without_waiting_for_deadline() { - let dispatcher = Arc::new(ThreadedDispatcher::new()); - let background = BackgroundExecutor::new(dispatcher.clone()); - - let fired = Arc::new(AtomicBool::new(false)); - let timer = background.timer(Duration::from_secs(10)); - background - .spawn({ - let fired = fired.clone(); - async move { - timer.await; - fired.store(true, Ordering::SeqCst); - } - }) - .detach(); - - dispatcher.run_until_idle(); - assert_eq!(dispatcher.cancel_pending_timers(), 1); - dispatcher.run_until_idle(); - - assert!(fired.load(Ordering::SeqCst)); - assert_eq!(dispatcher.cancel_pending_timers(), 0); - } -} diff --git a/crates/gpui_pre/src/platform/visual_test.rs b/crates/gpui_pre/src/platform/visual_test.rs deleted file mode 100644 index 042a757..0000000 --- a/crates/gpui_pre/src/platform/visual_test.rs +++ /dev/null @@ -1,264 +0,0 @@ -//! Visual test platform that combines real rendering (macOs-only for now) with controllable TestDispatcher. -//! -//! This platform is used for visual tests that need: -//! - Real rendering (e.g. Metal/compositor) for accurate screenshots -//! - Deterministic task scheduling via TestDispatcher -//! - Controllable time via `advance_clock` - -use crate::ScreenCaptureSource; -use crate::{ - AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor, Keymap, - Menu, MenuItem, OwnedMenu, PathPromptOptions, Platform, PlatformDisplay, - PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PlatformWindow, Task, - TestDispatcher, WindowAppearance, WindowParams, -}; -use anyhow::Result; -use futures::channel::oneshot; -use parking_lot::Mutex; - -use std::{ - path::{Path, PathBuf}, - rc::Rc, - sync::Arc, -}; - -/// A platform that combines real Mac rendering with controllable TestDispatcher. -/// -/// This allows visual tests to: -/// - Render real UI via Metal for accurate screenshots -/// - Control task scheduling deterministically via TestDispatcher -/// - Advance simulated time for testing time-based behaviors (tooltips, animations, etc.) -pub struct VisualTestPlatform { - dispatcher: TestDispatcher, - background_executor: BackgroundExecutor, - foreground_executor: ForegroundExecutor, - platform: Rc, - clipboard: Mutex>, - find_pasteboard: Mutex>, -} - -impl VisualTestPlatform { - /// Creates a new VisualTestPlatform with the given random seed. - /// - /// The seed is used for deterministic random number generation in the TestDispatcher. - pub fn new(platform: Rc, seed: u64) -> Self { - let dispatcher = TestDispatcher::new(seed); - let arc_dispatcher = Arc::new(dispatcher.clone()); - - let background_executor = BackgroundExecutor::new(arc_dispatcher.clone()); - let foreground_executor = ForegroundExecutor::new(arc_dispatcher); - - Self { - dispatcher, - background_executor, - foreground_executor, - platform, - clipboard: Mutex::new(None), - find_pasteboard: Mutex::new(None), - } - } - - /// Returns a reference to the TestDispatcher for controlling task scheduling and time. - pub fn dispatcher(&self) -> &TestDispatcher { - &self.dispatcher - } -} - -impl Platform for VisualTestPlatform { - fn background_executor(&self) -> BackgroundExecutor { - self.background_executor.clone() - } - - fn foreground_executor(&self) -> ForegroundExecutor { - self.foreground_executor.clone() - } - - fn text_system(&self) -> Arc { - self.platform.text_system() - } - - fn run(&self, _on_finish_launching: Box) { - panic!("VisualTestPlatform::run should not be called in tests") - } - - fn quit(&self) {} - - fn restart(&self, _binary_path: Option, _arguments: Vec) {} - - fn activate(&self, _ignoring_other_apps: bool) {} - - fn hide(&self) {} - - fn hide_other_apps(&self) {} - - fn unhide_other_apps(&self) {} - - fn displays(&self) -> Vec> { - self.platform.displays() - } - - fn primary_display(&self) -> Option> { - self.platform.primary_display() - } - - fn active_window(&self) -> Option { - self.platform.active_window() - } - - fn window_stack(&self) -> Option> { - self.platform.window_stack() - } - - fn is_screen_capture_supported(&self) -> bool { - self.platform.is_screen_capture_supported() - } - - fn screen_capture_sources( - &self, - ) -> oneshot::Receiver>>> { - self.platform.screen_capture_sources() - } - - fn open_window( - &self, - handle: AnyWindowHandle, - options: WindowParams, - ) -> Result> { - self.platform.open_window(handle, options) - } - - fn window_appearance(&self) -> WindowAppearance { - self.platform.window_appearance() - } - - fn open_url(&self, url: &str) { - self.platform.open_url(url) - } - - fn on_open_urls(&self, _callback: Box)>) {} - - fn register_url_scheme(&self, _url: &str) -> Task> { - Task::ready(Ok(())) - } - - fn prompt_for_paths( - &self, - _options: PathPromptOptions, - ) -> oneshot::Receiver>>> { - let (tx, rx) = oneshot::channel(); - tx.send(Ok(None)).ok(); - rx - } - - fn prompt_for_new_path( - &self, - _directory: &Path, - _suggested_name: Option<&str>, - ) -> oneshot::Receiver>> { - let (tx, rx) = oneshot::channel(); - tx.send(Ok(None)).ok(); - rx - } - - fn can_select_mixed_files_and_dirs(&self) -> bool { - true - } - - fn reveal_path(&self, path: &Path) { - self.platform.reveal_path(path) - } - - fn open_with_system(&self, path: &Path) { - self.platform.open_with_system(path) - } - - fn on_quit(&self, _callback: Box bool>) {} - - fn on_reopen(&self, _callback: Box) {} - - fn on_system_wake(&self, _callback: Box) {} - - fn set_menus(&self, _menus: Vec, _keymap: &Keymap) {} - - fn get_menus(&self) -> Option> { - None - } - - fn set_dock_menu(&self, _menu: Vec, _keymap: &Keymap) {} - - fn on_app_menu_action(&self, _callback: Box) {} - - fn on_will_open_app_menu(&self, _callback: Box) {} - - fn on_validate_app_menu_command(&self, _callback: Box bool>) {} - - fn app_path(&self) -> Result { - self.platform.app_path() - } - - fn path_for_auxiliary_executable(&self, name: &str) -> Result { - self.platform.path_for_auxiliary_executable(name) - } - - fn set_cursor_style(&self, style: CursorStyle) { - self.platform.set_cursor_style(style) - } - - fn hide_cursor_until_mouse_moves(&self) { - self.platform.hide_cursor_until_mouse_moves(); - } - - fn is_cursor_visible(&self) -> bool { - self.platform.is_cursor_visible() - } - - fn should_auto_hide_scrollbars(&self) -> bool { - self.platform.should_auto_hide_scrollbars() - } - - fn read_from_clipboard(&self) -> Option { - self.clipboard.lock().clone() - } - - fn write_to_clipboard(&self, item: ClipboardItem) { - *self.clipboard.lock() = Some(item); - } - - #[cfg(target_os = "macos")] - fn read_from_find_pasteboard(&self) -> Option { - self.find_pasteboard.lock().clone() - } - - #[cfg(target_os = "macos")] - fn write_to_find_pasteboard(&self, item: ClipboardItem) { - *self.find_pasteboard.lock() = Some(item); - } - - fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task> { - Task::ready(Ok(())) - } - - fn read_credentials(&self, _url: &str) -> Task)>>> { - Task::ready(Ok(None)) - } - - fn delete_credentials(&self, _url: &str) -> Task> { - Task::ready(Ok(())) - } - - fn keyboard_layout(&self) -> Box { - self.platform.keyboard_layout() - } - - fn keyboard_mapper(&self) -> Rc { - self.platform.keyboard_mapper() - } - - fn on_keyboard_layout_change(&self, _callback: Box) {} - - fn thermal_state(&self) -> super::ThermalState { - super::ThermalState::Nominal - } - - fn on_thermal_state_change(&self, _callback: Box) {} -} diff --git a/crates/gpui_pre/src/platform_scheduler.rs b/crates/gpui_pre/src/platform_scheduler.rs deleted file mode 100644 index a555a5a..0000000 --- a/crates/gpui_pre/src/platform_scheduler.rs +++ /dev/null @@ -1,457 +0,0 @@ -use crate::{PlatformDispatcher, RunnableMeta}; -use async_task::Runnable; -use chrono::{DateTime, Utc}; -use futures::channel::oneshot; -use scheduler::Instant; -use scheduler::{ - Clock, LocalExecutor, Priority, Scheduler, SessionId, Task, TestScheduler, Timer, - spawn_dedicated_thread, -}; -#[cfg(not(target_family = "wasm"))] -use std::task::{Context, Poll}; -use std::{ - any::Any, - future::Future, - pin::Pin, - sync::{ - Arc, - atomic::{AtomicU16, Ordering}, - }, - time::Duration, -}; - -/// A production implementation of [`Scheduler`] that wraps a [`PlatformDispatcher`]. -/// -/// This allows GPUI to use the scheduler crate's executor types with the platform's -/// native dispatch mechanisms (e.g., Grand Central Dispatch on macOS). -pub struct PlatformScheduler { - dispatcher: Arc, - clock: Arc, - next_session_id: AtomicU16, - #[cfg(feature = "profiler")] - foreground_runnables: crate::profiler::journal::ForegroundRunnableCounter, -} - -impl PlatformScheduler { - pub fn new(dispatcher: Arc) -> Self { - Self { - dispatcher: dispatcher.clone(), - clock: Arc::new(PlatformClock { dispatcher }), - next_session_id: AtomicU16::new(0), - #[cfg(feature = "profiler")] - foreground_runnables: crate::profiler::journal::foreground_runnable_counter(), - } - } - - pub fn foreground_executor(self: &Arc) -> LocalExecutor { - let session_id = self.next_session_id(); - let scheduler = Arc::downgrade(self); - LocalExecutor::new(session_id, self.clone(), move |runnable| { - if let Some(scheduler) = scheduler.upgrade() { - scheduler.schedule_local(session_id, runnable); - } - }) - } - - fn next_session_id(&self) -> SessionId { - SessionId::new(self.next_session_id.fetch_add(1, Ordering::SeqCst)) - } - - #[cfg(feature = "profiler")] - pub(crate) fn foreground_runnable_counter( - &self, - ) -> crate::profiler::journal::ForegroundRunnableCounter { - self.foreground_runnables.clone() - } -} - -impl Scheduler for PlatformScheduler { - #[cfg(not(target_family = "wasm"))] - fn block( - &self, - _session_id: Option, - mut future: Pin<&mut dyn Future>, - timeout: Option, - ) -> bool { - use waker_fn::waker_fn; - let deadline = timeout.map(|t| Instant::now() + t); - let parker = parking::Parker::new(); - let unparker = parker.unparker(); - let waker = waker_fn(move || { - unparker.unpark(); - }); - let mut cx = Context::from_waker(&waker); - if let Poll::Ready(()) = future.as_mut().poll(&mut cx) { - return true; - } - - let park_deadline = |deadline: Instant| { - // Timer expirations are only delivered every ~15.6 milliseconds by default on Windows. - // We increase the resolution during this wait so that short timeouts stay reasonably short. - let _timer_guard = self.dispatcher.increase_timer_resolution(); - parker.park_deadline(deadline) - }; - - loop { - match deadline { - Some(deadline) if !park_deadline(deadline) && deadline <= Instant::now() => { - return false; - } - Some(_) => (), - None => parker.park(), - } - if let Poll::Ready(()) = future.as_mut().poll(&mut cx) { - break true; - } - } - } - - fn schedule_local(&self, _session_id: SessionId, runnable: Runnable) { - #[cfg(feature = "profiler")] - self.foreground_runnables.queued(); - self.dispatcher - .dispatch_on_main_thread(runnable, Priority::default()); - } - - fn schedule_background_with_priority( - &self, - runnable: Runnable, - priority: Priority, - ) { - self.dispatcher.dispatch(runnable, priority); - } - - fn spawn_realtime(&self, f: Box) { - self.dispatcher.spawn_realtime(f); - } - - #[track_caller] - fn timer(&self, duration: Duration) -> Timer { - let (tx, rx) = oneshot::channel(); - let dispatcher = self.dispatcher.clone(); - - // Create a runnable that will send the completion signal - let location = std::panic::Location::caller(); - let (runnable, _task) = async_task::Builder::new() - .metadata(RunnableMeta { - location, - spawned: scheduler::SpawnTime(Instant::now()), - }) - .spawn( - move |_| async move { - let _ = tx.send(()); - }, - move |runnable| { - dispatcher.dispatch_after(duration, runnable); - }, - ); - runnable.schedule(); - - Timer::new(rx) - } - - fn clock(&self) -> Arc { - self.clock.clone() - } - - fn spawn_dedicated( - self: Arc, - f: Box< - dyn FnOnce( - LocalExecutor, - ) - -> Pin> + 'static>> - + Send - + 'static, - >, - ) -> Task> { - let session_id = self.next_session_id(); - spawn_dedicated_thread(session_id, self, move |executor| f(executor)) - } - - fn as_test(&self) -> Option<&TestScheduler> { - None - } -} - -/// A production clock that uses the platform dispatcher's time. -struct PlatformClock { - dispatcher: Arc, -} - -impl Clock for PlatformClock { - fn utc_now(&self) -> DateTime { - Utc::now() - } - - fn now(&self) -> Instant { - self.dispatcher.now() - } -} - -#[cfg(all(test, not(target_family = "wasm")))] -mod tests { - use super::*; - use crate::RunnableVariant; - use scheduler::BackgroundExecutor; - use std::time::Instant as StdInstant; - - // `spawn_dedicated` shouldn't touch the platform dispatcher at all; - // panicking on every method ensures the test catches it if it does. - struct SmokeDispatcher; - - impl PlatformDispatcher for SmokeDispatcher { - fn is_main_thread(&self) -> bool { - false - } - fn dispatch(&self, _runnable: RunnableVariant, _priority: Priority) { - panic!("SmokeDispatcher should not be asked to dispatch in this test"); - } - fn dispatch_on_main_thread(&self, _runnable: RunnableVariant, _priority: Priority) { - panic!("SmokeDispatcher does not implement a main thread"); - } - fn dispatch_after(&self, _duration: Duration, _runnable: RunnableVariant) { - panic!("SmokeDispatcher does not implement timers"); - } - fn spawn_realtime(&self, _f: Box) { - panic!("SmokeDispatcher does not implement realtime"); - } - } - - #[test] - fn dedicated_executor_tasks_share_one_thread() { - let background = - BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher)))); - let dedicated = scheduler::DedicatedExecutor::new(&background); - - let first = dedicated.spawn(async { std::thread::current().id() }); - let second = dedicated.spawn(async { std::thread::current().id() }); - - let first = futures::executor::block_on(first); - let second = futures::executor::block_on(second); - - assert_eq!(first, second, "tasks must share the dedicated thread"); - assert_ne!( - first, - std::thread::current().id(), - "dedicated tasks must not run on the spawning thread" - ); - } - - #[test] - fn spawn_dedicated_runs_on_a_real_separate_thread() { - let background = - BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher)))); - let started = StdInstant::now(); - let task = background.spawn_dedicated(|_executor| async move { - // A genuine blocking syscall on the dedicated thread. If - // `spawn_dedicated` were running the future on any shared - // executor, this would stall that executor. - let thread_id_before = std::thread::current().id(); - std::thread::sleep(Duration::from_millis(50)); - let thread_id_after = std::thread::current().id(); - assert_eq!(thread_id_before, thread_id_after); - (thread_id_before, "slept") - }); - let (dedicated_thread_id, message) = futures::executor::block_on(task); - let elapsed = started.elapsed(); - assert_eq!(message, "slept"); - assert_ne!( - dedicated_thread_id, - std::thread::current().id(), - "dedicated future ran on the test thread" - ); - assert!( - elapsed >= Duration::from_millis(40), - "expected the dedicated thread to genuinely sleep, elapsed = {:?}", - elapsed - ); - } - - #[test] - fn spawn_dedicated_returns_not_send_future_output() { - // The whole point of `spawn_dedicated` is that the future can be - // `!Send`. Constructing one with `Rc>` ensures the - // signature actually permits it. - use std::cell::RefCell; - use std::rc::Rc; - - let background = - BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher)))); - let task = background.spawn_dedicated(|_executor| async move { - let state = Rc::new(RefCell::new(0_i32)); - for _ in 0..3 { - *state.borrow_mut() += 1; - } - *state.borrow() - }); - let output = futures::executor::block_on(task); - assert_eq!(output, 3); - } - - #[test] - fn spawn_dedicated_dropping_task_cancels_future() { - use parking_lot::Mutex; - use std::sync::mpsc; - - let background = - BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher)))); - - let (started_tx, started_rx) = mpsc::channel::<()>(); - let (after_park_tx, after_park_rx) = mpsc::channel::<()>(); - let observed_post_await_write = Arc::new(Mutex::new(false)); - - let task = { - let observed_post_await_write = observed_post_await_write.clone(); - background.spawn_dedicated(move |_executor| async move { - // Announce that the future is live on the dedicated thread. - started_tx - .send(()) - .expect("started signal must be received"); - // Park forever. Dropping the `Task` must cancel us here so - // the code below this `await` never runs. - futures::future::pending::<()>().await; - *observed_post_await_write.lock() = true; - after_park_tx - .send(()) - .expect("after-park signal must be received"); - }) - }; - - // Wait until the dedicated future is actually parked at the await. - started_rx - .recv_timeout(Duration::from_secs(2)) - .expect("dedicated future failed to start"); - - // Drop the root Task: this must cancel the future. - drop(task); - - // If cancellation works, the future never advances past `pending`, - // so this recv must time out. - assert!( - after_park_rx - .recv_timeout(Duration::from_millis(100)) - .is_err(), - "dedicated future advanced past the await after its Task was dropped" - ); - assert!( - !*observed_post_await_write.lock(), - "dedicated future ran code past the cancellation point" - ); - } - - #[test] - fn spawn_dedicated_thread_tears_down_after_work_completes() { - use std::sync::mpsc; - - // Fires from `Drop` so we observe teardown of the dedicated future's - // captured state on whichever thread runs its destructor. - struct DropSignal { - tx: Option>, - } - impl Drop for DropSignal { - fn drop(&mut self) { - if let Some(tx) = self.tx.take() { - let _ = tx.send(std::thread::current().id()); - } - } - } - - let background = - BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher)))); - let (started_tx, started_rx) = mpsc::channel::(); - let (drop_tx, drop_rx) = mpsc::channel::(); - - let task = background.spawn_dedicated(move |_executor| async move { - // Captured by the future's state. When the future completes and - // its state is dropped on the dedicated thread, this guard's - // `Drop` fires and reports the thread id it ran on. - let _guard = DropSignal { tx: Some(drop_tx) }; - started_tx - .send(std::thread::current().id()) - .expect("started signal must be received"); - // Future returns immediately. The dedicated thread should then - // drop the future (firing _guard), exit the recv loop, and exit. - }); - - let dedicated_thread_id = started_rx - .recv_timeout(Duration::from_secs(2)) - .expect("dedicated future failed to start"); - assert_ne!( - dedicated_thread_id, - std::thread::current().id(), - "dedicated future ran on the test thread" - ); - - // Drive the root task to completion so its body finishes. - futures::executor::block_on(task); - - // The guard's drop runs from the dedicated thread as it tears down - // the future's captured state. If the executor/recv-loop were - // keeping the future alive past task completion, this would hang. - let drop_thread_id = drop_rx - .recv_timeout(Duration::from_secs(2)) - .expect("dedicated future's captured state was not dropped after task completion"); - assert_eq!( - drop_thread_id, dedicated_thread_id, - "dedicated future's captured state must be dropped on the dedicated thread, not elsewhere" - ); - } - - #[test] - fn spawn_dedicated_detached_child_outlives_root() { - use std::sync::mpsc; - - let background = - BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher)))); - - // `gate_rx` lets the detached child park until the test explicitly - // releases it — after we've already observed the root completing. - let (gate_tx, gate_rx) = mpsc::channel::<()>(); - let (child_done_tx, child_done_rx) = mpsc::channel::(); - - let task = background.spawn_dedicated(move |executor| async move { - executor - .spawn(async move { - // Blocking on `recv` is normally wrong inside an - // executor, but the dedicated thread is exclusive to - // this session, so blocking the only future on it is - // fine — this is the property `spawn_dedicated` is - // designed to provide. - gate_rx - .recv() - .expect("gate sender dropped before child resumed"); - child_done_tx - .send(std::thread::current().id()) - .expect("child_done receiver dropped"); - }) - .detach(); - // Root finishes here. The detached child must keep the - // dedicated thread alive until it completes. - }); - - futures::executor::block_on(task); - - // Negative assertion: the child has not finished, because the gate - // hasn't been released yet. - assert!( - child_done_rx - .recv_timeout(Duration::from_millis(50)) - .is_err(), - "detached child finished before being released" - ); - - // Release the gate. The detached child should now complete on the - // dedicated thread. - gate_tx.send(()).expect("gate receiver dropped"); - - let child_thread_id = child_done_rx - .recv_timeout(Duration::from_secs(2)) - .expect("detached child failed to complete after gate was released"); - assert_ne!( - child_thread_id, - std::thread::current().id(), - "detached child ran on the test thread instead of the dedicated thread" - ); - } -} diff --git a/crates/gpui_pre/src/prelude.rs b/crates/gpui_pre/src/prelude.rs deleted file mode 100644 index b5185a2..0000000 --- a/crates/gpui_pre/src/prelude.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! The GPUI prelude is a collection of traits and types that are widely used -//! throughout the library. It is recommended to import this prelude into your -//! application to avoid having to import each trait individually. - -pub use crate::{ - AppContext as _, BorrowAppContext, Context, Element, InteractiveElement, IntoElement, - ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, Styled, StyledImage, - TaskExt as _, VisualContext, util::FluentBuilder, -}; diff --git a/crates/gpui_pre/src/profiler.rs b/crates/gpui_pre/src/profiler.rs deleted file mode 100644 index 340f054..0000000 --- a/crates/gpui_pre/src/profiler.rs +++ /dev/null @@ -1,1582 +0,0 @@ -#[cfg(feature = "profiler")] -use hdrhistogram::Histogram; -use itertools::Itertools; -use scheduler::{Instant, SpawnTime}; -#[cfg(feature = "profiler")] -use smallvec::SmallVec; -use std::{ - cell::LazyCell, - collections::{HashMap, VecDeque}, - hash::{DefaultHasher, Hash, Hasher}, - sync::{ - Arc, - atomic::{AtomicU64, Ordering}, - }, - thread::ThreadId, - time::Duration, -}; - -mod actions; -#[cfg(feature = "profiler")] -pub mod hang; -#[cfg(feature = "profiler")] -pub mod journal; -pub use actions::{ActionStatistics, ActionTiming, take_action_stats}; - -use serde::{Deserialize, Serialize}; - -#[cfg(feature = "profiler")] -use crate::{Action, App, WindowId}; -use crate::{SharedString, TasksIncluded}; - -#[cfg(feature = "profiler")] -#[doc(hidden)] -pub fn get_all_timings(included: gpui::TasksIncluded) -> Vec { - ThreadTaskTimings::collect(upgraded_thread_timings(), included) -} - -#[cfg(feature = "profiler")] -#[doc(hidden)] -pub fn get_current_thread_timings(included: TasksIncluded) -> gpui::ThreadTaskTimings { - gpui::profiler::get_current_thread_task_timings(included) -} - -#[cfg(feature = "profiler")] -#[doc(hidden)] -pub fn take_all_stats(included: TasksIncluded) -> Vec { - ThreadTaskStatistics::collect_and_reset(upgraded_thread_timings(), included) -} - -#[cfg(not(feature = "profiler"))] -#[doc(hidden)] -pub fn get_all_timings(_included: gpui::TasksIncluded) -> Vec { - Vec::new() -} -#[cfg(not(feature = "profiler"))] -#[doc(hidden)] -pub fn get_current_thread_timings(_included: TasksIncluded) -> gpui::ThreadTaskTimings { - gpui::ThreadTaskTimings { - thread_name: None, - thread_id: std::thread::current().id(), - timings: Vec::new(), - stats: TaskStatistics::default(), - total_pushed: 0, - } -} -#[cfg(not(feature = "profiler"))] -#[doc(hidden)] -pub fn take_all_stats(_included: TasksIncluded) -> Vec { - Vec::new() -} - -#[doc(hidden)] -#[derive(Debug, Copy, Clone)] -pub struct YieldTime(pub Instant); - -#[doc(hidden)] -#[derive(Copy, Clone)] -pub struct TaskTiming { - pub location: &'static core::panic::Location<'static>, - pub spawned: SpawnTime, - pub start: Instant, - pub end: YieldTime, -} - -impl std::fmt::Debug for TaskTiming { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("TaskTiming") - .field("location", &self.location) - .field("since_spawned", &self.spawned.0.elapsed()) - .field("last_poll_duration", &self.poll_duration()) - .field("total_runtime", &self.since_spawn()) - .finish() - } -} - -#[doc(hidden)] -#[derive(Debug, Copy, Clone)] -pub struct ActiveTiming { - pub location: &'static core::panic::Location<'static>, - pub spawned: SpawnTime, - pub start: Instant, -} - -impl TaskTiming { - /// A task timing with a duration of zero. Any task will replace this in history. - pub fn placeholder() -> Self { - let now = Instant::now(); - Self { - location: std::panic::Location::caller(), - spawned: SpawnTime(now), - start: now, - end: YieldTime(now), - } - } - - #[inline(always)] - pub fn poll_duration(&self) -> Duration { - self.end.0 - self.start - } - - #[inline(always)] - fn since_spawn(&self) -> Duration { - self.end.0 - self.spawned.0 - } -} - -#[doc(hidden)] -#[derive(Debug, Clone)] -pub struct ThreadTaskTimings { - pub thread_name: Option, - pub thread_id: ThreadId, - pub timings: Vec, - pub stats: TaskStatistics, - pub total_pushed: u64, -} - -impl ThreadTaskTimings { - /// Convert upgraded per-thread timings into their structured format. - pub fn collect( - timings: Vec<(ThreadId, Arc)>, - included: TasksIncluded, - ) -> Vec { - timings - .into_iter() - .map(|(thread_id, timings)| { - let timings = timings.lock(); - let thread_name = timings.thread_name.clone(); - let total_pushed = timings.total_pushed; - let completed = &timings.timings; - - let mut vec = Vec::with_capacity(completed.len() + 1); // +1 for running task - let (s1, s2) = completed.as_slices(); - vec.extend_from_slice(s1); - vec.extend_from_slice(s2); - if let TasksIncluded::CompletedAndRunning = included - && let Some(running) = timings.running - { - vec.push(TaskTiming { - location: running.location, - spawned: running.spawned, - start: running.start, - end: YieldTime(Instant::now()), - }) - } - - ThreadTaskTimings { - thread_name, - thread_id, - timings: vec, - stats: timings.stats.clone(), - total_pushed, - } - }) - .collect() - } -} - -#[doc(hidden)] -#[derive(Debug)] -pub struct ThreadTaskStatistics { - pub thread_name: Option, - pub thread_id: ThreadId, - pub stats: TaskStatistics, -} - -impl ThreadTaskStatistics { - pub fn collect_and_reset( - timings: Vec<(ThreadId, Arc)>, - include_running: TasksIncluded, - ) -> Vec { - timings - .into_iter() - .map(|(thread_id, timings)| { - let mut timings = timings.lock(); - let thread_name = timings.thread_name.clone(); - - let mut stats = std::mem::take(&mut timings.stats); - if let TasksIncluded::CompletedAndRunning = include_running - && let Some(ActiveTiming { - location, - spawned, - start, - }) = timings.running - { - let end = YieldTime(Instant::now()); - let timing = TaskTiming { - location, - spawned, - start, - end, - }; - stats.add_runtime(timing); - stats.add_yield_timing(timing); - } - - Self { - thread_name, - thread_id, - stats, - } - }) - .collect() - } -} - -/// Serializable variant of [`core::panic::Location`] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SerializedLocation { - /// Name of the source file - pub file: SharedString, - /// Line in the source file - pub line: u32, - /// Column in the source file - pub column: u32, -} - -impl From<&core::panic::Location<'static>> for SerializedLocation { - fn from(value: &core::panic::Location<'static>) -> Self { - SerializedLocation { - file: value.file().into(), - line: value.line(), - column: value.column(), - } - } -} - -/// Serializable variant of [`TaskTiming`] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SerializedTaskTiming { - /// Location of the timing - pub location: SerializedLocation, - /// Time at which the measurement was reported in nanoseconds - pub start: u128, - /// Duration of the measurement in nanoseconds - pub duration: u128, -} - -impl SerializedTaskTiming { - /// Convert an array of [`TaskTiming`] into their serializable format - /// - /// # Params - /// - /// `anchor` - [`Instant`] that should be earlier than all timings to use as base anchor - pub fn convert(anchor: Instant, timings: &[TaskTiming]) -> Vec { - let serialized = timings - .iter() - .map(|timing| { - let start = timing.start.duration_since(anchor).as_nanos(); - let duration = timing.end.0.duration_since(timing.start).as_nanos(); - SerializedTaskTiming { - location: timing.location.into(), - start, - duration, - } - }) - .collect::>(); - - serialized - } - - /// `anchor` - [`Instant`] that should be earlier than all timings to use as base anchor - pub fn from(anchor: Instant, timing: TaskTiming) -> SerializedTaskTiming { - let start = timing.start.duration_since(anchor).as_nanos(); - let duration = timing.end.0.duration_since(timing.start).as_nanos(); - SerializedTaskTiming { - location: timing.location.into(), - start, - duration, - } - } -} - -/// Serializable variant of [`ThreadTaskTimings`] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SerializedThreadTaskTimings { - /// Thread name - pub thread_name: Option, - /// Hash of the thread id - pub thread_id: u64, - /// Timing records for this thread - pub timings: Vec, -} - -impl SerializedThreadTaskTimings { - /// Convert [`ThreadTaskTimings`] into their serializable format - /// - /// # Params - /// - /// `anchor` - [`Instant`] that should be earlier than all timings to use as base anchor - pub fn convert(anchor: Instant, timings: ThreadTaskTimings) -> SerializedThreadTaskTimings { - let serialized_timings = SerializedTaskTiming::convert(anchor, &timings.timings); - - let mut hasher = DefaultHasher::new(); - timings.thread_id.hash(&mut hasher); - let thread_id = hasher.finish(); - - SerializedThreadTaskTimings { - thread_name: timings.thread_name, - thread_id, - timings: serialized_timings, - } - } -} - -#[doc(hidden)] -#[derive(Debug, Clone)] -pub struct ThreadTimingsDelta { - /// Hashed thread id - pub thread_id: u64, - /// Thread name, if known - pub thread_name: Option, - /// New timings since the last call. If the circular buffer wrapped around - /// since the previous poll, some entries may have been lost. - pub new_timings: Vec, -} - -/// Tracks which timing events have already been seen so that callers can request only unseen events. -#[doc(hidden)] -pub struct ProfilingCollector { - startup_time: Instant, - cursors: HashMap, -} - -impl ProfilingCollector { - pub fn new(startup_time: Instant) -> Self { - Self { - startup_time, - cursors: HashMap::default(), - } - } - - pub fn startup_time(&self) -> Instant { - self.startup_time - } - - pub fn collect_unseen( - &mut self, - all_timings: Vec, - ) -> Vec { - let mut deltas = Vec::with_capacity(all_timings.len()); - - for thread in all_timings { - let mut hasher = DefaultHasher::new(); - thread.thread_id.hash(&mut hasher); - let hashed_id = hasher.finish(); - - let prev_cursor = self.cursors.get(&thread.thread_id).copied().unwrap_or(0); - let buffer_len = thread.timings.len() as u64; - let buffer_start = thread.total_pushed.saturating_sub(buffer_len); - - let mut slice = if prev_cursor < buffer_start { - // Cursor fell behind the buffer — some entries were evicted. - // Return everything still in the buffer. - thread.timings.as_slice() - } else { - let skip = (prev_cursor - buffer_start) as usize; - &thread.timings[skip.min(thread.timings.len())..] - }; - - let cursor_advance = thread.total_pushed; - self.cursors.insert(thread.thread_id, cursor_advance); - - if slice.is_empty() { - continue; - } - - let new_timings = SerializedTaskTiming::convert(self.startup_time, slice); - - deltas.push(ThreadTimingsDelta { - thread_id: hashed_id, - thread_name: thread.thread_name, - new_timings, - }); - } - - deltas - } - - pub fn reset(&mut self) { - self.cursors.clear(); - } -} - -// Allow 16MiB of task timing entries. -// VecDeque grows by doubling its capacity when full, so keep this a power of 2 to avoid wasting -// memory. -#[cfg(feature = "profiler")] -const MAX_TASK_TIMINGS: usize = (16 * 1024 * 1024) / core::mem::size_of::(); - -#[doc(hidden)] -pub(crate) type TaskTimings = VecDeque; - -#[doc(hidden)] -pub type GuardedTaskTimings = spin::Mutex; - -#[doc(hidden)] -pub struct GlobalThreadTimings { - pub thread_id: ThreadId, - pub timings: std::sync::Weak, -} - -#[doc(hidden)] -#[derive(Debug, Clone)] -pub struct TaskStatistics { - pub poll_time_to_beat: Duration, - pub runtime_to_beat: Duration, - pub longest_poll_times: [TaskTiming; 5], - pub longest_runtimes: [TaskTiming; 5], -} - -impl std::fmt::Display for TaskStatistics { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("Tasks that blocked the longest before yielding\n")?; - for timing in self.longest_poll_times { - f.write_fmt(format_args!( - "{:<20} - {}:{}\n", - format!("{:?}", timing.poll_duration()), - timing.location.file(), - timing.location.column() - ))?; - } - f.write_str("Tasks that ran the longest\n")?; - for timing in self.longest_runtimes { - f.write_fmt(format_args!( - "{:<20} - {}:{}\n", - format!("{:?}", timing.since_spawn()), - timing.location.file(), - timing.location.column() - ))?; - } - Ok(()) - } -} - -impl Default for TaskStatistics { - fn default() -> Self { - Self { - // Do not track polls that are not problematic - // this keeps more calls on the fast path - poll_time_to_beat: Duration::from_micros(100), - runtime_to_beat: Duration::from_micros(100), - longest_poll_times: [TaskTiming::placeholder(); 5], - longest_runtimes: [TaskTiming::placeholder(); 5], - } - } -} - -impl TaskStatistics { - #[inline(always)] - fn add_yield_timing(&mut self, task: TaskTiming) { - let yielded_after = task.poll_duration(); - if yielded_after >= self.poll_time_to_beat { - std::hint::cold_path(); // most tasks are not the worst, optimize for that - let to_replace = self - .longest_poll_times - .iter() - .position_min_by_key(|task| task.since_spawn()) - .expect("guarded by the comparison with nth_longest_yield_time"); - self.longest_poll_times[to_replace] = task; - - self.poll_time_to_beat = self - .longest_poll_times - .iter() - .map(|task| task.since_spawn()) - .min() - .expect("never empty"); - } - } - - #[inline(always)] - fn add_runtime(&mut self, task: TaskTiming) { - let runtime = task.since_spawn(); - if runtime >= self.runtime_to_beat { - std::hint::cold_path(); // most tasks are not the worst, optimize for that - let to_replace = self - .longest_runtimes - .iter() - .position_min_by_key(|task| task.since_spawn()) - .expect("guarded by the comparison with nth_longest_yield_time"); - self.longest_runtimes[to_replace] = task; - - self.runtime_to_beat = self - .longest_runtimes - .iter() - .map(|task| task.since_spawn()) - .min() - .expect("never empty"); - } - } -} - -#[doc(hidden)] -pub static GLOBAL_THREAD_TIMINGS: spin::Mutex> = - spin::Mutex::new(Vec::new()); - -/// Upgrades all live per-thread timing handles, holding the global registry -/// lock only for the duration of the upgrades. -/// -/// The upgraded `Arc`s must never be dropped while `GLOBAL_THREAD_TIMINGS` is -/// locked: dropping the last strong reference runs [`ThreadTimings::drop`], -/// which locks `GLOBAL_THREAD_TIMINGS` again and would deadlock the -/// non-reentrant spinlock. A thread exiting concurrently can hand off its last -/// reference to us at any time, so callers of this function process (lock, -/// read, drop) the returned handles only after the global lock is released. -fn upgraded_thread_timings() -> Vec<(ThreadId, Arc)> { - let global_thread_timings = GLOBAL_THREAD_TIMINGS.lock(); - global_thread_timings - .iter() - .filter_map(|t| Some((t.thread_id, t.timings.upgrade()?))) - .collect() -} - -thread_local! { - #[doc(hidden)] - pub static THREAD_TIMINGS: LazyCell> = LazyCell::new(|| { - let current_thread = std::thread::current(); - let thread_name = current_thread.name(); - let thread_id = current_thread.id(); - let timings = ThreadTimings::new(thread_name.map(|e| e.to_string()), thread_id); - let timings = Arc::new(spin::Mutex::new(timings)); - - { - let timings = Arc::downgrade(&timings); - let global_timings = GlobalThreadTimings { - thread_id: std::thread::current().id(), - timings, - }; - GLOBAL_THREAD_TIMINGS.lock().push(global_timings); - } - - timings - }); -} - -#[doc(hidden)] -pub struct ThreadTimings { - pub thread_name: Option, - pub thread_id: ThreadId, - pub timings: TaskTimings, - pub running: Option, - pub stats: TaskStatistics, - pub total_pushed: u64, -} - -impl ThreadTimings { - pub fn new(thread_name: Option, thread_id: ThreadId) -> Self { - ThreadTimings { - thread_name, - thread_id, - timings: TaskTimings::new(), - stats: TaskStatistics::default(), - total_pushed: 0, - running: None, - } - } - - #[cfg(feature = "profiler")] - pub fn update_running_task( - &mut self, - spawned: SpawnTime, - location: &'static std::panic::Location<'_>, - ) { - let start = Instant::now(); - self.running = Some(ActiveTiming { - spawned, - location, - start, - }); - } - #[cfg(not(feature = "profiler"))] - pub fn update_running_task(&mut self, _: SpawnTime, _: &'static std::panic::Location<'_>) {} - - #[cfg(feature = "profiler")] - pub fn save_task_timing(&mut self, ended: YieldTime) -> TaskTiming { - let ActiveTiming { - location, - start, - spawned, - } = self - .running - .take() - .expect("this function is only ever called after register_task_start"); - - let timing = TaskTiming { - location, - spawned, - start, - end: ended, - }; - self.stats.add_yield_timing(timing); - self.stats.add_runtime(timing); - - if trace_enabled() { - std::hint::cold_path(); // optimize for when the profiling is off - if self.timings.len() >= MAX_TASK_TIMINGS { - self.timings.pop_front(); - } - self.timings.push_back(timing); - self.total_pushed += 1; - } - timing - } - #[cfg(not(feature = "profiler"))] - pub fn save_task_timing(&mut self, _: YieldTime) {} - - // Running tasks are included in the reliability trace, which is written - // whenever the foreground executor makes no progress for > n seconds - pub fn get_thread_task_timings(&self, includes: TasksIncluded) -> ThreadTaskTimings { - ThreadTaskTimings { - thread_name: self.thread_name.clone(), - thread_id: self.thread_id, - timings: self - .timings - .iter() - .cloned() - .chain( - self.running - .filter(|_| matches!(includes, TasksIncluded::CompletedAndRunning)) - .map(|running| TaskTiming { - spawned: running.spawned, - location: running.location, - start: running.start, - end: YieldTime(Instant::now()), - }), - ) - .collect(), - stats: self.stats.clone(), - total_pushed: self.total_pushed, - } - } -} - -impl Drop for ThreadTimings { - fn drop(&mut self) { - let mut thread_timings = GLOBAL_THREAD_TIMINGS.lock(); - - let Some((index, _)) = thread_timings - .iter() - .enumerate() - .find(|(_, t)| t.thread_id == self.thread_id) - else { - return; - }; - thread_timings.swap_remove(index); - } -} - -#[doc(hidden)] -pub fn update_running_task(spawned: SpawnTime, location: &'static std::panic::Location<'_>) { - #[cfg(feature = "profiler")] - journal::begin_foreground_turn(); - THREAD_TIMINGS.with(|timings| { - timings.lock().update_running_task(spawned, location); - }); -} - -#[doc(hidden)] -pub fn save_task_timing() { - let yielded_at = YieldTime(Instant::now()); - #[cfg(feature = "profiler")] - { - let timing = THREAD_TIMINGS.with(|timings| timings.lock().save_task_timing(yielded_at)); - journal::record_task_poll(timing); - } - #[cfg(not(feature = "profiler"))] - THREAD_TIMINGS.with(|timings| { - timings.lock().save_task_timing(yielded_at); - }); -} - -#[doc(hidden)] -pub fn get_current_thread_task_timings(include_running: TasksIncluded) -> ThreadTaskTimings { - THREAD_TIMINGS.with(|timings| timings.lock().get_thread_task_timings(include_running)) -} - -const TRACE_SETTING_ENABLED: u64 = 1 << 63; -const TRACE_SCOPE_COUNT_MASK: u64 = TRACE_SETTING_ENABLED - 1; -static TRACE_STATE: AtomicU64 = AtomicU64::new(0); - -/// Enables or disables profiler trace collection at runtime. -/// -/// When transitioning from enabled to disabled, `add_task_timing` becomes -/// cheaper since only cheap statistics are gathered. The existing per-thread -/// task buffers and the frame-event buffer are cleared so stale data isn't -/// reported after a later re-enable. Active trace scopes keep collection enabled -/// until the last scope ends. Calls with the current setting are a no-op. -pub fn set_trace_enabled(enabled: bool) -> bool { - let mut state = TRACE_STATE.load(Ordering::Acquire); - loop { - let was_enabled = state & TRACE_SETTING_ENABLED != 0; - if was_enabled == enabled { - return false; - } - - let next_state = if enabled { - state | TRACE_SETTING_ENABLED - } else { - state & TRACE_SCOPE_COUNT_MASK - }; - match TRACE_STATE.compare_exchange_weak( - state, - next_state, - Ordering::AcqRel, - Ordering::Acquire, - ) { - Ok(_) => { - if next_state == 0 { - clear_trace_buffers(); - } - return true; - } - Err(updated_state) => state = updated_state, - } - } -} - -#[cfg(any(feature = "bench-support", all(test, feature = "profiler")))] -pub(crate) struct TraceGuard; - -#[cfg(any(feature = "bench-support", all(test, feature = "profiler")))] -pub(crate) fn trace_scope() -> TraceGuard { - let incremented = TRACE_STATE.fetch_update(Ordering::AcqRel, Ordering::Acquire, |state| { - (state & TRACE_SCOPE_COUNT_MASK < TRACE_SCOPE_COUNT_MASK).then_some(state + 1) - }); - assert!(incremented.is_ok(), "too many active profiler trace scopes"); - TraceGuard -} - -#[cfg(any(feature = "bench-support", all(test, feature = "profiler")))] -impl Drop for TraceGuard { - fn drop(&mut self) { - let previous_state = - TRACE_STATE.fetch_update(Ordering::AcqRel, Ordering::Acquire, |state| { - (state & TRACE_SCOPE_COUNT_MASK > 0).then_some(state - 1) - }); - match previous_state { - Ok(1) => clear_trace_buffers(), - Ok(_) => {} - Err(_) => debug_assert!(false, "profiler trace scope count underflowed"), - } - } -} - -/// Returns whether profiler trace collection is enabled. -pub fn trace_enabled() -> bool { - TRACE_STATE.load(Ordering::Relaxed) != 0 -} - -fn clear_trace_buffers() { - for (_, timings) in upgraded_thread_timings() { - let mut timings = timings.lock(); - timings.timings.clear(); - timings.timings.shrink_to_fit(); - timings.total_pushed = 0; - } - #[cfg(feature = "profiler")] - { - let mut frames = FRAME_TIMINGS.lock(); - frames.timings.clear(); - frames.timings.shrink_to_fit(); - frames.total_pushed = 0; - } -} - -/// Timing for a single drawn window frame. -#[cfg(feature = "profiler")] -#[derive(Debug, Copy, Clone)] -pub struct FrameTiming { - /// The window that was drawn. - pub window_id: WindowId, - /// When the frame first became dirty (its first invalidation). `None` if - /// profiler tracing was not yet enabled when the invalidation occurred. - pub dirty_at: Option, - /// Number of invalidations coalesced into this frame. - pub invalidations: u64, - /// When `Window::draw` started. - pub draw_start: Instant, - /// When `Window::draw` finished. - pub draw_end: Instant, -} - -#[cfg(feature = "profiler")] -impl FrameTiming { - /// Time spent inside `Window::draw`. - pub fn draw_duration(&self) -> Duration { - self.draw_end.duration_since(self.draw_start) - } - - /// Time from the frame's first invalidation to the end of its draw, if the - /// first invalidation was observed. - pub fn dirty_to_draw_duration(&self) -> Option { - self.dirty_at - .map(|dirty_at| self.draw_end.duration_since(dirty_at)) - } -} - -/// Work spent submitting a window frame to the platform. -#[cfg(feature = "profiler")] -#[derive(Debug, Copy, Clone)] -pub struct PresentTiming { - /// The window whose frame was submitted. - pub window_id: WindowId, - /// When the platform submission began. - pub present_start: Instant, - /// When the platform submission completed. - pub present_end: Instant, - /// The interval since the previous newly drawn frame was submitted, when - /// both frames belong to an active animation. - pub animation_interval: Option, -} - -#[cfg(feature = "profiler")] -impl PresentTiming { - /// Time spent submitting the frame to the platform. - pub fn present_duration(&self) -> Duration { - self.present_end.duration_since(self.present_start) - } -} - -/// A frame event observed by the profiler. -#[cfg(feature = "profiler")] -#[derive(Debug, Copy, Clone)] -pub enum FrameEvent { - /// A window frame was drawn. - Draw(FrameTiming), - /// A newly drawn window frame was presented. - Present(PresentTiming), -} - -/// A point-in-time snapshot of the frame-duration histograms for a window, -/// suitable for external formatting. -#[cfg(feature = "profiler")] -#[derive(Clone)] -pub struct FrameDurationSnapshot { - /// Histogram of durations from the first invalidation through presentation, in nanoseconds. - pub dirty_to_present_histogram: Histogram, - /// Histogram of `Window::draw` durations, in nanoseconds. - pub draw_duration_histogram: Histogram, - /// Histogram of intervals between consecutively presented frames while the - /// window was animating, in nanoseconds. - pub present_interval_histogram: Histogram, -} - -/// A point-in-time snapshot of the input-latency histograms for a window, -/// suitable for external formatting. -#[cfg(feature = "profiler")] -#[derive(Clone)] -pub struct InputLatencySnapshot { - /// Histogram of input-to-frame latency samples, in nanoseconds. - pub latency_histogram: Histogram, - /// Histogram of input events coalesced per rendered frame. - pub events_per_frame_histogram: Histogram, - /// Count of input events that arrived mid-draw and were excluded from - /// latency recording. - pub mid_draw_events_dropped: u64, -} - -#[cfg(feature = "profiler")] -enum WindowActivity { - Input { - started_at: Instant, - kind: &'static str, - }, - Draw { - started_at: Instant, - }, -} - -/// Collects profiling information for one window. -/// -/// Aggregate histograms are always populated when the `profiler` feature is -/// compiled in. Individual draw and present events are added to the global -/// profiler buffer only while tracing is enabled. -#[cfg(feature = "profiler")] -pub struct WindowProfiler { - window_id: WindowId, - active_activities: SmallVec<[WindowActivity; 4]>, - active_actions: SmallVec<[(&'static str, Instant); 2]>, - dirty_to_present_histogram: Histogram, - draw_duration_histogram: Histogram, - present_interval_histogram: Histogram, - first_input_at: Option, - pending_input_count: u64, - input_latency_histogram: Histogram, - events_per_frame_histogram: Histogram, - mid_draw_events_dropped: u64, - last_present_at: Option, - animating_at_last_present: bool, - pending_frame: Option, -} - -#[cfg(feature = "profiler")] -impl WindowProfiler { - /// Creates a profiler for a window. - pub fn new(window_id: WindowId) -> anyhow::Result { - let profiler = Self { - window_id, - active_activities: SmallVec::new(), - active_actions: SmallVec::new(), - dirty_to_present_histogram: Histogram::new(3).map_err(|error| { - anyhow::anyhow!("Failed to create dirty-to-present histogram: {error}") - })?, - draw_duration_histogram: Histogram::new(3).map_err(|error| { - anyhow::anyhow!("Failed to create draw duration histogram: {error}") - })?, - present_interval_histogram: Histogram::new(3).map_err(|error| { - anyhow::anyhow!("Failed to create present interval histogram: {error}") - })?, - first_input_at: None, - pending_input_count: 0, - input_latency_histogram: Histogram::new(3).map_err(|error| { - anyhow::anyhow!("Failed to create input latency histogram: {error}") - })?, - events_per_frame_histogram: Histogram::new(3).map_err(|error| { - anyhow::anyhow!("Failed to create events per frame histogram: {error}") - })?, - mid_draw_events_dropped: 0, - last_present_at: None, - animating_at_last_present: false, - pending_frame: None, - }; - journal::record_frame_pending(window_id, Instant::now()); - Ok(profiler) - } - - /// Records the beginning of an input dispatch. `kind` names the platform - /// input variant being dispatched (see [`crate::PlatformInput::kind_name`]). - pub fn begin_input(&mut self, kind: &'static str) { - journal::begin_foreground_turn(); - self.active_activities.push(WindowActivity::Input { - started_at: Instant::now(), - kind, - }); - } - - /// Records the end of an input dispatch. - pub fn end_input(&mut self, caused_invalidation: bool) { - let Some(WindowActivity::Input { started_at, kind }) = self.active_activities.pop() else { - debug_assert!(false, "input activity must be the current window activity"); - journal::end_foreground_turn(); - return; - }; - - journal::record_input(journal::InputTiming { - kind, - start: started_at, - end: Instant::now(), - caused_invalidation, - }); - journal::end_foreground_turn(); - - if !caused_invalidation { - return; - } - - let arrived_during_draw = self - .active_activities - .iter() - .any(|activity| matches!(activity, WindowActivity::Draw { .. })); - if arrived_during_draw { - self.mid_draw_events_dropped += 1; - } else { - self.first_input_at.get_or_insert(started_at); - self.pending_input_count += 1; - } - } - - /// Records the beginning of an action handler. - pub fn begin_action_handler(&mut self, action: &(dyn Action + 'static), cx: &mut App) { - journal::begin_foreground_turn(); - let name = actions::update_running_action(action, cx); - self.active_actions.push((name, Instant::now())); - } - - /// Records the end of the current action handler. - pub fn end_action_handler(&mut self) { - // Dual-write to the legacy aggregate store; its single global running - // slot misbehaves when tests run actions concurrently, which is why - // the journal entry is tracked here on the window instead. - actions::save_action_timing(); - let Some((name, start)) = self.active_actions.pop() else { - debug_assert!(false, "action handler must be begun before it ends"); - journal::end_foreground_turn(); - return; - }; - journal::record_action(ActionTiming { - name, - start, - end: Instant::now(), - }); - journal::end_foreground_turn(); - } - - /// Records the beginning of a window draw. - pub fn begin_draw(&mut self) { - journal::begin_foreground_turn(); - let started_at = Instant::now(); - journal::record_frame_pending(self.window_id, started_at); - self.active_activities - .push(WindowActivity::Draw { started_at }); - } - - /// Records the end of a window draw and returns the draw duration. - pub fn end_draw(&mut self, dirty_at: Option, invalidations: u64) -> Duration { - let Some(WindowActivity::Draw { - started_at: draw_start, - }) = self.active_activities.pop() - else { - debug_assert!(false, "draw activity must be the current window activity"); - journal::end_foreground_turn(); - return Duration::ZERO; - }; - - let draw_end = Instant::now(); - let frame_timing = FrameTiming { - window_id: self.window_id, - dirty_at, - invalidations, - draw_start, - draw_end, - }; - let draw_duration = frame_timing.draw_duration(); - self.record_draw_timing(frame_timing); - journal::end_foreground_turn(); - draw_duration - } - - /// Records that a frame was presented. - /// - /// `next_frame_scheduled` marks the animation state for the interval ending - /// at the next newly drawn frame's presentation. - pub fn record_present( - &mut self, - present_start: Instant, - present_end: Instant, - window_active: bool, - next_frame_scheduled: bool, - ) { - self.record_present_at( - present_start, - present_end, - window_active, - next_frame_scheduled, - ); - } - - /// Returns a snapshot of the current input-latency histograms. - pub fn input_latency_snapshot(&self) -> InputLatencySnapshot { - InputLatencySnapshot { - latency_histogram: self.input_latency_histogram.clone(), - events_per_frame_histogram: self.events_per_frame_histogram.clone(), - mid_draw_events_dropped: self.mid_draw_events_dropped, - } - } - - /// Returns a snapshot of the current frame-duration histograms. - pub fn frame_duration_snapshot(&self) -> FrameDurationSnapshot { - FrameDurationSnapshot { - dirty_to_present_histogram: self.dirty_to_present_histogram.clone(), - draw_duration_histogram: self.draw_duration_histogram.clone(), - present_interval_histogram: self.present_interval_histogram.clone(), - } - } - - fn record_present_at( - &mut self, - present_start: Instant, - present_end: Instant, - window_active: bool, - next_frame_scheduled: bool, - ) { - if let Some(first_input_at) = self.first_input_at.take() { - let latency_nanos = present_end.duration_since(first_input_at).as_nanos() as u64; - self.input_latency_histogram.record(latency_nanos).ok(); - } - if self.pending_input_count > 0 { - self.events_per_frame_histogram - .record(self.pending_input_count) - .ok(); - self.pending_input_count = 0; - } - - let frame = self.pending_frame.take(); - let animation_interval = - if frame.is_some() && self.animating_at_last_present && window_active { - self.last_present_at - .map(|last_present_at| present_end.duration_since(last_present_at)) - } else { - None - }; - let present_timing = PresentTiming { - window_id: self.window_id, - present_start, - present_end, - animation_interval, - }; - journal::record_present(present_timing, frame); - - let Some(frame) = frame else { - return; - }; - - if let Some(dirty_at) = frame.dirty_at - && let Err(error) = self - .dirty_to_present_histogram - .record(present_end.duration_since(dirty_at).as_nanos() as u64) - { - log::error!("failed to record dirty-to-present frame timing: {error}"); - } - - if let Some(animation_interval) = animation_interval { - self.present_interval_histogram - .record(animation_interval.as_nanos() as u64) - .ok(); - } - record_frame_event(FrameEvent::Present(present_timing)); - - self.last_present_at = Some(present_end); - self.animating_at_last_present = next_frame_scheduled && window_active; - } - - fn record_draw_timing(&mut self, timing: FrameTiming) { - self.record_draw_duration(timing.draw_duration()); - self.pending_frame = Some(timing); - record_frame_event(FrameEvent::Draw(timing)); - journal::record_draw(timing); - } - - fn record_draw_duration(&mut self, duration: Duration) { - self.draw_duration_histogram - .record(duration.as_nanos() as u64) - .ok(); - } -} - -#[cfg(feature = "profiler")] -impl Drop for WindowProfiler { - fn drop(&mut self) { - journal::record_window_closed(self.window_id); - } -} - -// Allow 16MiB of frame event entries. -#[cfg(feature = "profiler")] -const MAX_FRAME_TIMINGS: usize = (16 * 1024 * 1024) / core::mem::size_of::(); - -#[cfg(feature = "profiler")] -struct FrameTimings { - timings: VecDeque, - total_pushed: u64, -} - -#[cfg(feature = "profiler")] -static FRAME_TIMINGS: spin::Mutex = spin::Mutex::new(FrameTimings { - timings: VecDeque::new(), - total_pushed: 0, -}); - -/// Records a frame event. -/// -/// No-op unless profiler tracing is enabled via [`set_trace_enabled`]. -#[cfg(feature = "profiler")] -pub fn record_frame_event(event: FrameEvent) { - if !trace_enabled() { - return; - } - std::hint::cold_path(); // optimize for when profiling is off - - let mut frames = FRAME_TIMINGS.lock(); - if frames.timings.len() >= MAX_FRAME_TIMINGS { - frames.timings.pop_front(); - } - frames.timings.push_back(event); - frames.total_pushed += 1; -} - -/// Drains frame events recorded after this collector was created, tracking a -/// cursor so each call to [`Self::collect_unseen`] returns only new entries. -#[cfg(feature = "profiler")] -pub struct FrameTimingCollector { - cursor: u64, -} - -#[cfg(feature = "profiler")] -impl Default for FrameTimingCollector { - fn default() -> Self { - Self::new() - } -} - -#[cfg(feature = "profiler")] -impl FrameTimingCollector { - /// Creates a collector that only sees frame events recorded from this point on. - pub fn new() -> Self { - Self { - cursor: FRAME_TIMINGS.lock().total_pushed, - } - } - - /// Returns frame events recorded since the previous call (or since the - /// collector was created). If the ring buffer wrapped around since the - /// previous poll, the evicted entries are lost. - pub fn collect_unseen(&mut self) -> Vec { - let frames = FRAME_TIMINGS.lock(); - let buffer_len = frames.timings.len() as u64; - let buffer_start = frames.total_pushed.saturating_sub(buffer_len); - let skip = self.cursor.saturating_sub(buffer_start) as usize; - let unseen = frames - .timings - .iter() - .skip(skip.min(frames.timings.len())) - .copied() - .collect(); - self.cursor = frames.total_pushed; - unseen - } -} - -#[cfg(all(test, feature = "profiler"))] -mod tests { - use super::*; - use std::sync::{Mutex, MutexGuard}; - - #[test] - fn records_draw_events_only_while_tracing() { - let _trace_test_guard = TraceTestGuard::new(); - let window_id = WindowId::from(0xD0A0); - let mut window_profiler = - WindowProfiler::new(window_id).expect("window profiler should initialize"); - let dirty_at = Instant::now(); - let mut collector = FrameTimingCollector::new(); - - window_profiler.begin_draw(); - window_profiler.end_draw(Some(dirty_at), 3); - assert!( - collector - .collect_unseen() - .iter() - .all(|event| !event_matches_window(*event, window_id)) - ); - - set_trace_enabled(true); - let mut collector = FrameTimingCollector::new(); - window_profiler.begin_draw(); - window_profiler.end_draw(Some(dirty_at), 3); - - let timing = collector - .collect_unseen() - .into_iter() - .find_map(|event| match event { - FrameEvent::Draw(timing) if timing.window_id == window_id => Some(timing), - _ => None, - }) - .expect("draw event should be recorded while tracing"); - assert_eq!(timing.dirty_at, Some(dirty_at)); - assert_eq!(timing.invalidations, 3); - assert!(timing.draw_start >= dirty_at); - } - - #[test] - fn records_present_events_for_newly_drawn_frames() { - let _trace_test_guard = TraceTestGuard::new(); - set_trace_enabled(true); - let window_id = WindowId::from(0xA11E); - let mut window_profiler = - WindowProfiler::new(window_id).expect("window profiler should initialize"); - let start = Instant::now(); - let mut collector = FrameTimingCollector::new(); - - record_test_draw(&mut window_profiler, start); - window_profiler.record_present_at(start, start, true, true); - record_test_draw(&mut window_profiler, start + FRAME); - window_profiler.record_present_at(start + FRAME, start + FRAME, true, true); - window_profiler.record_present_at( - start + FRAME + FRAME / 2, - start + FRAME + FRAME / 2, - true, - true, - ); - - let present_timings = collector - .collect_unseen() - .into_iter() - .filter_map(|event| match event { - FrameEvent::Present(timing) if timing.window_id == window_id => Some(timing), - _ => None, - }) - .collect::>(); - let [first_present, second_present] = present_timings.as_slice() else { - panic!("expected exactly two present events, got {present_timings:?}"); - }; - assert_eq!(first_present.animation_interval, None); - assert_eq!(second_present.animation_interval, Some(FRAME)); - - #[cfg(feature = "profiler")] - { - assert_eq!(window_profiler.present_interval_histogram.len(), 1); - assert!( - window_profiler.present_interval_histogram.max() - >= second_present - .animation_interval - .expect("second present should have an animation interval") - .as_nanos() as u64 - ); - } - } - - #[test] - fn disabling_tracing_clears_frame_events() { - let _trace_test_guard = TraceTestGuard::new(); - set_trace_enabled(true); - let window_id = WindowId::from(0xC1EA); - let mut window_profiler = - WindowProfiler::new(window_id).expect("window profiler should initialize"); - let mut collector = FrameTimingCollector::new(); - - window_profiler.begin_draw(); - window_profiler.end_draw(None, 0); - assert!( - FRAME_TIMINGS - .lock() - .timings - .iter() - .copied() - .any(|event| event_matches_window(event, window_id)) - ); - - set_trace_enabled(false); - assert!( - collector - .collect_unseen() - .iter() - .all(|event| !event_matches_window(*event, window_id)) - ); - } - - #[cfg(feature = "profiler")] - #[test] - fn records_intervals_only_between_animation_frames() { - let mut window_profiler = - WindowProfiler::new(WindowId::from(1)).expect("window profiler should initialize"); - let start = Instant::now(); - - draw_and_present(&mut window_profiler, start, true, true); - assert_eq!(window_profiler.present_interval_histogram.len(), 0); - - draw_and_present(&mut window_profiler, start + FRAME, true, true); - assert_eq!(window_profiler.present_interval_histogram.len(), 1); - - draw_and_present(&mut window_profiler, start + FRAME * 2, true, false); - assert_eq!(window_profiler.present_interval_histogram.len(), 2); - - draw_and_present(&mut window_profiler, start + FRAME * 100, true, true); - assert_eq!(window_profiler.present_interval_histogram.len(), 2); - } - - #[cfg(feature = "profiler")] - #[test] - fn missed_frames_stretch_the_recorded_interval() { - let mut window_profiler = - WindowProfiler::new(WindowId::from(2)).expect("window profiler should initialize"); - let start = Instant::now(); - - draw_and_present(&mut window_profiler, start, true, true); - draw_and_present(&mut window_profiler, start + FRAME * 5, true, true); - - let recorded = window_profiler.present_interval_histogram.max(); - assert!(recorded >= (FRAME * 4).as_nanos() as u64); - } - - #[cfg(feature = "profiler")] - #[test] - fn ignores_re_presents_of_unchanged_frames() { - let mut window_profiler = - WindowProfiler::new(WindowId::from(3)).expect("window profiler should initialize"); - let start = Instant::now(); - - draw_and_present(&mut window_profiler, start, true, true); - window_profiler.record_present_at(start + FRAME / 2, start + FRAME / 2, true, true); - draw_and_present(&mut window_profiler, start + FRAME, true, true); - - assert_eq!(window_profiler.present_interval_histogram.len(), 1); - assert!( - window_profiler.present_interval_histogram.max() >= (FRAME * 3 / 4).as_nanos() as u64 - ); - } - - #[cfg(feature = "profiler")] - #[test] - fn skips_intervals_for_inactive_windows() { - let mut window_profiler = - WindowProfiler::new(WindowId::from(4)).expect("window profiler should initialize"); - let start = Instant::now(); - - draw_and_present(&mut window_profiler, start, false, true); - draw_and_present(&mut window_profiler, start + FRAME, false, true); - assert_eq!(window_profiler.present_interval_histogram.len(), 0); - - draw_and_present(&mut window_profiler, start + FRAME * 2, true, true); - assert_eq!(window_profiler.present_interval_histogram.len(), 0); - - draw_and_present(&mut window_profiler, start + FRAME * 3, true, true); - assert_eq!(window_profiler.present_interval_histogram.len(), 1); - } - - #[test] - fn records_dirty_to_present_durations() { - let mut window_profiler = - WindowProfiler::new(WindowId::from(8)).expect("window profiler should initialize"); - let draw_end = Instant::now(); - let present_end = draw_end + Duration::from_millis(6); - - record_test_draw(&mut window_profiler, draw_end); - window_profiler.record_present_at(present_end, present_end, true, false); - - let snapshot = window_profiler.frame_duration_snapshot(); - let histogram = snapshot.dirty_to_present_histogram; - assert_eq!(histogram.len(), 1); - assert!(histogram.max() >= Duration::from_millis(10).as_nanos() as u64); - } - - #[cfg(feature = "profiler")] - #[test] - fn records_every_draw_duration() { - let mut window_profiler = - WindowProfiler::new(WindowId::from(5)).expect("window profiler should initialize"); - - window_profiler.record_draw_duration(Duration::from_millis(2)); - window_profiler.record_draw_duration(Duration::from_millis(40)); - - let snapshot = window_profiler.frame_duration_snapshot(); - assert_eq!(snapshot.draw_duration_histogram.len(), 2); - assert!(snapshot.draw_duration_histogram.max() >= 39_000_000); - } - - #[test] - fn records_input_latency_at_the_frame_presentation_timestamp() { - let mut window_profiler = - WindowProfiler::new(WindowId::from(6)).expect("window profiler should initialize"); - let first_input_at = Instant::now(); - let presented_at = first_input_at + Duration::from_millis(12); - - begin_input_at(&mut window_profiler, first_input_at); - window_profiler.end_input(true); - begin_input_at( - &mut window_profiler, - first_input_at + Duration::from_millis(2), - ); - window_profiler.end_input(true); - record_test_draw(&mut window_profiler, presented_at); - window_profiler.record_present_at(presented_at, presented_at, true, false); - - let snapshot = window_profiler.input_latency_snapshot(); - assert_eq!(snapshot.latency_histogram.len(), 1); - assert!(snapshot.latency_histogram.max() >= Duration::from_millis(12).as_nanos() as u64); - assert_eq!(snapshot.events_per_frame_histogram.len(), 1); - assert_eq!(snapshot.events_per_frame_histogram.max(), 2); - assert_eq!(snapshot.mid_draw_events_dropped, 0); - } - - #[test] - fn excludes_input_that_arrives_during_a_draw() { - let mut window_profiler = - WindowProfiler::new(WindowId::from(7)).expect("window profiler should initialize"); - - window_profiler.begin_draw(); - begin_input_at(&mut window_profiler, Instant::now()); - window_profiler.end_input(true); - window_profiler.end_draw(None, 0); - - let snapshot = window_profiler.input_latency_snapshot(); - assert!(snapshot.latency_histogram.is_empty()); - assert!(snapshot.events_per_frame_histogram.is_empty()); - assert_eq!(snapshot.mid_draw_events_dropped, 1); - } - - #[test] - fn overlapping_trace_scopes_keep_tracing_enabled() { - let _trace_test_guard = TraceTestGuard::new(); - let first_scope = trace_scope(); - let second_scope = trace_scope(); - - assert!(trace_enabled()); - drop(first_scope); - assert!(trace_enabled()); - drop(second_scope); - assert!(!trace_enabled()); - } - - const FRAME: Duration = Duration::from_millis(16); - static TRACE_TEST_LOCK: Mutex<()> = Mutex::new(()); - - struct TraceTestGuard { - was_enabled: bool, - _lock: MutexGuard<'static, ()>, - } - - impl TraceTestGuard { - fn new() -> Self { - let lock = TRACE_TEST_LOCK - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - let was_enabled = trace_enabled(); - set_trace_enabled(false); - Self { - was_enabled, - _lock: lock, - } - } - } - - impl Drop for TraceTestGuard { - fn drop(&mut self) { - set_trace_enabled(false); - if self.was_enabled { - set_trace_enabled(true); - } - } - } - - fn event_matches_window(event: FrameEvent, window_id: WindowId) -> bool { - match event { - FrameEvent::Draw(timing) => timing.window_id == window_id, - FrameEvent::Present(timing) => timing.window_id == window_id, - } - } - - fn begin_input_at(window_profiler: &mut WindowProfiler, started_at: Instant) { - window_profiler - .active_activities - .push(WindowActivity::Input { - started_at, - kind: "test", - }); - } - - #[cfg(feature = "profiler")] - fn draw_and_present( - window_profiler: &mut WindowProfiler, - presented_at: Instant, - window_active: bool, - next_frame_scheduled: bool, - ) { - record_test_draw(window_profiler, presented_at); - window_profiler.record_present_at( - presented_at, - presented_at, - window_active, - next_frame_scheduled, - ); - } - - fn record_test_draw(window_profiler: &mut WindowProfiler, draw_end: Instant) { - window_profiler.record_draw_timing(FrameTiming { - window_id: window_profiler.window_id, - dirty_at: Some(draw_end - Duration::from_millis(4)), - invalidations: 1, - draw_start: draw_end - Duration::from_millis(2), - draw_end, - }); - } -} diff --git a/crates/gpui_pre/src/profiler/actions.rs b/crates/gpui_pre/src/profiler/actions.rs deleted file mode 100644 index a4892f8..0000000 --- a/crates/gpui_pre/src/profiler/actions.rs +++ /dev/null @@ -1,213 +0,0 @@ -use std::time::Duration; - -use itertools::Itertools; -use scheduler::Instant; - -#[cfg(feature = "profiler")] -use crate::action::Action; - -#[doc(hidden)] -#[derive(Clone)] -pub struct ActionStatistics { - runtime_to_beat: Duration, - - longest_runtimes: heapless::Vec, - running: Option<(&'static str, Instant)>, -} - -impl std::fmt::Debug for ActionStatistics { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ActionStatistics") - .field("runtime_to_beat", &self.runtime_to_beat) - .field("longest_runtimes", &self.longest_runtimes) - .field( - "running", - &self.running.map(|(id, started)| (id, started.elapsed())), - ) - .finish() - } -} - -impl std::fmt::Display for ActionStatistics { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("Actions that blocked the longest\n")?; - for action in self - .longest_runtimes(true) - .sorted_by_key(|action| action.runtime()) - .rev() - { - f.write_fmt(format_args!( - "{:<20} - {}", - format!("{:?}", action.runtime()), // impl dbg does not support alignment - action.name - ))?; - writeln!(f)?; - } - Ok(()) - } -} - -impl Default for ActionStatistics { - fn default() -> Self { - Self::new() - } -} - -impl ActionStatistics { - const fn new() -> Self { - Self { - // This keeps more calls on the fast path by only tracking - // problematic polls - runtime_to_beat: Duration::from_micros(100), - longest_runtimes: heapless::Vec::new(), - running: None, - } - } - - pub fn take(&mut self) -> Self { - let taken = std::mem::take(self); - self.running = taken.running; - taken - } - - pub fn is_empty(&self) -> bool { - self.longest_runtimes.is_empty() - } - - #[cfg(feature = "profiler")] - pub fn update_running_action(&mut self, action: &'static str, started: Instant) { - self.running = Some((action, started)); - } - #[cfg(not(feature = "profiler"))] - pub fn update_running_action(&mut self, _action: &'static str, _started: Instant) {} - - #[cfg(feature = "profiler")] - pub fn save_action_timing(&mut self) { - let now = Instant::now(); - - let Some((action, started)) = self.running.take() else { - // Actions are ran only on the foreground executor and therefore - // sequentially _except_ in tests where they can run concurrently. - // - // When ran sequentially self.running will always be Some. When ran - // concurrently that is no longer true. But that is fine, we do not - // need to track action timings in tests. - std::hint::cold_path(); - return; - }; - - let timing = ActionTiming { - name: action, - start: started, - end: now, - }; - - let runtime = now.duration_since(started); - if runtime >= self.runtime_to_beat { - std::hint::cold_path(); // most actions are not the worst, optimize for that - - if self.longest_runtimes.is_full() - && let Some(to_replace) = self - .longest_runtimes - .iter_mut() - .min_by_key(|action| runtime >= action.runtime()) - { - *to_replace = timing; - } else { - self.longest_runtimes - .push(timing) - .expect("just checked it is not full"); - }; - - self.runtime_to_beat = self - .longest_runtimes - .iter() - .map(|action| action.runtime()) - .min() - .expect("never empty"); - } - } - #[cfg(not(feature = "profiler"))] - pub fn save_action_timing(&mut self) {} - - pub fn longest_runtimes(&self, include_running: bool) -> impl Iterator { - self.longest_runtimes.iter().copied().chain( - self.running - .into_iter() - .filter(move |_| include_running) - .map(|(name, start)| ActionTiming { - name, - start, - end: Instant::now(), - }), - ) - } -} - -#[doc(hidden)] -/// UNSTABLE only for use in the profiler and zed-reliability -#[derive(Copy, Clone)] -pub struct ActionTiming { - pub name: &'static str, - pub start: Instant, - pub end: Instant, -} - -impl core::fmt::Debug for ActionTiming { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ActionTiming") - .field("name", &self.name) - .field("runtime", &self.runtime()) - .finish() - } -} - -impl ActionTiming { - pub fn duration(&self) -> Duration { - self.end.saturating_duration_since(self.start) - } -} - -impl ActionTiming { - #[doc(hidden)] - pub fn runtime(&self) -> Duration { - self.end - self.start - } -} - -// The profiler is careful to never block when the lock is held, therefore a -// spinlock is optimal. -#[cfg(feature = "profiler")] -static ACTION_STATISTICS: spin::Mutex = - const { spin::Mutex::new(ActionStatistics::new()) }; - -#[doc(hidden)] -#[cfg(feature = "profiler")] -pub(crate) fn update_running_action( - action: &(dyn Action + 'static), - cx: &mut crate::App, -) -> &'static str { - let now = Instant::now(); - let action = action.type_id(); - let action = cx.actions.try_resolve_action(&action).unwrap_or("un-named"); - ACTION_STATISTICS.lock().update_running_action(action, now); - action -} - -#[doc(hidden)] -#[cfg(feature = "profiler")] -pub(crate) fn save_action_timing() { - ACTION_STATISTICS.lock().save_action_timing(); -} - -#[doc(hidden)] -#[cfg(feature = "profiler")] -pub fn take_action_stats() -> ActionStatistics { - ACTION_STATISTICS.lock().take() -} - -#[doc(hidden)] -#[cfg(not(feature = "profiler"))] -pub fn take_action_stats() -> ActionStatistics { - ActionStatistics::default() -} diff --git a/crates/gpui_pre/src/profiler/hang.rs b/crates/gpui_pre/src/profiler/hang.rs deleted file mode 100644 index 7aea645..0000000 --- a/crates/gpui_pre/src/profiler/hang.rs +++ /dev/null @@ -1,1297 +0,0 @@ -//! Post-hoc detection of foreground hangs from the journal's event stream. -//! -//! A hang is any single piece of foreground work — a task poll, an action -//! handler, an input dispatch, a window draw, or platform presentation — that -//! blocked the foreground thread for at least a threshold duration. An -//! interval whose total foreground spend reached the frame budget also -//! counts, even when no single event crossed the threshold: many small -//! pieces of work can drop a frame — or starve a headless app — as -//! thoroughly as one long stall. [`HangDetector`] drains the journal and -//! reports each completed activity interval containing hangs as a -//! [`HangIncident`]. - -use std::time::Duration; - -use scheduler::Instant; -use serde::Serialize; - -use super::SerializedLocation; -use super::journal::{ - ForegroundEvent, ForegroundJournal, ForegroundJournalCollector, ForegroundJournalEntry, - FrameSnapshot, IntervalBoundary, IntervalSealer, -}; - -/// Detects foreground hangs by polling the journal. -/// -/// Detection is post-hoc: a hang is reported once an explicit presentation -/// or foreground-idle boundary completes its interval. Work that never -/// yields back to the foreground is not observed until it does. -pub struct HangDetector { - collector: ForegroundJournalCollector, - sealer: IntervalSealer, - threshold: Duration, - frame_budget: Duration, - first_present_at: Option, -} - -/// One sealed interval that contained at least one hang. -#[derive(Debug, Clone)] -pub struct HangIncident { - /// The interval the hangs occurred in, including all non-hang foreground - /// work recorded alongside them. - pub snapshot: FrameSnapshot, - /// Which detection rule qualified the interval. - pub trigger: HangTrigger, - /// For [`HangTrigger::Threshold`], the events that blocked the foreground - /// for at least the detector's threshold, longest first. For - /// [`HangTrigger::Budget`], no event crossed the threshold and this instead - /// holds every event in the interval, longest first. - pub contributors: Vec, -} - -/// The detection rule that qualified an interval as a [`HangIncident`]. -/// -/// Recorded explicitly so consumers can separate the two classes without -/// re-deriving them from `stall_ms`, which stops working whenever the -/// detector's thresholds change. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum HangTrigger { - /// A single event blocked the foreground for at least the hang threshold. - Threshold, - /// No single event crossed the threshold, but the interval's total - /// foreground spend reached the frame budget. - Budget, -} - -impl HangDetector { - /// Creates a detector reporting single events at or above `threshold` - /// and intervals whose total foreground spend reached `frame_budget`. - /// Only events recorded from this point on are observed. - pub fn new(journal: ForegroundJournal, threshold: Duration, frame_budget: Duration) -> Self { - Self { - collector: journal.collector(), - sealer: IntervalSealer::new(Instant::now()), - threshold, - frame_budget, - first_present_at: None, - } - } - - /// When the first newly drawn frame observed by this detector finished - /// platform submission. `None` until a presentation boundary is observed. - pub fn first_present_at(&self) -> Option { - self.first_present_at - } - - /// Drains newly recorded events and returns the incidents sealed since - /// the previous poll. - pub fn poll(&mut self) -> Vec { - let drained = self.collector.collect_unseen(); - if self.first_present_at.is_none() { - self.first_present_at = drained.entries.iter().find_map(|entry| match entry { - ForegroundJournalEntry::Boundary(IntervalBoundary::Presented(presented)) => { - Some(presented.presentation.present_end) - } - _ => None, - }); - } - self.sealer - .push_entries(drained.entries) - .into_iter() - .filter_map(|snapshot| { - HangIncident::detect(snapshot, self.threshold, self.frame_budget) - }) - .collect() - } -} - -/// A [`HangIncident`] in a telemetry-friendly form: timestamps and durations -/// in fractional milliseconds since app startup (microsecond precision), -/// locations as plain data, contributor count capped by the converter. -#[derive(Debug, Clone, Serialize)] -pub struct SerializedHangIncident { - /// `"startup"` when the active window began before the first observed - /// newly drawn frame finished platform submission (see - /// [`HangDetector::first_present_at`]), otherwise `"steady"`. - pub phase: &'static str, - /// `"threshold"` or `"budget"` (see [`HangTrigger`]). - pub trigger: HangTrigger, - /// When the incident's active window started, in milliseconds since app - /// startup: the sealing frame's first invalidation, or the earliest - /// contributor's start when nothing was pending a repaint. Foreground - /// idle time between the previous frame and the cause is excluded. - pub start_ms: f64, - /// Length of the active window in milliseconds: from the cause to the - /// seal. Exceeds `stall_ms` when several stalls piled up on one frame. - pub active_ms: f64, - /// The longest single block of foreground work, in milliseconds: the - /// best estimate of the freeze a user perceived. Below the hang - /// threshold (possibly zero) when the frame budget alone triggered the - /// incident. - pub stall_ms: f64, - /// For presentation-sealed incidents, how long the submitted frame had - /// been dirty, in milliseconds. - pub dirty_to_present_ms: Option, - /// What closed the incident: `"present"` or `"idle"`. This labels the - /// boundary, not the hang's cause — the cause is the first contributor. - pub sealed_by: &'static str, - /// Fraction of the active window the foreground spent working, - /// `0.0..=1.0`. Low values with a high `dirty_to_present_ms` indicate - /// throttling or scheduling delay rather than application work. - pub busy_fraction: f64, - /// Total events recorded in the interval (before the contributor cap). - pub event_count: usize, - /// Count of task polls below the journal's floor. - pub small_poll_count: u64, - /// Total duration of task polls below the journal's floor, in milliseconds. - pub small_poll_total_ms: f64, - /// Events lost to caps or ring overwrites. - pub dropped_events: u64, - /// Whether one or more journal entries were unavailable in the interval. - /// Threshold-qualified contributors remain valid, but cumulative budget - /// conclusions and boundary attribution are not trusted across the gap. - pub journal_discontinuous: bool, - /// The incident's contributors (see [`HangIncident::contributors`]) in - /// start order. The cap keeps the longest ones; `stall_ms` is always - /// among them. - pub contributors: Vec, - /// Contributors elided by the cap. - pub contributors_elided: usize, -} - -/// One hang contributor in serialized form. -/// -/// Foreground work nests: an input dispatch can synchronously draw a window, -/// a draw can poll a task. `depth` expresses that containment — a depth-1 -/// event's time is already inside some depth-0 event's duration, so summing -/// sibling durations across depths double-counts. -#[derive(Debug, Clone, Serialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum SerializedHangContributor { - /// A foreground task poll. - TaskPoll { - /// Where the task was spawned. - location: SerializedLocation, - /// When the poll started, in milliseconds since app startup. - start_ms: f64, - /// How long the poll blocked the foreground, in milliseconds. - duration_ms: f64, - /// How many other events in the interval contain this one. - depth: usize, - }, - /// An action handler. - Action { - /// The action's name. - name: &'static str, - /// When the handler started, in milliseconds since app startup. - start_ms: f64, - /// How long the handler ran, in milliseconds. - duration_ms: f64, - /// How many other events in the interval contain this one. - depth: usize, - }, - /// A platform input dispatch. - Input { - /// The platform input variant dispatched, e.g. `"key_down"`. - /// Named `input_kind` because `kind` is this enum's serde tag. - input_kind: &'static str, - /// When the dispatch started, in milliseconds since app startup. - start_ms: f64, - /// How long the dispatch ran, in milliseconds. - duration_ms: f64, - /// Whether handling the input invalidated a window. - caused_invalidation: bool, - /// How many other events in the interval contain this one. - depth: usize, - }, - /// A window draw. - Draw { - /// The window that was drawn. - window_id: u64, - /// When the draw started, in milliseconds since app startup. - start_ms: f64, - /// How long the draw took, in milliseconds. - duration_ms: f64, - /// Time from the frame's first invalidation to the end of its draw, - /// in milliseconds. - dirty_to_draw_ms: Option, - /// Invalidations coalesced into the frame. - invalidations: u64, - /// How many other events in the interval contain this one. - depth: usize, - }, - /// Work spent submitting a frame to the platform. - Present { - /// The window whose frame was submitted. - window_id: u64, - /// When submission began, in milliseconds since app startup. - start_ms: f64, - /// How long platform submission took, in milliseconds. - duration_ms: f64, - /// How many other events in the interval contain this one. - depth: usize, - }, -} - -/// Milliseconds with microsecond precision: keeps `dbg!`/JSON output short -/// (`115.954` rather than `115.95400000000001` or `115954`). -fn as_millis(duration: Duration) -> f64 { - duration.as_micros() as f64 / 1000.0 -} - -impl SerializedHangIncident { - /// Converts an incident, keeping at most `max_contributors` contributors. - /// `first_present_at` is the end of the first observed newly drawn frame's - /// platform submission (typically [`HangDetector::first_present_at`]); - /// incidents whose active window begins before it are tagged `"startup"`. - pub fn convert( - startup: Instant, - incident: &HangIncident, - max_contributors: usize, - first_present_at: Option, - ) -> Self { - let since_startup = - |instant: Instant| as_millis(instant.saturating_duration_since(startup)); - let snapshot = &incident.snapshot; - let (active_start, active_end) = incident.active_window(); - let active = active_end.duration_since(active_start); - let busy_fraction = if active.is_zero() { - 1.0 - } else { - snapshot - .occupancy_within(active_start, active_end) - .div_duration_f64(active) - .min(1.0) - }; - Self { - phase: match first_present_at { - Some(first_present_at) if active_start >= first_present_at => "steady", - _ => "startup", - }, - trigger: incident.trigger, - start_ms: since_startup(active_start), - active_ms: as_millis(active), - stall_ms: incident - .contributors - .first() - .map(|event| as_millis(event.duration())) - .unwrap_or(0.0), - dirty_to_present_ms: match snapshot.boundary { - IntervalBoundary::Presented(presented) => { - presented.dirty_to_present_duration().map(as_millis) - } - IntervalBoundary::Idle { .. } => None, - }, - sealed_by: match snapshot.boundary { - IntervalBoundary::Presented(_) => "present", - IntervalBoundary::Idle { .. } => "idle", - }, - busy_fraction: (busy_fraction * 1000.0).round() / 1000.0, - event_count: snapshot.events.len(), - small_poll_count: snapshot.small_poll_summary().count, - small_poll_total_ms: as_millis(snapshot.small_poll_summary().total), - dropped_events: snapshot.dropped_events, - journal_discontinuous: snapshot.journal_discontinuous, - contributors: { - let mut kept: Vec<&ForegroundEvent> = incident - .contributors - .iter() - .take(max_contributors) - .collect(); - kept.sort_by_key(|event| event.start_time()); - kept.into_iter() - .map(|event| { - SerializedHangContributor::convert( - startup, - event, - nesting_depth(event, &snapshot.events), - ) - }) - .collect() - }, - contributors_elided: incident.contributors.len().saturating_sub(max_contributors), - } - } -} - -/// How many events in `events` strictly contain `event`'s span. Events with -/// identical spans don't count as containing each other, so `event`'s own -/// presence in `events` contributes nothing. -fn nesting_depth(event: &ForegroundEvent, events: &[ForegroundEvent]) -> usize { - let (start, end) = (event.start_time(), event.end_time()); - events - .iter() - .filter(|other| { - let (other_start, other_end) = (other.start_time(), other.end_time()); - other_start <= start && end <= other_end && (other_start < start || end < other_end) - }) - .count() -} - -impl SerializedHangContributor { - fn convert(startup: Instant, event: &ForegroundEvent, depth: usize) -> Self { - let since_startup = - |instant: Instant| as_millis(instant.saturating_duration_since(startup)); - let duration_ms = as_millis(event.duration()); - match event { - ForegroundEvent::TaskPoll(timing) => Self::TaskPoll { - location: timing.location.into(), - start_ms: since_startup(timing.start), - duration_ms, - depth, - }, - ForegroundEvent::Action(timing) => Self::Action { - name: timing.name, - start_ms: since_startup(timing.start), - duration_ms, - depth, - }, - ForegroundEvent::Input(timing) => Self::Input { - input_kind: timing.kind, - start_ms: since_startup(timing.start), - duration_ms, - caused_invalidation: timing.caused_invalidation, - depth, - }, - ForegroundEvent::Draw(timing) => Self::Draw { - window_id: timing.window_id.as_u64(), - start_ms: since_startup(timing.draw_start), - duration_ms, - dirty_to_draw_ms: timing.dirty_to_draw_duration().map(as_millis), - invalidations: timing.invalidations, - depth, - }, - ForegroundEvent::Present(timing) => Self::Present { - window_id: timing.window_id.as_u64(), - start_ms: since_startup(timing.present_start), - duration_ms, - depth, - }, - ForegroundEvent::SmallPolls(flush) => { - // The sealer folds these out of snapshot events; a contributor - // can therefore never be one, but serialize defensively as an - // unnamed poll spanning the flush rather than panicking. - Self::TaskPoll { - location: SerializedLocation { - file: "".into(), - line: 0, - column: 0, - }, - start_ms: since_startup(flush.since), - duration_ms, - depth, - } - } - } - } -} - -impl HangIncident { - /// The incident's reporting window: from its earliest cause — the - /// sealing frame's first invalidation, or the earliest contributor's - /// start when no repaint was pending — to the seal. This trims - /// foreground-idle time between the previous frame and the cause, and - /// may begin before the underlying snapshot when a contributor was - /// already running at the previous seal. - pub fn active_window(&self) -> (Instant, Instant) { - let snapshot = &self.snapshot; - let dirty_at = snapshot.boundary.dirty_at(); - let earliest_contributor = self - .contributors - .iter() - .map(|event| event.start_time()) - .min(); - let start = [dirty_at, earliest_contributor] - .into_iter() - .flatten() - .min() - .unwrap_or(snapshot.interval_start); - (start, snapshot.interval_end()) - } - - /// Returns an incident when the snapshot contains at least one event - /// that blocked the foreground for `threshold` or longer, or when the - /// interval's total foreground spend — event time plus folded - /// small-poll time — reached `frame_budget` even though no single event - /// did. In the latter case every event in the interval becomes a - /// contributor, since no single stall explains the busy interval. - pub fn detect( - snapshot: FrameSnapshot, - threshold: Duration, - frame_budget: Duration, - ) -> Option { - let mut contributors: Vec = snapshot - .events - .iter() - .filter(|event| event.duration() >= threshold) - .copied() - .collect(); - let trigger = if contributors.is_empty() { - if snapshot.journal_discontinuous { - return None; - } - let spend = snapshot.occupancy_within(snapshot.interval_start, snapshot.interval_end()); - if spend < frame_budget { - return None; - } - contributors = snapshot.events.clone(); - HangTrigger::Budget - } else { - HangTrigger::Threshold - }; - contributors.sort_by_key(|event| std::cmp::Reverse(event.duration())); - Some(Self { - snapshot, - trigger, - contributors, - }) - } -} - -#[cfg(test)] -mod tests { - use std::cell::Cell; - use std::rc::Rc; - use std::thread; - use std::time::Duration; - - use proptest::prelude::*; - use rand::prelude::*; - use scheduler::SpawnTime; - - use crate::{ - self as gpui, Context, FocusHandle, InteractiveElement, IntoElement, Modifiers, - MouseButton, Render, Styled, TestAppContext, VisualTestContext, Window, WindowId, div, - point, px, - }; - - use crate::profiler::{ActionTiming, FrameTiming, PresentTiming, TaskTiming, YieldTime}; - - use super::super::journal::{ - FRAME_DEADLINE, ForegroundEvent, ForegroundJournalEntry, FrameSnapshot, FrameStateChange, - InputTiming, IntervalBoundary, PollSummary, PresentedFrame, SmallPollFlush, - install_test_foreground_journal, record_present, - }; - use super::{ - HangDetector, HangIncident, HangTrigger, SerializedHangContributor, SerializedHangIncident, - }; - - actions!(hang_test, [HangyAction]); - - // Well above legitimate per-event work in a test app (layout of one div, - // empty polls), well below the injected hangs. - const HANG_THRESHOLD: Duration = Duration::from_millis(10); - // Equal to the threshold, mirroring how production wires the detector - // today. - const FRAME_BUDGET: Duration = HANG_THRESHOLD; - - /// A hang that outlives the frame deadline stays in one incident with - /// the frame it starved: the deadline only unblocks idle boundaries, so - /// the eventual presentation seals the hang together with its - /// dirty-to-present association. - #[test] - fn a_hang_outliving_the_frame_deadline_keeps_its_frame_association() { - let start = scheduler::Instant::now(); - let window_id = WindowId::from(0x51E17); - let hang_end = start + FRAME_DEADLINE * 5; - let presented_at = hang_end + Duration::from_millis(16); - let (journal, _journal_guard) = install_test_foreground_journal(64, 8); - let mut detector = HangDetector::new(journal, HANG_THRESHOLD, FRAME_BUDGET); - - let snapshots = detector.sealer.push_entries([ - ForegroundJournalEntry::FrameState(FrameStateChange::Pending { - window_id, - dirty_at: start, - }), - ForegroundJournalEntry::Event(task_poll_event(start, hang_end)), - ]); - assert!(snapshots.is_empty(), "nothing seals mid-hang"); - - let snapshots = detector - .sealer - .push_entries([ForegroundJournalEntry::Boundary( - IntervalBoundary::Presented(PresentedFrame { - frame: frame(window_id, presented_at), - presentation: PresentTiming { - window_id, - present_start: presented_at - Duration::from_millis(1), - present_end: presented_at, - animation_interval: None, - }, - }), - )]); - let [snapshot] = snapshots.as_slice() else { - panic!("expected one presented snapshot, got {snapshots:?}"); - }; - let incident = HangIncident::detect(snapshot.clone(), HANG_THRESHOLD, FRAME_BUDGET) - .expect("the hang qualifies"); - assert!(matches!( - incident.contributors[0], - ForegroundEvent::TaskPoll(timing) if timing.end.0 == hang_end - )); - assert!(matches!( - incident.snapshot.boundary, - IntervalBoundary::Presented(_) - )); - } - - #[test] - fn serialized_incident_reports_presented_seal_fields() { - let startup = scheduler::Instant::now(); - let at = |ms: u64| startup + Duration::from_millis(ms); - let window_id = WindowId::from(0xF1E1D); - - let presentation = PresentTiming { - window_id, - present_start: at(380), - present_end: at(400), - animation_interval: None, - }; - let frame = FrameTiming { - window_id, - dirty_at: Some(at(100)), - invalidations: 3, - draw_start: at(350), - draw_end: at(380), - }; - let snapshot = FrameSnapshot { - interval_start: at(150), - boundary: IntervalBoundary::Presented(PresentedFrame { - frame, - presentation, - }), - events: vec![ - task_poll_event(at(150), at(300)), - ForegroundEvent::Action(ActionTiming { - name: "test::SlowAction", - start: at(300), - end: at(340), - }), - ForegroundEvent::Input(InputTiming { - kind: "test", - start: at(340), - end: at(345), - caused_invalidation: false, - }), - ForegroundEvent::Present(presentation), - ], - small_polls: vec![SmallPollFlush { - summary: PollSummary { - count: 2, - total: Duration::from_millis(1), - }, - since: at(105), - until: at(145), - }], - dropped_events: 0, - journal_discontinuous: false, - }; - - let incident = - HangIncident::detect(snapshot, HANG_THRESHOLD, FRAME_BUDGET).expect("has contributors"); - let serialized = SerializedHangIncident::convert(startup, &incident, 1, Some(at(50))); - - assert_eq!(serialized.phase, "steady"); - assert_eq!(serialized.trigger, HangTrigger::Threshold); - // The frame's first invalidation anchors the active window, not the - // interval start or the first contributor. - assert_eq!(serialized.start_ms, 100.0); - assert_eq!(serialized.active_ms, 300.0); - assert_eq!(serialized.stall_ms, 150.0); - assert_eq!(serialized.dirty_to_present_ms, Some(300.0)); - assert_eq!(serialized.sealed_by, "present"); - // Occupancy: poll 150ms + action 40ms + input 5ms + present 20ms + - // folded polls 1ms = 216ms of the 300ms window. - assert_eq!(serialized.busy_fraction, 0.72); - assert_eq!(serialized.event_count, 4); - assert_eq!(serialized.small_poll_count, 2); - assert_eq!(serialized.small_poll_total_ms, 1.0); - assert_eq!(serialized.dropped_events, 0); - // Three contributors qualify (poll, action, present); the cap keeps - // the longest and counts the rest. - assert_eq!(serialized.contributors.len(), 1); - assert_eq!(serialized.contributors_elided, 2); - assert!(matches!( - serialized.contributors[0], - SerializedHangContributor::TaskPoll { - start_ms, - duration_ms, - .. - } if start_ms == 150.0 && duration_ms == 150.0 - )); - } - - #[test] - fn serialized_incident_reports_idle_seal_fields() { - let startup = scheduler::Instant::now(); - let at = |ms: u64| startup + Duration::from_millis(ms); - - let idle = FrameSnapshot { - interval_start: at(200), - boundary: IntervalBoundary::Idle { ended_at: at(260) }, - events: vec![task_poll_event(at(200), at(260))], - small_polls: Vec::new(), - dropped_events: 0, - journal_discontinuous: false, - }; - let incident = - HangIncident::detect(idle, HANG_THRESHOLD, FRAME_BUDGET).expect("has contributors"); - let serialized = SerializedHangIncident::convert(startup, &incident, 8, None); - assert_eq!(serialized.phase, "startup"); - assert_eq!(serialized.sealed_by, "idle"); - assert_eq!(serialized.dirty_to_present_ms, None); - // With no frame, the earliest contributor anchors the active window. - assert_eq!(serialized.start_ms, 200.0); - assert_eq!(serialized.active_ms, 60.0); - assert_eq!(serialized.stall_ms, 60.0); - assert_eq!(serialized.busy_fraction, 1.0); - } - - #[test] - fn phase_is_startup_until_the_first_present() { - let startup = scheduler::Instant::now(); - let at = |ms: u64| startup + Duration::from_millis(ms); - let snapshot = FrameSnapshot { - interval_start: at(500), - boundary: IntervalBoundary::Idle { ended_at: at(600) }, - events: vec![task_poll_event(at(500), at(600))], - small_polls: Vec::new(), - dropped_events: 0, - journal_discontinuous: false, - }; - let incident = - HangIncident::detect(snapshot, HANG_THRESHOLD, FRAME_BUDGET).expect("has contributors"); - - let phase = |first_present_at| { - SerializedHangIncident::convert(startup, &incident, 8, first_present_at).phase - }; - assert_eq!(phase(None), "startup", "nothing has been presented yet"); - assert_eq!( - phase(Some(at(700))), - "startup", - "the incident began before the first presentation" - ); - assert_eq!( - phase(Some(at(500))), - "steady", - "an incident beginning exactly at first presentation is steady" - ); - assert_eq!(phase(Some(at(100))), "steady"); - } - - /// The detector must latch the first presentation it observes and never - /// move it. - #[test] - fn first_present_at_latches_on_the_first_observed_presentation() { - let (journal, _journal_guard) = install_test_foreground_journal(64, 8); - let mut detector = HangDetector::new(journal, HANG_THRESHOLD, FRAME_BUDGET); - let window_id = WindowId::from(0x1A7C4); - - let first_present_end = scheduler::Instant::now(); - record_present( - presentation(window_id, first_present_end), - Some(frame(window_id, first_present_end)), - ); - detector.poll(); - let latched = detector - .first_present_at() - .expect("a presentation was recorded"); - assert_eq!(latched, first_present_end); - - record_present( - presentation(window_id, first_present_end + Duration::from_millis(16)), - Some(frame( - window_id, - first_present_end + Duration::from_millis(16), - )), - ); - detector.poll(); - assert_eq!(detector.first_present_at(), Some(latched)); - } - - /// The only work inside the 100ms active window here is the 50ms poll, - /// so the busy fraction is 0.5: the folded polls' flush span lies - /// entirely before the window and must contribute nothing to it - /// (PR #62779 review finding 5). - #[test] - fn busy_fraction_excludes_small_polls_outside_the_active_window() { - let startup = scheduler::Instant::now(); - let at = |ms: u64| startup + Duration::from_millis(ms); - let snapshot = FrameSnapshot { - interval_start: at(0), - boundary: IntervalBoundary::Idle { ended_at: at(1000) }, - events: vec![task_poll_event(at(900), at(950))], - // Folded polls that ran during at(0)..at(800), long before the - // active window opens at the contributor's start. - small_polls: vec![SmallPollFlush { - summary: PollSummary { - count: 500, - total: Duration::from_millis(400), - }, - since: at(0), - until: at(800), - }], - dropped_events: 0, - journal_discontinuous: false, - }; - - let incident = - HangIncident::detect(snapshot, HANG_THRESHOLD, FRAME_BUDGET).expect("has contributors"); - let serialized = SerializedHangIncident::convert(startup, &incident, 8, None); - - assert_eq!(serialized.busy_fraction, 0.5); - } - - /// Many sub-threshold pieces of work can drop a frame as thoroughly as - /// one long stall: an interval whose total foreground spend reaches the - /// budget is an incident even when no single event crosses the hang - /// threshold, and every event becomes a contributor so the report shows - /// what filled the interval. - #[test] - fn an_interval_of_small_work_over_budget_is_an_incident() { - let startup = scheduler::Instant::now(); - let at = |ms: u64| startup + Duration::from_millis(ms); - let window_id = WindowId::from(0xB0D6E7); - let snapshot = FrameSnapshot { - interval_start: at(0), - boundary: IntervalBoundary::Presented(PresentedFrame { - frame: FrameTiming { - window_id, - dirty_at: Some(at(0)), - invalidations: 1, - draw_start: at(140), - draw_end: at(145), - }, - presentation: PresentTiming { - window_id, - present_start: at(145), - present_end: at(150), - animation_interval: None, - }, - }), - events: vec![ - task_poll_event(at(0), at(5)), - task_poll_event(at(5), at(13)), - task_poll_event(at(13), at(18)), - ], - small_polls: Vec::new(), - dropped_events: 0, - journal_discontinuous: false, - }; - - let incident = HangIncident::detect(snapshot, HANG_THRESHOLD, FRAME_BUDGET) - .expect("foreground spend exceeded the frame budget"); - assert_eq!(incident.trigger, HangTrigger::Budget); - assert_eq!(incident.contributors.len(), 3); - assert_eq!( - incident.contributors[0].duration(), - Duration::from_millis(8) - ); - let serialized = SerializedHangIncident::convert(startup, &incident, 8, Some(startup)); - assert_eq!(serialized.trigger, HangTrigger::Budget); - assert_eq!(serialized.stall_ms, 8.0); - assert_eq!(serialized.dirty_to_present_ms, Some(150.0)); - assert_eq!(serialized.sealed_by, "present"); - } - - #[test] - fn a_journal_gap_suppresses_budget_inference_but_retains_observed_hangs() { - let start = scheduler::Instant::now(); - let at = |ms: u64| start + Duration::from_millis(ms); - let mut sealer = super::super::journal::IntervalSealer::new(start); - let snapshots = sealer.push_entries([ - ForegroundJournalEntry::Event(task_poll_event(at(0), at(5))), - ForegroundJournalEntry::Discontinuity { lost: 1 }, - ForegroundJournalEntry::Event(task_poll_event(at(10), at(15))), - ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { ended_at: at(15) }), - ForegroundJournalEntry::Discontinuity { lost: 1 }, - ForegroundJournalEntry::Event(task_poll_event(at(20), at(32))), - ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { ended_at: at(32) }), - ForegroundJournalEntry::Event(task_poll_event(at(40), at(52))), - ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { ended_at: at(52) }), - ]); - let [budget_only, observed_hang, clean] = snapshots.as_slice() else { - panic!("expected two discontinuous snapshots followed by a clean one"); - }; - - assert!(budget_only.journal_discontinuous); - assert_eq!(budget_only.dropped_events, 1); - assert!(HangIncident::detect(budget_only.clone(), HANG_THRESHOLD, FRAME_BUDGET).is_none()); - - let observed_hang = - HangIncident::detect(observed_hang.clone(), HANG_THRESHOLD, FRAME_BUDGET) - .expect("the directly observed threshold-qualified hang remains valid"); - assert!(observed_hang.snapshot.journal_discontinuous); - assert_eq!(observed_hang.contributors.len(), 1); - let serialized = SerializedHangIncident::convert(start, &observed_hang, 8, None); - assert!(serialized.journal_discontinuous); - - assert!(!clean.journal_discontinuous); - assert!(HangIncident::detect(clean.clone(), HANG_THRESHOLD, FRAME_BUDGET).is_some()); - } - - /// A frame that reaches the screen late with almost no foreground work - /// behind it — pure scheduling or presentation delay — is not a hang: - /// the budget measures foreground spend, not dirty-to-present time. - #[test] - fn a_slow_frame_with_little_foreground_spend_is_not_an_incident() { - let startup = scheduler::Instant::now(); - let at = |ms: u64| startup + Duration::from_millis(ms); - let window_id = WindowId::from(0xFA57); - let snapshot = FrameSnapshot { - interval_start: at(0), - boundary: IntervalBoundary::Presented(PresentedFrame { - frame: FrameTiming { - window_id, - dirty_at: Some(at(0)), - invalidations: 1, - draw_start: at(145), - draw_end: at(147), - }, - presentation: PresentTiming { - window_id, - present_start: at(149), - present_end: at(150), - animation_interval: None, - }, - }), - events: vec![task_poll_event(at(0), at(5))], - small_polls: Vec::new(), - dropped_events: 0, - journal_discontinuous: false, - }; - - assert!(HangIncident::detect(snapshot, HANG_THRESHOLD, FRAME_BUDGET).is_none()); - } - - /// The budget applies to idle-sealed intervals too: a headless app (or - /// a stretch with no repaint pending) can still starve the foreground - /// with accumulated sub-threshold work. - #[test] - fn an_idle_sealed_interval_over_budget_is_an_incident() { - let startup = scheduler::Instant::now(); - let at = |ms: u64| startup + Duration::from_millis(ms); - let snapshot = FrameSnapshot { - interval_start: at(0), - boundary: IntervalBoundary::Idle { ended_at: at(200) }, - events: (0..10) - .map(|i| task_poll_event(at(i * 20), at(i * 20 + 5))) - .collect(), - small_polls: Vec::new(), - dropped_events: 0, - journal_discontinuous: false, - }; - - let incident = HangIncident::detect(snapshot, HANG_THRESHOLD, FRAME_BUDGET) - .expect("foreground spend exceeded the frame budget"); - assert_eq!(incident.contributors.len(), 10); - let serialized = SerializedHangIncident::convert(startup, &incident, 8, Some(startup)); - assert_eq!(serialized.sealed_by, "idle"); - assert_eq!(serialized.dirty_to_present_ms, None); - assert_eq!(serialized.stall_ms, 5.0); - assert_eq!(serialized.contributors_elided, 2); - } - - /// Contributors serialize in start order with their nesting depth: an - /// input dispatch that synchronously drew a window reads as the input at - /// depth 0 followed by the draw at depth 1, rather than two unrelated - /// blocks of equal wall time. - #[test] - fn serialized_contributors_are_chronological_with_nesting_depths() { - let startup = scheduler::Instant::now(); - let at = |ms: u64| startup + Duration::from_millis(ms); - let window_id = WindowId::from(0x2E57ED); - let snapshot = FrameSnapshot { - interval_start: at(0), - boundary: IntervalBoundary::Idle { ended_at: at(80) }, - events: vec![ - task_poll_event(at(50), at(70)), - ForegroundEvent::Input(InputTiming { - kind: "mouse_move", - start: at(0), - end: at(40), - caused_invalidation: true, - }), - ForegroundEvent::Draw(FrameTiming { - window_id, - dirty_at: Some(at(0)), - invalidations: 1, - draw_start: at(1), - draw_end: at(39), - }), - ], - small_polls: Vec::new(), - dropped_events: 0, - journal_discontinuous: false, - }; - - let incident = - HangIncident::detect(snapshot, HANG_THRESHOLD, FRAME_BUDGET).expect("has contributors"); - let serialized = SerializedHangIncident::convert(startup, &incident, 8, Some(startup)); - - assert_eq!(serialized.stall_ms, 40.0); - assert!(matches!( - serialized.contributors.as_slice(), - [ - SerializedHangContributor::Input { - input_kind: "mouse_move", - depth: 0, - .. - }, - SerializedHangContributor::Draw { depth: 1, .. }, - SerializedHangContributor::TaskPoll { depth: 0, .. }, - ] - )); - } - - /// Folded small polls count toward foreground spend, so an interval can - /// reach the budget with no retained events at all. The incident then - /// has no contributors and the small-poll summary carries the story. - #[test] - fn small_poll_spend_alone_can_reach_the_budget() { - let startup = scheduler::Instant::now(); - let at = |ms: u64| startup + Duration::from_millis(ms); - let window_id = WindowId::from(0xDE1A7); - let snapshot = FrameSnapshot { - interval_start: at(0), - boundary: IntervalBoundary::Presented(PresentedFrame { - frame: FrameTiming { - window_id, - dirty_at: Some(at(0)), - invalidations: 1, - draw_start: at(148), - draw_end: at(149), - }, - presentation: PresentTiming { - window_id, - present_start: at(149), - present_end: at(150), - animation_interval: None, - }, - }), - events: Vec::new(), - small_polls: vec![SmallPollFlush { - summary: PollSummary { - count: 40, - total: Duration::from_millis(12), - }, - since: at(0), - until: at(150), - }], - dropped_events: 0, - journal_discontinuous: false, - }; - - let incident = HangIncident::detect(snapshot, HANG_THRESHOLD, FRAME_BUDGET) - .expect("small-poll spend exceeded the frame budget"); - assert!(incident.contributors.is_empty()); - let serialized = SerializedHangIncident::convert(startup, &incident, 8, Some(startup)); - assert_eq!(serialized.stall_ms, 0.0); - assert_eq!(serialized.event_count, 0); - assert_eq!(serialized.small_poll_count, 40); - assert_eq!(serialized.dirty_to_present_ms, Some(150.0)); - assert_eq!(serialized.busy_fraction, 0.08); - } - - proptest! { - #![proptest_config(ProptestConfig { - failure_persistence: None, - ..ProptestConfig::default() - })] - - #[test] - fn detection_matches_threshold_and_budget_model_at_boundaries( - durations_ms in prop::collection::vec(0u16..=50, 0..=16), - threshold_ms in 0u16..=60, - frame_budget_ms in 0u16..=500, - small_poll_total_ms in 0u16..=100, - ) { - let origin = scheduler::Instant::now(); - let mut cursor_ms = 0u64; - let events = durations_ms - .iter() - .map(|duration_ms| { - let start = origin + Duration::from_millis(cursor_ms); - cursor_ms += u64::from(*duration_ms); - let end = origin + Duration::from_millis(cursor_ms); - cursor_ms += 1; - ForegroundEvent::Input(InputTiming { - kind: "test", - start, - end, - caused_invalidation: false, - }) - }) - .collect::>(); - let interval_end_ms = cursor_ms.max(1); - let snapshot = FrameSnapshot { - interval_start: origin, - boundary: IntervalBoundary::Idle { - ended_at: origin + Duration::from_millis(interval_end_ms), - }, - events, - small_polls: vec![SmallPollFlush { - summary: PollSummary { - count: u64::from(small_poll_total_ms > 0), - total: Duration::from_millis(u64::from(small_poll_total_ms)), - }, - since: origin, - until: origin + Duration::from_millis(interval_end_ms), - }], - dropped_events: 0, - journal_discontinuous: false, - }; - let qualifying_durations = durations_ms - .iter() - .copied() - .filter(|duration| *duration >= threshold_ms) - .collect::>(); - let occupancy_ms = durations_ms - .iter() - .map(|duration| u64::from(*duration)) - .sum::() - + u64::from(small_poll_total_ms); - let expected_incident = - !qualifying_durations.is_empty() || occupancy_ms >= u64::from(frame_budget_ms); - - let incident = HangIncident::detect( - snapshot, - Duration::from_millis(u64::from(threshold_ms)), - Duration::from_millis(u64::from(frame_budget_ms)), - ); - prop_assert_eq!(incident.is_some(), expected_incident); - - if let Some(incident) = incident { - let mut expected_contributors = if qualifying_durations.is_empty() { - durations_ms - } else { - qualifying_durations - }; - expected_contributors.sort_unstable_by(|first, second| second.cmp(first)); - let observed_contributors = incident - .contributors - .iter() - .map(|event| event.duration().as_millis() as u16) - .collect::>(); - prop_assert_eq!(observed_contributors, expected_contributors); - } - } - } - - #[derive(Clone, Default)] - struct HangControls { - render: Rc>>, - input: Rc>>, - action: Rc>>, - } - - struct HangyView { - controls: HangControls, - focus_handle: FocusHandle, - } - - impl Render for HangyView { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - if let Some(duration) = self.controls.render.take() { - thread::sleep(duration); - } - let action_controls = self.controls.clone(); - let input_controls = self.controls.clone(); - div() - .size_full() - .track_focus(&self.focus_handle) - .on_action(cx.listener(move |_, _: &HangyAction, _, _| { - if let Some(duration) = action_controls.action.take() { - thread::sleep(duration); - } - })) - .on_mouse_down(MouseButton::Left, move |_, _, _| { - if let Some(duration) = input_controls.input.take() { - thread::sleep(duration); - } - }) - } - } - - #[derive(Debug, Copy, Clone, PartialEq, Eq)] - enum HangKind { - Render, - Input, - Action, - Poll, - } - - /// Renders a real element tree and injects randomly ordered, randomly - /// sized hangs through the production dispatch paths: view rendering - /// (window draw), platform input dispatch, action dispatch, and a - /// foreground task poll. Asserts the detector reports every one of them. - #[gpui::test(iterations = 5)] - fn detects_randomly_placed_foreground_hangs(mut rng: StdRng, cx: &mut TestAppContext) { - let (journal, _journal_guard) = install_test_foreground_journal(1024, 16); - let mut detector = HangDetector::new(journal, HANG_THRESHOLD, FRAME_BUDGET); - - let controls = HangControls::default(); - let (view, cx) = cx.add_window_view(|_, cx| HangyView { - controls: controls.clone(), - focus_handle: cx.focus_handle(), - }); - view.update_in(cx, |view, window, cx| { - window.focus(&view.focus_handle, cx); - }); - draw_window(cx); - - let mut injected: Vec<(HangKind, Duration)> = Vec::new(); - for _ in 0..rng.random_range(1..=4) { - let duration = HANG_THRESHOLD + Duration::from_millis(rng.random_range(5..25)); - let kind = match rng.random_range(0..4) { - 0 => { - controls.render.set(Some(duration)); - view.update_in(cx, |_, _, cx| cx.notify()); - draw_window(cx); - HangKind::Render - } - 1 => { - controls.input.set(Some(duration)); - cx.simulate_mouse_down( - point(px(5.), px(5.)), - MouseButton::Left, - Modifiers::none(), - ); - HangKind::Input - } - 2 => { - controls.action.set(Some(duration)); - cx.dispatch_action(HangyAction); - HangKind::Action - } - _ => { - simulate_blocked_foreground_poll(duration); - HangKind::Poll - } - }; - injected.push((kind, duration)); - - // Innocent interleaved activity that must not confuse detection. - if rng.random_bool(0.5) { - draw_window(cx); - } - if rng.random_bool(0.5) { - cx.simulate_mouse_up(point(px(5.), px(5.)), MouseButton::Left, Modifiers::none()); - } - } - - // A final presentation seals whatever interval is still open. - draw_window(cx); - - let incidents = detector.poll(); - let contributors: Vec = incidents - .iter() - .flat_map(|incident| incident.contributors.iter().copied()) - .collect(); - - for kind in [ - HangKind::Render, - HangKind::Input, - HangKind::Action, - HangKind::Poll, - ] { - let expected: Vec = injected - .iter() - .filter(|(injected_kind, _)| *injected_kind == kind) - .map(|(_, duration)| *duration) - .collect(); - let observed: Vec = contributors - .iter() - .filter(|event| matches_kind(event, kind) && event.duration() >= HANG_THRESHOLD) - .map(|event| event.duration()) - .collect(); - assert_all_matched(kind, expected, observed); - } - } - - /// Every injected hang must be covered by a distinct observed contributor - /// at least as long as the injected sleep (sleeps never wake early). - fn assert_all_matched( - kind: HangKind, - mut expected: Vec, - mut observed: Vec, - ) { - assert_eq!( - observed.len(), - expected.len(), - "expected every observed {kind:?} hang to correspond to one injection; \ - expected {expected:?}, observed {observed:?}" - ); - expected.sort_unstable_by(|a, b| b.cmp(a)); - observed.sort_unstable_by(|a, b| b.cmp(a)); - let mut observed = observed.into_iter(); - for expected_duration in expected { - let matched = observed.find(|observed| *observed >= expected_duration); - assert!( - matched.is_some(), - "injected {kind:?} hang of {expected_duration:?} was not detected" - ); - } - } - - fn matches_kind(event: &ForegroundEvent, kind: HangKind) -> bool { - match (event, kind) { - (ForegroundEvent::Draw(_), HangKind::Render) => true, - (ForegroundEvent::Input(_), HangKind::Input) => true, - (ForegroundEvent::Action(timing), HangKind::Action) => { - timing.name.ends_with("HangyAction") - } - (ForegroundEvent::TaskPoll(timing), HangKind::Poll) => { - timing.location.file() == file!() - } - _ => false, - } - } - - fn draw_window(cx: &mut VisualTestContext) { - cx.update(|window, cx| { - let arena_clear = window.draw(cx); - window.present_if_needed(); - arena_clear.clear(cx); - }); - } - - /// The deterministic test scheduler does not bracket runnables with the - /// profiler hooks, so drive the same public hooks the platform - /// dispatchers call around a poll that blocks the foreground. - fn simulate_blocked_foreground_poll(duration: Duration) { - let location = std::panic::Location::caller(); - crate::profiler::update_running_task(SpawnTime(scheduler::Instant::now()), location); - thread::sleep(duration); - crate::profiler::save_task_timing(); - } - - fn task_poll_event(start: scheduler::Instant, end: scheduler::Instant) -> ForegroundEvent { - ForegroundEvent::TaskPoll(TaskTiming { - location: std::panic::Location::caller(), - spawned: SpawnTime(start), - start, - end: YieldTime(end), - }) - } - - fn presentation(window_id: WindowId, present_end: scheduler::Instant) -> PresentTiming { - PresentTiming { - window_id, - present_start: present_end - Duration::from_millis(1), - present_end, - animation_interval: None, - } - } - - fn frame(window_id: WindowId, draw_end: scheduler::Instant) -> FrameTiming { - FrameTiming { - window_id, - dirty_at: Some(draw_end - Duration::from_millis(2)), - invalidations: 1, - draw_start: draw_end - Duration::from_millis(1), - draw_end, - } - } -} diff --git a/crates/gpui_pre/src/profiler/journal.rs b/crates/gpui_pre/src/profiler/journal.rs deleted file mode 100644 index 761181a..0000000 --- a/crates/gpui_pre/src/profiler/journal.rs +++ /dev/null @@ -1,2766 +0,0 @@ -//! A journal of foreground work and the semantic boundaries between activity intervals. -//! -//! The foreground thread records task polls, action handlers, input dispatches, -//! draws, and presentations as [`ForegroundEvent`]s in a bounded journal ring. -//! Task polls shorter than [`TASK_POLL_FLOOR`] are folded into summaries, which -//! bound the stream by the number of slow polls while preserving their exact -//! count and total duration. -//! -//! Presentation and the foreground going idle are explicit [`IntervalBoundary`] -//! entries in the same stream. Independent [`ForegroundJournalCollector`]s -//! feed entries to [`IntervalSealer`], a pure state machine that groups all work -//! preceding each boundary into a [`FrameSnapshot`]. The sealer does not infer -//! boundaries from elapsed time or from incidental event kinds such as draws. - -use std::cell::{RefCell, UnsafeCell}; -use std::collections::{HashMap, VecDeque}; -use std::mem::MaybeUninit; -use std::sync::{ - Arc, - atomic::{AtomicU64, AtomicUsize, Ordering}, -}; -use std::time::Duration; - -use scheduler::Instant; - -use super::{ActionTiming, FrameTiming, PresentTiming, TaskTiming}; -use crate::WindowId; - -/// Task polls shorter than this are folded into a [`PollSummary`] instead of -/// being recorded individually. This keeps the stream bounded by the number -/// of *slow* polls while preserving their exact count and total duration. -pub const TASK_POLL_FLOOR: Duration = Duration::from_micros(100); - -/// A dirty frame that has not been presented by this deadline stops blocking -/// foreground-idle boundaries, so a window that never presents (hidden or -/// occluded windows receive no frame callbacks) cannot suppress boundaries -/// indefinitely. Expiry only unblocks: the open interval still seals at a -/// real presentation or idle boundary, keeping a starving hang and the frame -/// it starved in one interval. -pub const FRAME_DEADLINE: Duration = Duration::from_secs(1); - -// Backstop against pathological event storms within a single interval. At the -// 100us floor, a fully hung second can produce at most ~10k recordable polls, -// so this bound is only reachable when something is already deeply wrong. -const MAX_INTERVAL_EVENTS: usize = 16 * 1024; - -// Allow 4MiB for the fixed ring allocation, including each slot's atomic -// metadata. The poll floor and frame cadence bound the event rate to roughly -// 10k per second in the worst case, so this holds several seconds of -// worst-case traffic between consumer drains. -const MAX_JOURNAL_ENTRIES: usize = (4 * 1024 * 1024) / core::mem::size_of::(); - -// Absorbs brief collisions with a collector reading the exact slot being -// wrapped. The foreground never waits for a reader; queued entries are retried -// in order on the next publication. -const MAX_PENDING_JOURNAL_ENTRIES: usize = 64; - -/// One entry in the foreground stream. -/// -/// Events are recorded in completion order (ordered by their end time). An -/// event that was in progress across a frame boundary (e.g. the task poll -/// enclosing a draw) is recorded when it *ends*, with timestamps that may -/// precede events recorded before it. -#[derive(Debug, Copy, Clone)] -pub enum ForegroundEvent { - /// A foreground task poll at least [`TASK_POLL_FLOOR`] long. - TaskPoll(TaskTiming), - /// A completed action handler. - Action(ActionTiming), - /// A dispatched platform input event. - Input(InputTiming), - /// A completed window draw. - Draw(FrameTiming), - /// Work spent submitting a frame to the platform. - Present(PresentTiming), - /// Aggregate of task polls below [`TASK_POLL_FLOOR`], flushed before the - /// next individually retained event or interval boundary. - SmallPolls(SmallPollFlush), -} - -impl ForegroundEvent { - /// When the work described by this event began. For draws this is the - /// start of the draw itself, not the frame's first invalidation. - pub fn start_time(&self) -> Instant { - match self { - Self::TaskPoll(timing) => timing.start, - Self::Action(timing) => timing.start, - Self::Input(timing) => timing.start, - Self::Draw(timing) => timing.draw_start, - Self::Present(timing) => timing.present_start, - Self::SmallPolls(flush) => flush.since, - } - } - - /// When the work described by this event ended. - pub fn end_time(&self) -> Instant { - match self { - Self::TaskPoll(timing) => timing.end.0, - Self::Action(timing) => timing.end, - Self::Input(timing) => timing.end, - Self::Draw(timing) => timing.draw_end, - Self::Present(timing) => timing.present_end, - Self::SmallPolls(flush) => flush.until, - } - } - - /// How long the work described by this event took. For - /// [`Self::SmallPolls`] this is the span the summary covers, not time - /// spent polling; use the summary's `total` for occupancy. - pub fn duration(&self) -> Duration { - self.end_time().duration_since(self.start_time()) - } -} - -/// Timing of one platform input dispatch on a window. -#[derive(Debug, Copy, Clone)] -pub struct InputTiming { - /// The platform input variant dispatched (see - /// [`crate::PlatformInput::kind_name`]). - pub kind: &'static str, - /// When the input dispatch started. - pub start: Instant, - /// When the input dispatch finished. - pub end: Instant, - /// Whether handling the input invalidated a window. - pub caused_invalidation: bool, -} - -/// Exact count and total duration of task polls below [`TASK_POLL_FLOOR`]. -#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] -pub struct PollSummary { - /// Number of polls below the floor. - pub count: u64, - /// Total duration of polls below the floor. - pub total: Duration, -} - -impl PollSummary { - fn add(&mut self, other: Self) { - self.count += other.count; - self.total += other.total; - } -} - -/// A flushed [`PollSummary`] and the tightest span containing its folded polls. -#[derive(Debug, Copy, Clone)] -pub struct SmallPollFlush { - /// The folded polls. - pub summary: PollSummary, - /// When the first folded poll began. - pub since: Instant, - /// When the last folded poll ended. - pub until: Instant, -} - -/// A newly drawn frame and the platform submission that completed its interval. -#[derive(Debug, Copy, Clone)] -pub struct PresentedFrame { - /// The draw whose rendered scene was submitted. - pub frame: FrameTiming, - /// Work spent submitting that scene to the platform. - pub presentation: PresentTiming, -} - -impl PresentedFrame { - /// Time from the frame's first invalidation through platform submission. - pub fn dirty_to_present_duration(&self) -> Option { - self.frame - .dirty_at - .map(|dirty_at| self.presentation.present_end.duration_since(dirty_at)) - } -} - -/// The semantic event that completed a foreground activity interval. -#[derive(Debug, Copy, Clone)] -pub enum IntervalBoundary { - /// A newly drawn frame was submitted to the platform. - Presented(PresentedFrame), - /// The foreground returned to an idle platform loop with no unexpired - /// frame pending. - Idle { - /// When the foreground went idle. - ended_at: Instant, - }, -} - -impl IntervalBoundary { - /// When the interval ended. - pub fn end_time(&self) -> Instant { - match self { - Self::Presented(presented) => presented.presentation.present_end, - Self::Idle { ended_at } => *ended_at, - } - } - - /// The first invalidation of the frame satisfied by this boundary, if any. - pub fn dirty_at(&self) -> Option { - match self { - Self::Presented(presented) => presented.frame.dirty_at, - Self::Idle { .. } => None, - } - } -} - -/// A control-plane change to one window's pending-frame state. -#[derive(Debug, Copy, Clone)] -pub enum FrameStateChange { - /// The window has a frame waiting to be presented. - Pending { - /// The window waiting for presentation. - window_id: WindowId, - /// When this frame generation first became dirty. - dirty_at: Instant, - }, - /// The window closed, so any pending frame no longer blocks idle - /// boundaries. - Closed { - /// The window that closed. - window_id: WindowId, - /// When the window closed. - at: Instant, - }, -} - -/// One item retained in the foreground journal. -#[derive(Debug, Copy, Clone)] -pub enum ForegroundJournalEntry { - /// A completed piece of foreground work. - Event(ForegroundEvent), - /// A semantic interval boundary. - Boundary(IntervalBoundary), - /// A change to pending-frame state. This is metadata, not foreground work. - FrameState(FrameStateChange), - /// One or more logical entries were unavailable at this point in the - /// stream. Consumers must not infer interval boundaries across this gap. - Discontinuity { - /// Number of unavailable logical entries. - lost: u64, - }, -} - -/// An immutable view of one sealed foreground interval, produced by -/// [`IntervalSealer`]. -#[derive(Debug, Clone)] -pub struct FrameSnapshot { - /// When the interval started (the previous seal, or the end of the idle - /// stretch preceding the interval's first event). - pub interval_start: Instant, - /// The semantic event that completed the interval, including its metadata. - pub boundary: IntervalBoundary, - /// Foreground work recorded during the interval, in completion order. - /// [`ForegroundEvent::SmallPolls`] entries are collected into - /// `small_polls` instead of appearing here. - pub events: Vec, - /// Span-tagged aggregates of task polls below [`TASK_POLL_FLOOR`]. Spans - /// are retained so occupancy can apportion folded poll time to reporting - /// windows narrower than the interval. - pub small_polls: Vec, - /// Events lost to the interval's event cap, plus ring losses reported - /// via [`IntervalSealer::note_lost`]. - pub dropped_events: u64, - /// Whether the journal had an unobserved gap during this interval. - pub journal_discontinuous: bool, -} - -impl FrameSnapshot { - /// When the interval ended. - pub fn interval_end(&self) -> Instant { - self.boundary.end_time() - } - - /// The combined count and total of all folded sub-floor polls in the - /// interval. - pub fn small_poll_summary(&self) -> PollSummary { - let mut summary = PollSummary::default(); - for flush in &self.small_polls { - summary.add(flush.summary); - } - summary - } - - /// Total foreground time occupied within the interval: the union of the - /// recorded events' spans (clamped to the interval, so nested work like an - /// action inside an input dispatch is not double counted) plus the folded - /// small polls. Folded polls cannot be unioned with nested individually - /// timed work, so this remains a close approximation. - pub fn occupancy(&self) -> Duration { - self.occupancy_within(self.interval_start, self.interval_end()) - } - - /// Like [`Self::occupancy`], but measured against an arbitrary window - /// (e.g. a reporting window anchored at a frame's first invalidation). - /// Event spans are clamped to the window. Folded polls carry no - /// individual timestamps, so each flush's total is apportioned by how - /// much of its span overlaps the window — assuming the folded time is - /// spread uniformly across the span. - pub fn occupancy_within(&self, window_start: Instant, window_end: Instant) -> Duration { - let mut spans: Vec<(Instant, Instant)> = self - .events - .iter() - .map(|event| { - let start = event.start_time().max(window_start); - let end = event.end_time().min(window_end).max(start); - (start, end) - }) - .collect(); - spans.sort_by_key(|(start, _)| *start); - - let mut occupied = Duration::ZERO; - let mut merged_until: Option = None; - for (start, end) in spans { - let start = match merged_until { - Some(merged_until) => start.max(merged_until), - None => start, - }; - occupied += end.duration_since(start); - merged_until = Some(match merged_until { - Some(merged_until) => merged_until.max(end), - None => end, - }); - } - - let apportioned_small_polls: Duration = self - .small_polls - .iter() - .map(|flush| { - let span = flush.until.duration_since(flush.since); - if span.is_zero() { - // A point-like flush lies either inside or outside the - // window. - if flush.since >= window_start && flush.since <= window_end { - flush.summary.total - } else { - Duration::ZERO - } - } else { - let overlap_start = flush.since.max(window_start); - let overlap_end = flush.until.min(window_end).max(overlap_start); - let overlap = overlap_end.duration_since(overlap_start); - flush.summary.total.mul_f64(overlap.div_duration_f64(span)) - } - }) - .sum(); - occupied + apportioned_small_polls - } - - /// The fraction of the interval the foreground spent working, in `0.0..=1.0`. - pub fn busy_fraction(&self) -> f64 { - let interval = self.interval_end().duration_since(self.interval_start); - if interval.is_zero() { - return 1.0; - } - (self.occupancy().div_duration_f64(interval)).min(1.0) - } -} - -#[derive(Clone)] -pub(crate) struct ForegroundRunnableCounter(Arc); - -impl ForegroundRunnableCounter { - fn new() -> Self { - Self(Arc::new(AtomicUsize::new(0))) - } - - pub(crate) fn queued(&self) { - self.0.fetch_add(1, Ordering::Release); - } - - fn finished(&self) { - let _ = self - .0 - .fetch_update(Ordering::AcqRel, Ordering::Acquire, |count| { - count.checked_sub(1) - }); - } - - fn has_runnables(&self) -> bool { - self.0.load(Ordering::Acquire) > 0 - } -} - -struct ForegroundJournalWriter { - foreground_runnables: ForegroundRunnableCounter, - publisher: JournalPublisher, - turn_depth: usize, - pending_frames: HashMap, - retained_since_boundary: bool, - small_polls: Option, -} - -impl ForegroundJournalWriter { - fn new(foreground_runnables: ForegroundRunnableCounter, publisher: JournalPublisher) -> Self { - Self { - foreground_runnables, - publisher, - turn_depth: 0, - pending_frames: HashMap::new(), - retained_since_boundary: false, - small_polls: None, - } - } - - fn begin_turn(&mut self) { - self.turn_depth += 1; - } - - // Idle boundaries require a retained event since the last boundary, not - // merely folded sub-floor polls. Sporadic wake-ups (timers, file - // watchers) leave the foreground idle after every tiny poll; a boundary - // for each would re-admit to the ring the very polls the fold keeps out - // of it, wrapping the ring within seconds. Folded-only work is discarded - // at true idle so unrelated wake-ups cannot accumulate toward a later - // frame budget. Polls remain folded while a frame or runnable is pending. - fn end_turn(&mut self, ended_at: Instant) { - let Some(turn_depth) = self.turn_depth.checked_sub(1) else { - debug_assert!(false, "foreground turn must be begun before it ends"); - return; - }; - self.turn_depth = turn_depth; - if self.turn_depth > 0 - || self.foreground_runnables.has_runnables() - || self.has_unexpired_pending_frame(ended_at) - { - return; - } - if !self.retained_since_boundary { - self.small_polls = None; - return; - } - - self.record_entry(ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { - ended_at, - })); - } - - fn has_unexpired_pending_frame(&mut self, now: Instant) -> bool { - if self.pending_frames.is_empty() { - return false; - } - self.pending_frames - .retain(|_, dirty_at| now.saturating_duration_since(*dirty_at) < FRAME_DEADLINE); - !self.pending_frames.is_empty() - } - - fn fold_small_poll(&mut self, timing: TaskTiming) { - let flush = self.small_polls.get_or_insert(SmallPollFlush { - summary: PollSummary::default(), - since: timing.start, - until: timing.end.0, - }); - flush.summary.count += 1; - flush.summary.total += timing.poll_duration(); - flush.since = flush.since.min(timing.start); - flush.until = flush.until.max(timing.end.0); - } - - fn take_small_polls(&mut self) -> Option { - self.small_polls.take() - } - - fn record_event(&mut self, event: ForegroundEvent) { - self.retained_since_boundary = true; - self.record_entry(ForegroundJournalEntry::Event(event)); - } - - fn record_entry(&mut self, entry: ForegroundJournalEntry) { - let small_polls = self - .take_small_polls() - .map(ForegroundEvent::SmallPolls) - .map(ForegroundJournalEntry::Event); - self.publisher - .publish([small_polls, Some(entry)].into_iter().flatten()); - if matches!(entry, ForegroundJournalEntry::Boundary(_)) { - self.retained_since_boundary = false; - } - } - - fn record_frame_state(&mut self, change: FrameStateChange) { - self.record_entry(ForegroundJournalEntry::FrameState(change)); - } - - fn record_frame_pending(&mut self, window_id: WindowId, dirty_at: Instant) { - let should_record = match self.pending_frames.get(&window_id) { - Some(previous_dirty_at) => { - dirty_at.saturating_duration_since(*previous_dirty_at) >= FRAME_DEADLINE - } - None => true, - }; - if !should_record { - return; - } - - self.pending_frames.insert(window_id, dirty_at); - self.record_frame_state(FrameStateChange::Pending { - window_id, - dirty_at, - }); - } - - fn record_window_closed(&mut self, window_id: WindowId, at: Instant) { - self.pending_frames.remove(&window_id); - self.record_frame_state(FrameStateChange::Closed { window_id, at }); - } - - fn record_present(&mut self, timing: PresentTiming, frame: Option) { - match frame { - Some(frame) => { - self.pending_frames.remove(&frame.window_id); - self.record_entry(ForegroundJournalEntry::Boundary( - IntervalBoundary::Presented(PresentedFrame { - frame, - presentation: timing, - }), - )); - } - None => { - self.pending_frames.remove(&timing.window_id); - self.record_event(ForegroundEvent::Present(timing)); - } - } - } -} - -thread_local! { - static FOREGROUND_RUNNABLES: ForegroundRunnableCounter = ForegroundRunnableCounter::new(); - static FOREGROUND_JOURNAL: RefCell> = const { RefCell::new(None) }; -} - -pub(crate) fn foreground_runnable_counter() -> ForegroundRunnableCounter { - FOREGROUND_RUNNABLES.with(Clone::clone) -} - -/// Starts journaling on the calling thread. Called once by `App` construction -/// on the main thread; every other thread's recording calls are no-ops. -/// Idempotent so that multiple `App`s on one thread (tests) share one journal. -pub(crate) fn install_foreground_journal() -> ForegroundJournal { - let foreground_runnables = foreground_runnable_counter(); - FOREGROUND_JOURNAL.with(|journal| { - let mut journal = journal.borrow_mut(); - if let Some(journal) = journal.as_ref() { - return ForegroundJournal { - ring: Arc::clone(&journal.publisher.ring), - }; - } - - let (handle, publisher) = - ForegroundJournal::new(MAX_JOURNAL_ENTRIES, MAX_PENDING_JOURNAL_ENTRIES); - *journal = Some(ForegroundJournalWriter::new( - foreground_runnables, - publisher, - )); - handle - }) -} - -/// Restores the previous foreground journal on drop, so a test's writer -/// (turn depth, pending frames, retained-since-boundary state) never leaks -/// into a later test that happens to reuse the same test-harness thread. -#[cfg(test)] -pub(crate) struct TestForegroundJournalGuard { - previous: Option, - _not_send: std::marker::PhantomData>, -} - -#[cfg(test)] -impl Drop for TestForegroundJournalGuard { - fn drop(&mut self) { - FOREGROUND_JOURNAL.with(|journal| { - *journal.borrow_mut() = self.previous.take(); - }); - } -} - -/// Installs a fresh journal for the duration of a test, isolated from -/// whatever a previous test left on this thread. Used by tests outside this -/// module (e.g. `bench_context`) that need to observe journal entries -/// without interference from the shared per-thread journal. -#[cfg(test)] -pub(crate) fn install_test_foreground_journal( - capacity: usize, - pending_capacity: usize, -) -> (ForegroundJournal, TestForegroundJournalGuard) { - let foreground_runnables = foreground_runnable_counter(); - let (handle, publisher) = ForegroundJournal::new(capacity, pending_capacity); - let previous = FOREGROUND_JOURNAL.with(|journal| { - journal.borrow_mut().replace(ForegroundJournalWriter::new( - foreground_runnables, - publisher, - )) - }); - ( - handle, - TestForegroundJournalGuard { - previous, - _not_send: std::marker::PhantomData, - }, - ) -} - -fn with_journal(f: impl FnOnce(&mut ForegroundJournalWriter)) { - FOREGROUND_JOURNAL.with(|journal| { - if let Some(journal) = journal.borrow_mut().as_mut() { - f(journal); - } - }); -} - -// TODO(gpui-profiler): the turn brackets in the dispatchers and -// WindowProfiler are bare begin/end call pairs rather than uses of this -// guard. A caught unwind between a pair leaves `turn_depth` (and potentially -// the runnable counter) permanently unbalanced, which silently disables -// idle boundaries for the rest of the process. Zed aborts on panics so -// this is latent today, but gpui-as-a-library callers may catch unwinds; -// route all bracketing through this guard. -pub(crate) struct ForegroundTurnGuard; - -impl Drop for ForegroundTurnGuard { - fn drop(&mut self) { - end_foreground_turn(); - } -} - -pub(crate) fn foreground_turn() -> ForegroundTurnGuard { - begin_foreground_turn(); - ForegroundTurnGuard -} - -pub(crate) fn begin_foreground_turn() { - with_journal(ForegroundJournalWriter::begin_turn); -} - -pub(crate) fn end_foreground_turn() { - with_journal(|journal| journal.end_turn(Instant::now())); -} - -pub(crate) fn record_task_poll(timing: TaskTiming) { - FOREGROUND_RUNNABLES.with(ForegroundRunnableCounter::finished); - with_journal(|journal| { - if timing.poll_duration() >= TASK_POLL_FLOOR { - journal.record_event(ForegroundEvent::TaskPoll(timing)); - } else { - journal.fold_small_poll(timing); - } - journal.end_turn(timing.end.0); - }); -} - -pub(crate) fn record_action(timing: ActionTiming) { - with_journal(|journal| journal.record_event(ForegroundEvent::Action(timing))); -} - -pub(crate) fn record_input(timing: InputTiming) { - with_journal(|journal| journal.record_event(ForegroundEvent::Input(timing))); -} - -pub(crate) fn record_draw(timing: FrameTiming) { - with_journal(|journal| journal.record_event(ForegroundEvent::Draw(timing))); -} - -pub(crate) fn record_present(timing: PresentTiming, frame: Option) { - with_journal(|journal| journal.record_present(timing, frame)); -} - -pub(crate) fn record_frame_pending(window_id: WindowId, dirty_at: Instant) { - with_journal(|journal| journal.record_frame_pending(window_id, dirty_at)); -} - -pub(crate) fn record_window_closed(window_id: WindowId) { - let at = Instant::now(); - with_journal(|journal| journal.record_window_closed(window_id, at)); -} - -const SLOT_WRITER: usize = 1 << (usize::BITS - 1); -const SLOT_READER_MASK: usize = !SLOT_WRITER; -const EMPTY_SEQUENCE: u64 = u64::MAX; - -struct JournalSlot { - users: AtomicUsize, - sequence: AtomicU64, - entry: UnsafeCell>, -} - -// `users` ensures the entry is only read while no writer owns the slot and is -// only written while no readers own it. -unsafe impl Sync for JournalSlot {} - -impl JournalSlot { - fn new() -> Self { - Self { - users: AtomicUsize::new(0), - sequence: AtomicU64::new(EMPTY_SEQUENCE), - entry: UnsafeCell::new(MaybeUninit::uninit()), - } - } - - fn try_publish(&self, sequence: u64, entry: ForegroundJournalEntry) -> bool { - if self - .users - .compare_exchange(0, SLOT_WRITER, Ordering::Acquire, Ordering::Relaxed) - .is_err() - { - return false; - } - - // SAFETY: setting SLOT_WRITER from zero gives this writer exclusive - // access to the slot until the Release store below. - unsafe { - (*self.entry.get()).write(entry); - } - self.sequence.store(sequence, Ordering::Relaxed); - self.users.store(0, Ordering::Release); - true - } - - fn try_read(&self, expected_sequence: u64) -> Option { - let _guard = JournalSlotReadGuard::try_new(self)?; - if self.sequence.load(Ordering::Relaxed) != expected_sequence { - return None; - } - - // SAFETY: a matching sequence means the slot was initialized for this - // logical entry, and `guard` prevents the writer from overwriting it - // until after the Copy. - Some(unsafe { *(*self.entry.get()).assume_init_ref() }) - } - - fn try_add_reader(&self) -> bool { - let mut users = self.users.load(Ordering::Relaxed); - loop { - if users & SLOT_WRITER != 0 || users == SLOT_READER_MASK { - return false; - } - match self.users.compare_exchange_weak( - users, - users + 1, - Ordering::Acquire, - Ordering::Relaxed, - ) { - Ok(_) => return true, - Err(updated_users) => users = updated_users, - } - } - } - - fn remove_reader(&self) { - let previous = self.users.fetch_sub(1, Ordering::Release); - debug_assert!( - previous > 0 && previous & SLOT_WRITER == 0, - "invalid slot reader state: {previous:#x}" - ); - } -} - -struct JournalSlotReadGuard<'a> { - slot: &'a JournalSlot, -} - -impl<'a> JournalSlotReadGuard<'a> { - fn try_new(slot: &'a JournalSlot) -> Option { - if slot.try_add_reader() { - Some(Self { slot }) - } else { - None - } - } -} - -impl Drop for JournalSlotReadGuard<'_> { - fn drop(&mut self) { - self.slot.remove_reader(); - } -} - -struct JournalRing { - slots: Box<[JournalSlot]>, - finalized: AtomicU64, - offered: AtomicU64, -} - -impl JournalRing { - fn new(capacity: usize) -> Self { - let capacity = capacity.max(1); - Self { - slots: (0..capacity).map(|_| JournalSlot::new()).collect(), - finalized: AtomicU64::new(0), - offered: AtomicU64::new(0), - } - } - - fn capacity(&self) -> usize { - self.slots.len() - } - - fn try_publish(&self, sequence: u64, entry: ForegroundJournalEntry) -> bool { - let index = (sequence % self.slots.len() as u64) as usize; - self.slots[index].try_publish(sequence, entry) - } - - fn read(&self, sequence: u64) -> Option { - let index = (sequence % self.slots.len() as u64) as usize; - self.slots[index].try_read(sequence) - } -} - -struct PendingJournalEntry { - sequence: u64, - entry: ForegroundJournalEntry, -} - -struct JournalPublisher { - ring: Arc, - next_sequence: u64, - pending: VecDeque, - dropped_after_pending: u64, - pending_capacity: usize, -} - -impl JournalPublisher { - fn new(ring: Arc, pending_capacity: usize) -> Self { - Self { - ring, - next_sequence: 0, - pending: VecDeque::with_capacity(pending_capacity), - dropped_after_pending: 0, - pending_capacity, - } - } - - fn publish(&mut self, entries: impl IntoIterator) { - for entry in entries { - self.flush_pending(); - self.publish_one(entry); - } - self.flush_pending(); - } - - fn publish_one(&mut self, entry: ForegroundJournalEntry) { - let sequence = self.next_sequence; - self.next_sequence += 1; - self.ring - .offered - .store(self.next_sequence, Ordering::Release); - - if !self.pending.is_empty() || self.dropped_after_pending > 0 { - if self.dropped_after_pending == 0 && self.pending.len() < self.pending_capacity { - self.pending - .push_back(PendingJournalEntry { sequence, entry }); - } else { - self.dropped_after_pending += 1; - } - return; - } - - if self.ring.try_publish(sequence, entry) { - self.ring.finalized.store(sequence + 1, Ordering::Release); - } else if self.pending_capacity > 0 { - self.pending - .push_back(PendingJournalEntry { sequence, entry }); - } else { - self.dropped_after_pending = 1; - } - } - - fn flush_pending(&mut self) { - while let Some(pending) = self.pending.front() { - if !self.ring.try_publish(pending.sequence, pending.entry) { - return; - } - let sequence = pending.sequence; - self.pending.pop_front(); - self.ring.finalized.store(sequence + 1, Ordering::Release); - } - - if self.dropped_after_pending > 0 { - self.ring - .finalized - .store(self.next_sequence, Ordering::Release); - self.dropped_after_pending = 0; - } - } -} - -/// A cloneable handle to one foreground journal stream. -/// -/// Each collector has an independent cursor. Collectors briefly pin one slot -/// at a time and never block recording; entries they could not observe are -/// reported as discontinuities. Apps on the same foreground thread share the -/// stream. -#[derive(Clone)] -pub struct ForegroundJournal { - ring: Arc, -} - -impl ForegroundJournal { - fn new(capacity: usize, pending_capacity: usize) -> (Self, JournalPublisher) { - let ring = Arc::new(JournalRing::new(capacity)); - ( - Self { - ring: Arc::clone(&ring), - }, - JournalPublisher::new(ring, pending_capacity), - ) - } - - /// Creates an independent collector that observes entries offered after - /// this call. - pub fn collector(&self) -> ForegroundJournalCollector { - ForegroundJournalCollector { - cursor: self.ring.offered.load(Ordering::Acquire), - ring: Arc::clone(&self.ring), - } - } -} - -/// Entries returned by one [`ForegroundJournalCollector::collect_unseen`] call. -#[derive(Debug, Default)] -pub struct DrainedEntries { - /// Journal entries recorded since the previous drain, in recording order, - /// including synthetic [`ForegroundJournalEntry::Discontinuity`] markers at - /// unavailable logical positions. - pub entries: Vec, - /// Entries unavailable to this collector because they were overwritten or - /// could not be published after the fixed pending queue filled. This is the - /// aggregate of the discontinuity markers in `entries`. - pub lost: u64, -} - -/// Reads the foreground stream, tracking a cursor so each call to -/// [`Self::collect_unseen`] returns only entries recorded since the previous -/// call. Independent collectors do not affect each other. -pub struct ForegroundJournalCollector { - cursor: u64, - ring: Arc, -} - -impl ForegroundJournalCollector { - /// Returns entries recorded since the previous call (or since the - /// collector was created), reporting how many were unavailable before this - /// drain observed them. - pub fn collect_unseen(&mut self) -> DrainedEntries { - let end = self.ring.finalized.load(Ordering::Acquire); - if self.cursor >= end { - return DrainedEntries::default(); - } - let retained_start = end.saturating_sub(self.ring.capacity() as u64); - let mut lost = retained_start.saturating_sub(self.cursor); - self.cursor = self.cursor.max(retained_start); - let mut entries = Vec::with_capacity((end - self.cursor) as usize + usize::from(lost > 0)); - if lost > 0 { - entries.push(ForegroundJournalEntry::Discontinuity { lost }); - } - - while self.cursor < end { - if let Some(entry) = self.ring.read(self.cursor) { - entries.push(entry); - } else { - lost += 1; - match entries.last_mut() { - Some(ForegroundJournalEntry::Discontinuity { lost }) => *lost += 1, - _ => entries.push(ForegroundJournalEntry::Discontinuity { lost: 1 }), - } - } - self.cursor += 1; - } - - DrainedEntries { entries, lost } - } -} - -/// A pure state machine that groups foreground journal entries into -/// [`FrameSnapshot`]s. -/// -/// Feed it drained entries in recording order. Work is carried across calls -/// until an explicit [`IntervalBoundary`] arrives. Idle time before an -/// interval's first event is excluded without relying on an elapsed-time -/// heuristic. -#[derive(Debug)] -pub struct IntervalSealer { - interval_start: Instant, - events: Vec, - small_polls: Vec, - dropped_events: u64, - journal_discontinuous: bool, -} - -impl IntervalSealer { - /// Creates a sealer whose first interval starts at `start` (typically - /// the moment the consumer's collector was created). - pub fn new(start: Instant) -> Self { - Self { - interval_start: start, - events: Vec::new(), - small_polls: Vec::new(), - dropped_events: 0, - journal_discontinuous: false, - } - } - - /// Accounts for a loss supplied outside a collector drain. Collector losses - /// are already represented by ordered discontinuity entries. - pub fn note_lost(&mut self, lost: u64) { - self.dropped_events += lost; - self.journal_discontinuous |= lost > 0; - } - - /// Processes a batch of drained entries, returning the snapshots completed - /// by explicit boundaries. Work without a following boundary is carried - /// over to subsequent calls. - pub fn push_entries( - &mut self, - entries: impl IntoIterator, - ) -> Vec { - let mut snapshots = Vec::new(); - for entry in entries { - match entry { - ForegroundJournalEntry::Event(event) => { - if self.is_empty() { - self.interval_start = self.interval_start.max(event.start_time()); - } - match event { - ForegroundEvent::SmallPolls(flush) => self.push_small_polls(flush), - event => self.push_event(event), - } - } - ForegroundJournalEntry::Boundary(boundary) => { - if let IntervalBoundary::Presented(presented) = boundary { - if self.is_empty() { - self.interval_start = self - .interval_start - .max(presented.presentation.present_start); - } - self.push_event(ForegroundEvent::Present(presented.presentation)); - } - if self.is_empty() { - self.interval_start = self.interval_start.max(boundary.end_time()); - } else { - snapshots.push(self.seal(boundary)); - } - } - // Pending-frame state gates boundaries on the writer side; - // the entries remain in the stream for consumers that want - // dirty timing, but the sealer has no use for them. - ForegroundJournalEntry::FrameState(_) => {} - ForegroundJournalEntry::Discontinuity { lost } => self.note_lost(lost), - } - } - snapshots - } - - fn is_empty(&self) -> bool { - self.events.is_empty() - && self.small_polls.is_empty() - && self.dropped_events == 0 - && !self.journal_discontinuous - } - - fn push_event(&mut self, event: ForegroundEvent) { - if self.events.len() >= MAX_INTERVAL_EVENTS { - self.dropped_events += 1; - } else { - self.events.push(event); - } - } - - fn push_small_polls(&mut self, flush: SmallPollFlush) { - if self.small_polls.len() >= MAX_INTERVAL_EVENTS - && let Some(last) = self.small_polls.last_mut() - { - // Degrade gracefully at the cap: widen the last flush's span - // instead of dropping poll time, at the cost of coarser - // apportioning. - last.summary.add(flush.summary); - last.since = last.since.min(flush.since); - last.until = last.until.max(flush.until); - } else { - self.small_polls.push(flush); - } - } - - fn seal(&mut self, boundary: IntervalBoundary) -> FrameSnapshot { - let ended = boundary.end_time(); - let snapshot = FrameSnapshot { - interval_start: self.interval_start, - boundary, - events: std::mem::take(&mut self.events), - small_polls: std::mem::take(&mut self.small_polls), - dropped_events: std::mem::take(&mut self.dropped_events), - journal_discontinuous: std::mem::take(&mut self.journal_discontinuous), - }; - self.interval_start = ended; - snapshot - } -} - -#[cfg(test)] -mod tests { - use proptest::prelude::*; - use scheduler::SpawnTime; - - use super::*; - use crate::{WindowId, profiler::YieldTime}; - - #[test] - fn draw_waits_for_its_presentation_boundary() { - let start = Instant::now(); - let input = InputTiming { - kind: "test", - start: start + Duration::from_millis(1), - end: start + Duration::from_millis(2), - caused_invalidation: true, - }; - let frame = FrameTiming { - window_id: WindowId::from(1), - dirty_at: Some(input.start), - invalidations: 1, - draw_start: start + Duration::from_millis(3), - draw_end: start + Duration::from_millis(4), - }; - let presentation = PresentTiming { - window_id: frame.window_id, - present_start: start + Duration::from_millis(5), - present_end: start + Duration::from_millis(6), - animation_interval: None, - }; - let mut sealer = IntervalSealer::new(start); - - let snapshots = sealer.push_entries([ - ForegroundJournalEntry::Event(ForegroundEvent::Input(input)), - ForegroundJournalEntry::Event(ForegroundEvent::Draw(frame)), - ]); - assert!(snapshots.is_empty()); - - let snapshots = sealer.push_entries([ForegroundJournalEntry::Boundary( - IntervalBoundary::Presented(PresentedFrame { - frame, - presentation, - }), - )]); - let [snapshot] = snapshots.as_slice() else { - panic!("expected one presentation-sealed snapshot, got {snapshots:?}"); - }; - assert_eq!(snapshot.interval_start, input.start); - assert_eq!(snapshot.interval_end(), presentation.present_end); - assert_eq!(snapshot.events.len(), 3); - assert!(matches!( - snapshot.events.last(), - Some(ForegroundEvent::Present(timing)) - if timing.present_duration() == Duration::from_millis(1) - )); - assert_eq!( - match snapshot.boundary { - IntervalBoundary::Presented(presented) => { - presented.dirty_to_present_duration() - } - IntervalBoundary::Idle { .. } => None, - }, - Some(Duration::from_millis(5)) - ); - } - - #[test] - fn outermost_turn_seals_no_frame_work_when_idle() { - let start = Instant::now(); - let counter = ForegroundRunnableCounter::new(); - let (mut journal, mut collector) = test_journal(counter); - let events = [ - ForegroundEvent::Input(InputTiming { - kind: "test", - start, - end: start + Duration::from_millis(1), - caused_invalidation: false, - }), - ForegroundEvent::Action(ActionTiming { - name: "test.action", - start: start + Duration::from_millis(2), - end: start + Duration::from_millis(3), - }), - ForegroundEvent::TaskPoll(task_timing( - start + Duration::from_millis(4), - start + Duration::from_millis(5), - )), - ]; - - for event in events { - journal.begin_turn(); - journal.record_event(event); - journal.end_turn(event.end_time()); - } - - let entries = collector.collect_unseen().entries; - for event in events { - assert!(entries.iter().any(|entry| { - matches!( - entry, - ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { ended_at }) - if *ended_at == event.end_time() - ) - })); - } - } - - #[test] - fn nested_turns_only_seal_after_the_outermost_turn() { - let start = Instant::now(); - let (mut journal, mut collector) = test_journal(ForegroundRunnableCounter::new()); - let action = ForegroundEvent::Action(ActionTiming { - name: "test.action", - start, - end: start + Duration::from_millis(1), - }); - let input = ForegroundEvent::Input(InputTiming { - kind: "test", - start, - end: start + Duration::from_millis(2), - caused_invalidation: false, - }); - - journal.begin_turn(); - journal.begin_turn(); - journal.record_event(action); - journal.end_turn(action.end_time()); - assert!(!has_boundary_at( - &collector.collect_unseen().entries, - action.end_time() - )); - - journal.record_event(input); - journal.end_turn(input.end_time()); - assert!(collector.collect_unseen().entries.iter().any(|entry| { - matches!( - entry, - ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { ended_at }) - if *ended_at == input.end_time() - ) - })); - } - - #[test] - fn an_immediately_ready_runnable_prevents_an_idle_boundary_between_polls() { - let start = Instant::now(); - let counter = ForegroundRunnableCounter::new(); - let (mut journal, mut collector) = test_journal(counter.clone()); - counter.queued(); - counter.queued(); - - let first = ForegroundEvent::TaskPoll(task_timing(start, start + Duration::from_millis(1))); - journal.begin_turn(); - journal.record_event(first); - counter.finished(); - journal.end_turn(first.end_time()); - assert!(!has_boundary_at( - &collector.collect_unseen().entries, - first.end_time() - )); - - let second = ForegroundEvent::TaskPoll(task_timing( - start + Duration::from_millis(2), - start + Duration::from_millis(3), - )); - journal.begin_turn(); - journal.record_event(second); - counter.finished(); - journal.end_turn(second.end_time()); - assert!(collector.collect_unseen().entries.iter().any(|entry| { - matches!( - entry, - ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { ended_at }) - if *ended_at == second.end_time() - ) - })); - } - - #[test] - fn a_pending_frame_prevents_idle_until_presentation() { - let start = Instant::now(); - let window_id = WindowId::from(0xD17A); - let (mut journal, mut collector) = test_journal(ForegroundRunnableCounter::new()); - journal.record_frame_pending(window_id, start); - journal.begin_turn(); - journal.record_event(ForegroundEvent::Input(InputTiming { - kind: "test", - start, - end: start + Duration::from_millis(1), - caused_invalidation: true, - })); - let input_end = start + Duration::from_millis(1); - journal.end_turn(input_end); - assert!(!has_boundary_at( - &collector.collect_unseen().entries, - input_end - )); - - let frame = frame_timing(window_id, start, start + Duration::from_millis(2)); - let presentation = presentation_timing(window_id, start + Duration::from_millis(3)); - journal.record_present(presentation, Some(frame)); - assert!(collector.collect_unseen().entries.iter().any(|entry| { - matches!( - entry, - ForegroundJournalEntry::Boundary(IntervalBoundary::Presented(presented)) - if presented.frame.window_id == window_id - ) - })); - } - - #[test] - fn a_present_without_a_draw_clears_the_window_pending_state() { - let start = Instant::now(); - let presented_at = start + Duration::from_millis(1); - let window_id = WindowId::from(0xD17B); - let (mut journal, mut collector) = test_journal(ForegroundRunnableCounter::new()); - journal.record_frame_pending(window_id, start); - journal.begin_turn(); - journal.record_present(presentation_timing(window_id, presented_at), None); - journal.end_turn(presented_at); - - let entries = collector.collect_unseen().entries; - assert!(entries.iter().any(|entry| { - matches!( - entry, - ForegroundJournalEntry::Event(ForegroundEvent::Present(timing)) - if timing.window_id == window_id - ) - })); - assert!(has_boundary_at(&entries, presented_at)); - } - - /// A frame that outlives [`FRAME_DEADLINE`] no longer seals an interval - /// of its own: the work that starved it and its eventual presentation - /// stay in one interval, preserving the dirty-to-present association. - #[test] - fn a_presentation_after_the_deadline_seals_the_whole_interval() { - let start = Instant::now(); - let window_id = WindowId::from(0xDEA1); - let presented_at = start + FRAME_DEADLINE + Duration::from_millis(250); - let mut sealer = IntervalSealer::new(start); - let snapshots = sealer.push_entries([ - pending_frame(window_id, start), - input_entry(start, start + Duration::from_millis(20)), - ForegroundJournalEntry::Boundary(presented_boundary(window_id, start, presented_at)), - ]); - let [snapshot] = snapshots.as_slice() else { - panic!("expected one presented snapshot, got {snapshots:?}"); - }; - assert!(matches!( - snapshot.boundary, - IntervalBoundary::Presented(presented) - if presented.frame.window_id == window_id - && presented.dirty_to_present_duration() - == Some(presented_at.duration_since(start)) - )); - assert_eq!(snapshot.interval_end(), presented_at); - assert_eq!(snapshot.events.len(), 2); - } - - /// Frame expiry unblocks idle boundaries on the writer side: retained - /// work after the deadline seals at its turn's end even though the window - /// never presented. - #[test] - fn an_expired_frame_no_longer_blocks_idle_boundaries() { - let start = Instant::now(); - let window_id = WindowId::from(0xDEA2); - let (mut journal, mut collector) = test_journal(ForegroundRunnableCounter::new()); - journal.record_frame_pending(window_id, start); - - let blocked_end = start + Duration::from_millis(20); - journal.begin_turn(); - journal.record_event(ForegroundEvent::Input(InputTiming { - kind: "test", - start, - end: blocked_end, - caused_invalidation: true, - })); - journal.end_turn(blocked_end); - assert!(!has_boundary_at( - &collector.collect_unseen().entries, - blocked_end - )); - - let unblocked_end = start + FRAME_DEADLINE + Duration::from_millis(1); - journal.begin_turn(); - journal.record_event(ForegroundEvent::Input(InputTiming { - kind: "test", - start: start + FRAME_DEADLINE, - end: unblocked_end, - caused_invalidation: false, - })); - journal.end_turn(unblocked_end); - assert!(has_boundary_at( - &collector.collect_unseen().entries, - unblocked_end - )); - } - - /// Closing a window clears its pending frame, so idle boundaries resume - /// without waiting for the deadline. - #[test] - fn closing_a_window_unblocks_idle_boundaries() { - let start = Instant::now(); - let window_id = WindowId::from(0xDEA7); - let (mut journal, mut collector) = test_journal(ForegroundRunnableCounter::new()); - journal.record_frame_pending(window_id, start); - journal.record_window_closed(window_id, start + Duration::from_millis(1)); - - let event_end = start + Duration::from_millis(2); - journal.begin_turn(); - journal.record_event(ForegroundEvent::Input(InputTiming { - kind: "test", - start: start + Duration::from_millis(1), - end: event_end, - caused_invalidation: false, - })); - journal.end_turn(event_end); - assert!(has_boundary_at( - &collector.collect_unseen().entries, - event_end - )); - } - - /// One window presenting must not unblock idle boundaries while another - /// window's unexpired frame is still pending. - #[test] - fn presenting_one_window_does_not_clear_another_pending_window() { - let start = Instant::now(); - let first_window = WindowId::from(0xDEA5); - let second_window = WindowId::from(0xDEA6); - let (mut journal, mut collector) = test_journal(ForegroundRunnableCounter::new()); - journal.record_frame_pending(first_window, start); - journal.record_frame_pending(second_window, start + Duration::from_millis(100)); - journal.record_present( - presentation_timing(first_window, start + Duration::from_millis(500)), - Some(frame_timing( - first_window, - start, - start + Duration::from_millis(500), - )), - ); - - let event_end = start + Duration::from_millis(620); - journal.begin_turn(); - journal.record_event(ForegroundEvent::Input(InputTiming { - kind: "test", - start: start + Duration::from_millis(600), - end: event_end, - caused_invalidation: false, - })); - journal.end_turn(event_end); - - let entries = collector.collect_unseen().entries; - assert!(entries.iter().any(|entry| { - matches!( - entry, - ForegroundJournalEntry::Boundary(IntervalBoundary::Presented(presented)) - if presented.frame.window_id == first_window - ) - })); - assert!(!has_boundary_at(&entries, event_end)); - } - - #[test] - fn small_polls_are_flushed_immediately_before_a_retained_event() { - let start = Instant::now(); - let first = task_timing( - start + Duration::from_millis(1), - start + Duration::from_millis(1) + Duration::from_micros(20), - ); - let second = task_timing( - start + Duration::from_millis(3), - start + Duration::from_millis(3) + Duration::from_micros(30), - ); - let input = InputTiming { - kind: "test", - start: start + Duration::from_millis(5), - end: start + Duration::from_millis(6), - caused_invalidation: false, - }; - let (mut journal, mut collector) = test_journal(ForegroundRunnableCounter::new()); - - journal.fold_small_poll(first); - journal.fold_small_poll(second); - journal.record_event(ForegroundEvent::Input(input)); - - let drained = collector.collect_unseen(); - let input_index = drained - .entries - .iter() - .position(|entry| { - matches!( - entry, - ForegroundJournalEntry::Event(ForegroundEvent::Input(timing)) - if timing.start == input.start - ) - }) - .expect("input event should be retained"); - let Some(ForegroundJournalEntry::Event(ForegroundEvent::SmallPolls(flush))) = input_index - .checked_sub(1) - .and_then(|index| drained.entries.get(index)) - else { - panic!("small-poll summary should immediately precede the input event"); - }; - assert_eq!(flush.summary.count, 2); - assert_eq!(flush.summary.total, Duration::from_micros(50)); - assert_eq!(flush.since, first.start); - assert_eq!(flush.until, second.end.0); - } - - #[test] - fn small_polls_are_flushed_before_frame_state_changes() { - let start = Instant::now(); - let window_id = WindowId::from(0xDEA9); - let poll = task_timing(start, start + Duration::from_micros(50)); - let (mut journal, mut collector) = test_journal(ForegroundRunnableCounter::new()); - journal.fold_small_poll(poll); - journal.record_frame_pending(window_id, poll.end.0); - - let entries = collector.collect_unseen().entries; - let pending_index = entries - .iter() - .position(|entry| { - matches!( - entry, - ForegroundJournalEntry::FrameState(FrameStateChange::Pending { - window_id: pending_window, - .. - }) if *pending_window == window_id - ) - }) - .expect("pending frame should be retained"); - assert!(matches!( - pending_index - .checked_sub(1) - .and_then(|index| entries.get(index)), - Some(ForegroundJournalEntry::Event(ForegroundEvent::SmallPolls(flush))) - if flush.since == poll.start && flush.until == poll.end.0 - )); - } - - /// Sporadic wake-ups (a tiny folded poll after which the foreground goes - /// idle, repeatedly) must write nothing to the ring and must not accumulate - /// toward a later retained interval. - #[test] - fn sparse_small_polls_are_discarded_when_the_foreground_returns_to_idle() { - let start = Instant::now(); - let (mut journal, mut collector) = test_journal(ForegroundRunnableCounter::new()); - let mut tiny_wake = |ended_at: Instant| { - journal.begin_turn(); - journal.fold_small_poll(task_timing(ended_at - Duration::from_micros(50), ended_at)); - journal.end_turn(ended_at); - }; - - for second in 1..=160 { - tiny_wake(start + Duration::from_secs(second)); - } - let quiet = collector.collect_unseen().entries; - assert!(quiet.is_empty()); - - // A later retained event seals normally without inheriting the earlier - // folded-only wake-ups. - let retained = ForegroundEvent::TaskPoll(task_timing( - start + Duration::from_secs(161), - start + Duration::from_secs(161) + Duration::from_millis(1), - )); - journal.begin_turn(); - journal.record_event(retained); - journal.end_turn(retained.end_time()); - let entries = collector.collect_unseen().entries; - assert!(has_boundary_at(&entries, retained.end_time())); - assert!(!entries.iter().any(|entry| matches!( - entry, - ForegroundJournalEntry::Event(ForegroundEvent::SmallPolls(_)) - ))); - } - - #[test] - fn note_lost_is_reported_on_the_next_snapshot_only() { - let start = Instant::now(); - let mut sealer = IntervalSealer::new(start); - sealer.note_lost(7); - - let snapshots = sealer.push_entries([ - input_entry(start, start + Duration::from_millis(1)), - ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { - ended_at: start + Duration::from_millis(1), - }), - input_entry( - start + Duration::from_millis(2), - start + Duration::from_millis(3), - ), - ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { - ended_at: start + Duration::from_millis(3), - }), - ]); - - assert_eq!(snapshots.len(), 2); - assert_eq!(snapshots[0].dropped_events, 7); - assert_eq!(snapshots[1].dropped_events, 0); - } - - /// Ring losses must be able to surface even when every retained entry was - /// among the losses: a loss-only interval still seals at the next - /// boundary rather than sliding away as empty. - #[test] - fn losses_alone_seal_a_snapshot_at_the_next_boundary() { - let start = Instant::now(); - let mut sealer = IntervalSealer::new(start); - sealer.note_lost(3); - - let snapshots = - sealer.push_entries([ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { - ended_at: start + Duration::from_millis(1), - })]); - - assert_eq!(snapshots.len(), 1); - assert_eq!(snapshots[0].dropped_events, 3); - assert!(snapshots[0].events.is_empty()); - } - - #[test] - fn interval_event_cap_counts_overflowing_events() { - let start = Instant::now(); - let mut sealer = IntervalSealer::new(start); - let overflow = 5; - let mut entries: Vec = (0..(MAX_INTERVAL_EVENTS + overflow) as u64) - .map(|i| { - input_entry( - start + Duration::from_micros(i), - start + Duration::from_micros(i + 1), - ) - }) - .collect(); - entries.push(ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { - ended_at: start + Duration::from_micros((MAX_INTERVAL_EVENTS + overflow) as u64 + 1), - })); - - let snapshots = sealer.push_entries(entries); - - assert_eq!(snapshots.len(), 1); - assert_eq!(snapshots[0].events.len(), MAX_INTERVAL_EVENTS); - assert_eq!(snapshots[0].dropped_events, overflow as u64); - } - - #[test] - fn collect_unseen_returns_each_entry_once_in_order() { - let start = Instant::now(); - let (journal, mut publisher) = ForegroundJournal::new(32, 4); - let mut collector = journal.collector(); - let timestamps: Vec = (0..19) - .map(|i| start + Duration::from_micros(i as u64 + 1)) - .collect(); - publisher.publish(timestamps.iter().map(|&at| input_entry(at, at))); - - let ours = |entries: &[ForegroundJournalEntry]| -> Vec { - entries - .iter() - .filter_map(|entry| match entry { - ForegroundJournalEntry::Event(ForegroundEvent::Input(timing)) - if timestamps.contains(&timing.end) => - { - Some(timing.end) - } - _ => None, - }) - .collect() - }; - - let drained = collector.collect_unseen(); - assert_eq!(ours(&drained.entries), timestamps); - - // The cursor advanced past everything: a second drain sees none of - // our entries again. - let drained = collector.collect_unseen(); - assert!(ours(&drained.entries).is_empty()); - } - - #[test] - fn concurrent_collection_preserves_the_complete_logical_sequence() { - const ENTRY_COUNT: u64 = 20_000; - - let origin = Instant::now(); - let (journal, mut publisher) = ForegroundJournal::new(8, 4); - let mut collector = journal.collector(); - let done = Arc::new(std::sync::atomic::AtomicBool::new(false)); - let collector_done = Arc::clone(&done); - let collector_task = std::thread::spawn(move || { - let mut observed = Vec::new(); - loop { - let drained = collector.collect_unseen(); - observed.extend(normalize_ring_drain(origin, drained.entries)); - if collector_done.load(Ordering::Acquire) { - let drained = collector.collect_unseen(); - observed.extend(normalize_ring_drain(origin, drained.entries)); - return (collector, observed); - } - std::thread::yield_now(); - } - }); - - for sequence in 0..ENTRY_COUNT { - publisher.publish([ring_entry(origin, sequence)]); - } - done.store(true, Ordering::Release); - - let (mut collector, mut observed) = collector_task - .join() - .expect("collector thread should not panic"); - publisher.flush_pending(); - observed.extend(normalize_ring_drain( - origin, - collector.collect_unseen().entries, - )); - - let mut expected_sequence = 0; - for entry in observed { - match entry { - ModelDrainEntry::Entry(sequence) => { - assert_eq!(sequence, expected_sequence); - expected_sequence += 1; - } - ModelDrainEntry::Discontinuity(lost) => expected_sequence += lost, - } - } - assert_eq!(expected_sequence, ENTRY_COUNT); - assert_eq!( - publisher.ring.finalized.load(Ordering::Acquire), - ENTRY_COUNT - ); - } - - #[derive(Clone, Debug)] - enum RingOperation { - Publish, - Collect(u8), - NewCollector, - Pin(u8), - Unpin(u8), - } - - #[derive(Clone, Debug, PartialEq, Eq)] - enum ModelDrainEntry { - Entry(u64), - Discontinuity(u64), - } - - struct ModelRing { - capacity: usize, - pending_capacity: usize, - next_sequence: u64, - finalized: u64, - offered: u64, - slots: Vec>, - pinned: Vec, - pending: VecDeque<(u64, u64)>, - dropped_after_pending: u64, - } - - impl ModelRing { - fn new(capacity: usize, pending_capacity: usize) -> Self { - Self { - capacity, - pending_capacity, - next_sequence: 0, - finalized: 0, - offered: 0, - slots: vec![None; capacity], - pinned: vec![false; capacity], - pending: VecDeque::new(), - dropped_after_pending: 0, - } - } - - fn publish(&mut self) { - self.flush_pending(); - let sequence = self.next_sequence; - self.next_sequence += 1; - self.offered = self.next_sequence; - - if !self.pending.is_empty() || self.dropped_after_pending > 0 { - if self.dropped_after_pending == 0 && self.pending.len() < self.pending_capacity { - self.pending.push_back((sequence, sequence)); - } else { - self.dropped_after_pending += 1; - } - } else { - let index = sequence as usize % self.capacity; - if self.pinned[index] { - if self.pending_capacity > 0 { - self.pending.push_back((sequence, sequence)); - } else { - self.dropped_after_pending = 1; - } - } else { - self.slots[index] = Some((sequence, sequence)); - self.finalized = sequence + 1; - } - } - self.flush_pending(); - } - - fn flush_pending(&mut self) { - while let Some(&(sequence, value)) = self.pending.front() { - let index = sequence as usize % self.capacity; - if self.pinned[index] { - return; - } - self.slots[index] = Some((sequence, value)); - self.pending.pop_front(); - self.finalized = sequence + 1; - } - - if self.dropped_after_pending > 0 { - self.finalized = self.next_sequence; - self.dropped_after_pending = 0; - } - } - - fn collect(&self, cursor: &mut u64) -> (Vec, u64) { - let end = self.finalized; - if *cursor >= end { - return (Vec::new(), 0); - } - - let retained_start = end.saturating_sub(self.capacity as u64); - let mut lost = retained_start.saturating_sub(*cursor); - *cursor = (*cursor).max(retained_start); - let mut entries = Vec::new(); - if lost > 0 { - entries.push(ModelDrainEntry::Discontinuity(lost)); - } - while *cursor < end { - let index = *cursor as usize % self.capacity; - match self.slots[index] { - Some((sequence, value)) if sequence == *cursor => { - entries.push(ModelDrainEntry::Entry(value)); - } - _ => { - lost += 1; - match entries.last_mut() { - Some(ModelDrainEntry::Discontinuity(lost)) => *lost += 1, - _ => entries.push(ModelDrainEntry::Discontinuity(1)), - } - } - } - *cursor += 1; - } - (entries, lost) - } - } - - fn ring_entry(origin: Instant, sequence: u64) -> ForegroundJournalEntry { - let at = origin + Duration::from_micros(sequence); - input_entry(at, at) - } - - fn normalize_ring_drain( - origin: Instant, - entries: Vec, - ) -> Vec { - entries - .into_iter() - .map(|entry| match entry { - ForegroundJournalEntry::Event(ForegroundEvent::Input(timing)) => { - ModelDrainEntry::Entry(timing.start.duration_since(origin).as_micros() as u64) - } - ForegroundJournalEntry::Discontinuity { lost } => { - ModelDrainEntry::Discontinuity(lost) - } - other => panic!("ring property observed an unexpected entry: {other:?}"), - }) - .collect() - } - - #[derive(Debug, Clone, PartialEq, Eq)] - struct NormalizedEvent { - kind: u8, - start: u64, - end: u64, - } - - #[derive(Debug, Clone, PartialEq, Eq)] - struct NormalizedSmallPolls { - count: u64, - total_micros: u64, - since: u64, - until: u64, - } - - #[derive(Debug, Clone, PartialEq, Eq)] - struct NormalizedSnapshot { - interval_start: u64, - boundary_kind: u8, - interval_end: u64, - events: Vec, - small_polls: Vec, - dropped_events: u64, - journal_discontinuous: bool, - } - - #[derive(Debug, Clone, PartialEq, Eq)] - struct NormalizedSealerTail { - interval_start: u64, - events: Vec, - small_polls: Vec, - dropped_events: u64, - journal_discontinuous: bool, - } - - fn completion_order_entries( - origin: Instant, - specifications: &[(u8, u8, u8)], - ) -> Vec { - let mut completed_at_micros = 1u64; - specifications - .iter() - .map(|(advance, span, kind)| { - completed_at_micros += u64::from(*advance) + 1; - let started_at_micros = completed_at_micros.saturating_sub(u64::from(*span)); - let start = origin + Duration::from_micros(started_at_micros); - let end = origin + Duration::from_micros(completed_at_micros); - match kind % 7 { - 0 => input_entry(start, end), - 1 => ForegroundJournalEntry::Event(ForegroundEvent::Action(ActionTiming { - name: "test.action", - start, - end, - })), - 2 => ForegroundJournalEntry::Event(ForegroundEvent::TaskPoll(task_timing( - start, end, - ))), - 3 => { - ForegroundJournalEntry::Event(ForegroundEvent::SmallPolls(SmallPollFlush { - summary: PollSummary { - count: u64::from(*span % 5) + 1, - total: Duration::from_micros(u64::from(*span)), - }, - since: start, - until: end, - })) - } - 4 => pending_frame(WindowId::from(completed_at_micros), start), - 5 => ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { ended_at: end }), - _ => ForegroundJournalEntry::Boundary(presented_boundary( - WindowId::from(completed_at_micros), - start, - end, - )), - } - }) - .collect() - } - - fn normalize_event(origin: Instant, event: ForegroundEvent) -> NormalizedEvent { - let kind = match event { - ForegroundEvent::TaskPoll(_) => 0, - ForegroundEvent::Action(_) => 1, - ForegroundEvent::Input(_) => 2, - ForegroundEvent::Draw(_) => 3, - ForegroundEvent::Present(_) => 4, - ForegroundEvent::SmallPolls(_) => 5, - }; - NormalizedEvent { - kind, - start: event.start_time().duration_since(origin).as_micros() as u64, - end: event.end_time().duration_since(origin).as_micros() as u64, - } - } - - fn normalize_small_polls(origin: Instant, flush: SmallPollFlush) -> NormalizedSmallPolls { - NormalizedSmallPolls { - count: flush.summary.count, - total_micros: flush.summary.total.as_micros() as u64, - since: flush.since.duration_since(origin).as_micros() as u64, - until: flush.until.duration_since(origin).as_micros() as u64, - } - } - - fn normalize_snapshot(origin: Instant, snapshot: &FrameSnapshot) -> NormalizedSnapshot { - NormalizedSnapshot { - interval_start: snapshot.interval_start.duration_since(origin).as_micros() as u64, - boundary_kind: match snapshot.boundary { - IntervalBoundary::Idle { .. } => 0, - IntervalBoundary::Presented(_) => 1, - }, - interval_end: snapshot.interval_end().duration_since(origin).as_micros() as u64, - events: snapshot - .events - .iter() - .copied() - .map(|event| normalize_event(origin, event)) - .collect(), - small_polls: snapshot - .small_polls - .iter() - .copied() - .map(|flush| normalize_small_polls(origin, flush)) - .collect(), - dropped_events: snapshot.dropped_events, - journal_discontinuous: snapshot.journal_discontinuous, - } - } - - fn normalize_tail(origin: Instant, sealer: &IntervalSealer) -> NormalizedSealerTail { - NormalizedSealerTail { - interval_start: sealer.interval_start.duration_since(origin).as_micros() as u64, - events: sealer - .events - .iter() - .copied() - .map(|event| normalize_event(origin, event)) - .collect(), - small_polls: sealer - .small_polls - .iter() - .copied() - .map(|flush| normalize_small_polls(origin, flush)) - .collect(), - dropped_events: sealer.dropped_events, - journal_discontinuous: sealer.journal_discontinuous, - } - } - - fn reference_seal( - origin: Instant, - entries: &[ForegroundJournalEntry], - ) -> (Vec, NormalizedSealerTail) { - let mut interval_start = 0; - let mut events = Vec::new(); - let mut small_polls = Vec::new(); - let mut snapshots = Vec::new(); - let mut dropped_events = 0; - let mut journal_discontinuous = false; - - for entry in entries { - match *entry { - ForegroundJournalEntry::Event(event) => { - let event_start = event.start_time().duration_since(origin).as_micros() as u64; - if events.is_empty() - && small_polls.is_empty() - && dropped_events == 0 - && !journal_discontinuous - { - interval_start = interval_start.max(event_start); - } - match event { - ForegroundEvent::SmallPolls(flush) => { - small_polls.push(normalize_small_polls(origin, flush)); - } - event => events.push(normalize_event(origin, event)), - } - } - ForegroundJournalEntry::Boundary(boundary) => { - if let IntervalBoundary::Presented(presented) = boundary { - let present = ForegroundEvent::Present(presented.presentation); - if events.is_empty() - && small_polls.is_empty() - && dropped_events == 0 - && !journal_discontinuous - { - interval_start = - interval_start - .max(present.start_time().duration_since(origin).as_micros() - as u64); - } - events.push(normalize_event(origin, present)); - } - - let interval_end = - boundary.end_time().duration_since(origin).as_micros() as u64; - if events.is_empty() - && small_polls.is_empty() - && dropped_events == 0 - && !journal_discontinuous - { - interval_start = interval_start.max(interval_end); - } else { - snapshots.push(NormalizedSnapshot { - interval_start, - boundary_kind: match boundary { - IntervalBoundary::Idle { .. } => 0, - IntervalBoundary::Presented(_) => 1, - }, - interval_end, - events: std::mem::take(&mut events), - small_polls: std::mem::take(&mut small_polls), - dropped_events: std::mem::take(&mut dropped_events), - journal_discontinuous: std::mem::take(&mut journal_discontinuous), - }); - interval_start = interval_end; - } - } - ForegroundJournalEntry::FrameState(_) => {} - ForegroundJournalEntry::Discontinuity { lost } => { - dropped_events += lost; - journal_discontinuous = true; - } - } - } - - ( - snapshots, - NormalizedSealerTail { - interval_start, - events, - small_polls, - dropped_events, - journal_discontinuous, - }, - ) - } - - fn reference_occupancy_micros( - event_spans: &[(u8, u8)], - small_poll_spans: &[(u8, u8, u8)], - window_start: u8, - window_end: u8, - ) -> u64 { - let mut clamped_spans = event_spans - .iter() - .map(|&(first, second)| { - let start = first.min(second).max(window_start); - let end = first.max(second).min(window_end).max(start); - (start, end) - }) - .collect::>(); - clamped_spans.sort_unstable(); - - let mut occupied = 0u64; - let mut merged_until = None; - for (start, end) in clamped_spans { - let start = merged_until.map_or(start, |until| start.max(until)); - occupied += u64::from(end.saturating_sub(start)); - merged_until = Some(merged_until.map_or(end, |until| until.max(end))); - } - - occupied - + small_poll_spans - .iter() - .map(|&(first, second, factor)| { - let since = first.min(second); - let until = first.max(second); - if since == until { - if since >= window_start && since <= window_end { - u64::from(factor) - } else { - 0 - } - } else { - let overlap_start = since.max(window_start); - let overlap_end = until.min(window_end).max(overlap_start); - u64::from(overlap_end - overlap_start) * u64::from(factor) - } - }) - .sum::() - } - - #[derive(Debug, Clone)] - struct WriterOperation { - kind: u8, - advance: u16, - argument: u8, - } - - #[derive(Debug, Clone, PartialEq, Eq)] - enum WriterEntry { - Input(u64), - SmallPolls { - count: u64, - total_micros: u64, - since: u64, - until: u64, - }, - Pending { - window: u64, - at: u64, - }, - Closed { - window: u64, - at: u64, - }, - Present { - window: u64, - at: u64, - }, - Presented { - window: u64, - at: u64, - }, - Idle(u64), - } - - #[derive(Default)] - struct ModelSmallPolls { - count: u64, - total_micros: u64, - since: u64, - until: u64, - } - - #[derive(Default)] - struct ModelWriter { - turn_depth: usize, - runnables: usize, - pending_frames: HashMap, - retained_since_boundary: bool, - small_polls: Option, - entries: Vec, - } - - impl ModelWriter { - fn record_entry(&mut self, entry: WriterEntry) { - if let Some(small_polls) = self.small_polls.take() { - self.entries.push(WriterEntry::SmallPolls { - count: small_polls.count, - total_micros: small_polls.total_micros, - since: small_polls.since, - until: small_polls.until, - }); - } - let boundary = matches!(entry, WriterEntry::Presented { .. } | WriterEntry::Idle(_)); - self.entries.push(entry); - if boundary { - self.retained_since_boundary = false; - } - } - - fn record_input(&mut self, at: u64) { - self.retained_since_boundary = true; - self.record_entry(WriterEntry::Input(at)); - } - - fn fold_small_poll(&mut self, since: u64, until: u64) { - let small_polls = self.small_polls.get_or_insert(ModelSmallPolls { - since, - until, - ..ModelSmallPolls::default() - }); - small_polls.count += 1; - small_polls.total_micros += until - since; - small_polls.since = small_polls.since.min(since); - small_polls.until = small_polls.until.max(until); - } - - fn record_pending(&mut self, window: u64, at: u64) { - let should_record = self - .pending_frames - .get(&window) - .is_none_or(|previous| at.saturating_sub(*previous) >= 1_000_000); - if should_record { - self.pending_frames.insert(window, at); - self.record_entry(WriterEntry::Pending { window, at }); - } - } - - fn record_closed(&mut self, window: u64, at: u64) { - self.pending_frames.remove(&window); - self.record_entry(WriterEntry::Closed { window, at }); - } - - fn record_present(&mut self, window: u64, at: u64, has_frame: bool) { - if has_frame { - self.pending_frames.remove(&window); - self.record_entry(WriterEntry::Presented { window, at }); - } else { - self.pending_frames.remove(&window); - self.retained_since_boundary = true; - self.record_entry(WriterEntry::Present { window, at }); - } - } - - fn end_turn(&mut self, at: u64) { - if self.turn_depth == 0 { - return; - } - self.turn_depth -= 1; - if self.turn_depth > 0 || self.runnables > 0 { - return; - } - self.pending_frames - .retain(|_, dirty_at| at.saturating_sub(*dirty_at) < 1_000_000); - if !self.pending_frames.is_empty() { - return; - } - if !self.retained_since_boundary { - self.small_polls = None; - return; - } - self.record_entry(WriterEntry::Idle(at)); - } - } - - fn normalize_writer_entries( - origin: Instant, - entries: Vec, - ) -> Vec { - let at = |instant: Instant| instant.duration_since(origin).as_micros() as u64; - entries - .into_iter() - .map(|entry| match entry { - ForegroundJournalEntry::Event(ForegroundEvent::Input(timing)) => { - WriterEntry::Input(at(timing.end)) - } - ForegroundJournalEntry::Event(ForegroundEvent::SmallPolls(flush)) => { - WriterEntry::SmallPolls { - count: flush.summary.count, - total_micros: flush.summary.total.as_micros() as u64, - since: at(flush.since), - until: at(flush.until), - } - } - ForegroundJournalEntry::Event(ForegroundEvent::Present(timing)) => { - WriterEntry::Present { - window: timing.window_id.as_u64(), - at: at(timing.present_end), - } - } - ForegroundJournalEntry::FrameState(FrameStateChange::Pending { - window_id, - dirty_at, - }) => WriterEntry::Pending { - window: window_id.as_u64(), - at: at(dirty_at), - }, - ForegroundJournalEntry::FrameState(FrameStateChange::Closed { - window_id, - at: closed_at, - }) => WriterEntry::Closed { - window: window_id.as_u64(), - at: at(closed_at), - }, - ForegroundJournalEntry::Boundary(IntervalBoundary::Presented(presented)) => { - WriterEntry::Presented { - window: presented.frame.window_id.as_u64(), - at: at(presented.presentation.present_end), - } - } - ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { ended_at }) => { - WriterEntry::Idle(at(ended_at)) - } - other => panic!("writer property emitted an unexpected entry: {other:?}"), - }) - .collect() - } - - proptest! { - #![proptest_config(ProptestConfig { - failure_persistence: None, - ..ProptestConfig::default() - })] - - #[test] - fn ring_matches_reference_model_under_wrap_collisions_and_independent_cursors( - capacity in 1usize..=8, - pending_capacity in 0usize..=4, - operations in prop::collection::vec( - prop_oneof![ - 5 => Just(RingOperation::Publish), - 3 => any::().prop_map(RingOperation::Collect), - 1 => Just(RingOperation::NewCollector), - 2 => any::().prop_map(RingOperation::Pin), - 2 => any::().prop_map(RingOperation::Unpin), - ], - 1..=128, - ), - ) { - let origin = Instant::now(); - let (journal, mut publisher) = ForegroundJournal::new(capacity, pending_capacity); - let mut collectors = vec![journal.collector()]; - let mut model_collectors = vec![0]; - let mut model = ModelRing::new(capacity, pending_capacity); - let mut pins: Vec>> = - (0..capacity).map(|_| None).collect(); - - for operation in operations { - match operation { - RingOperation::Publish => { - let sequence = model.next_sequence; - publisher.publish([ring_entry(origin, sequence)]); - model.publish(); - } - RingOperation::Collect(collector) => { - let index = usize::from(collector) % collectors.len(); - let drained = collectors[index].collect_unseen(); - let (expected_entries, expected_lost) = - model.collect(&mut model_collectors[index]); - let observed_entries = normalize_ring_drain(origin, drained.entries); - prop_assert_eq!(observed_entries, expected_entries); - prop_assert_eq!(drained.lost, expected_lost); - } - RingOperation::NewCollector if collectors.len() < 4 => { - collectors.push(journal.collector()); - model_collectors.push(model.offered); - } - RingOperation::NewCollector => {} - RingOperation::Pin(slot) => { - let index = usize::from(slot) % capacity; - if pins[index].is_none() { - pins[index] = JournalSlotReadGuard::try_new(&journal.ring.slots[index]); - prop_assert!(pins[index].is_some()); - model.pinned[index] = true; - } - } - RingOperation::Unpin(slot) => { - let index = usize::from(slot) % capacity; - pins[index].take(); - model.pinned[index] = false; - } - } - - prop_assert_eq!(publisher.next_sequence, model.next_sequence); - prop_assert_eq!( - publisher.ring.offered.load(Ordering::Acquire), - model.offered - ); - prop_assert_eq!( - publisher.ring.finalized.load(Ordering::Acquire), - model.finalized - ); - prop_assert_eq!( - publisher - .pending - .iter() - .map(|entry| entry.sequence) - .collect::>(), - model - .pending - .iter() - .map(|(sequence, _)| *sequence) - .collect::>() - ); - prop_assert_eq!( - publisher.dropped_after_pending, - model.dropped_after_pending - ); - } - - for pin in &mut pins { - pin.take(); - } - model.pinned.fill(false); - publisher.flush_pending(); - model.flush_pending(); - - for (collector, model_cursor) in collectors.iter_mut().zip(&mut model_collectors) { - let drained = collector.collect_unseen(); - let (expected_entries, expected_lost) = model.collect(model_cursor); - let observed_entries = normalize_ring_drain(origin, drained.entries); - prop_assert_eq!(observed_entries, expected_entries); - prop_assert_eq!(drained.lost, expected_lost); - } - } - - #[test] - fn a_presentation_seals_exactly_once_regardless_of_delay( - present_delay_micros in 1u64..=2_000_000 - ) { - let start = Instant::now(); - let window_id = WindowId::from(0xDEA8); - let presented_at = start + Duration::from_micros(present_delay_micros); - let mut sealer = IntervalSealer::new(start); - let snapshots = sealer.push_entries([ - pending_frame(window_id, start), - input_entry(start, start + Duration::from_micros(1)), - ForegroundJournalEntry::Boundary(presented_boundary( - window_id, - start, - presented_at, - )), - ]); - - prop_assert_eq!(snapshots.len(), 1); - prop_assert!(matches!( - snapshots[0].boundary, - IntervalBoundary::Presented(_) - )); - prop_assert_eq!(snapshots[0].interval_end(), presented_at); - } - - #[test] - fn completion_order_sealer_matches_reference_under_arbitrary_batching( - specifications in prop::collection::vec((0u8..16, 0u8..128, any::()), 1..=96), - batch_sizes in prop::collection::vec(1usize..=12, 1..=24), - ) { - let origin = Instant::now(); - let entries = completion_order_entries(origin, &specifications); - let (expected_snapshots, expected_tail) = reference_seal(origin, &entries); - - let mut one_batch_sealer = IntervalSealer::new(origin); - let one_batch_snapshots = one_batch_sealer - .push_entries(entries.iter().copied()) - .iter() - .map(|snapshot| normalize_snapshot(origin, snapshot)) - .collect::>(); - prop_assert_eq!(&one_batch_snapshots, &expected_snapshots); - prop_assert_eq!(&normalize_tail(origin, &one_batch_sealer), &expected_tail); - - let mut batched_sealer = IntervalSealer::new(origin); - let mut batched_snapshots = Vec::new(); - let mut offset = 0; - let mut batch_index = 0; - while offset < entries.len() { - let batch_size = batch_sizes[batch_index % batch_sizes.len()]; - let batch_end = (offset + batch_size).min(entries.len()); - batched_snapshots.extend( - batched_sealer - .push_entries(entries[offset..batch_end].iter().copied()) - .iter() - .map(|snapshot| normalize_snapshot(origin, snapshot)), - ); - offset = batch_end; - batch_index += 1; - } - prop_assert_eq!(batched_snapshots, expected_snapshots); - prop_assert_eq!(normalize_tail(origin, &batched_sealer), expected_tail); - } - - #[test] - fn occupancy_matches_interval_union_and_fold_apportionment( - event_spans in prop::collection::vec((0u8..=128, 0u8..=128), 0..=16), - small_poll_spans in prop::collection::vec( - (0u8..=128, 0u8..=128, 0u8..=4), - 0..=12, - ), - window in (0u8..=128, 0u8..=128), - ) { - let origin = Instant::now(); - let window_start_micros = window.0.min(window.1); - let window_end_micros = window.0.max(window.1); - let events = event_spans - .iter() - .map(|&(first, second)| { - let start = origin + Duration::from_micros(u64::from(first.min(second))); - let end = origin + Duration::from_micros(u64::from(first.max(second))); - ForegroundEvent::Input(InputTiming { - kind: "test", - start, - end, - caused_invalidation: false, - }) - }) - .collect(); - let small_polls = small_poll_spans - .iter() - .map(|&(first, second, factor)| { - let since_micros = first.min(second); - let until_micros = first.max(second); - let span_micros = u64::from(until_micros - since_micros); - let total_micros = if span_micros == 0 { - u64::from(factor) - } else { - span_micros * u64::from(factor) - }; - SmallPollFlush { - summary: PollSummary { - count: 1, - total: Duration::from_micros(total_micros), - }, - since: origin + Duration::from_micros(u64::from(since_micros)), - until: origin + Duration::from_micros(u64::from(until_micros)), - } - }) - .collect(); - let snapshot = FrameSnapshot { - interval_start: origin, - boundary: IntervalBoundary::Idle { - ended_at: origin + Duration::from_micros(128), - }, - events, - small_polls, - dropped_events: 0, - journal_discontinuous: false, - }; - let expected_micros = reference_occupancy_micros( - &event_spans, - &small_poll_spans, - window_start_micros, - window_end_micros, - ); - let observed = snapshot.occupancy_within( - origin + Duration::from_micros(u64::from(window_start_micros)), - origin + Duration::from_micros(u64::from(window_end_micros)), - ); - - prop_assert_eq!(observed, Duration::from_micros(expected_micros)); - - let full_expected = - reference_occupancy_micros(&event_spans, &small_poll_spans, 0, 128); - prop_assert_eq!(snapshot.occupancy(), Duration::from_micros(full_expected)); - prop_assert!((0.0..=1.0).contains(&snapshot.busy_fraction())); - prop_assert_eq!( - snapshot.busy_fraction(), - (full_expected as f64 / 128.0).min(1.0) - ); - } - - #[test] - fn writer_matches_state_model_across_turn_frame_and_runnable_transitions( - operations in prop::collection::vec( - (any::(), any::(), any::()).prop_map( - |(kind, advance, argument)| WriterOperation { - kind, - advance, - argument, - }, - ), - 1..=128, - ), - ) { - let origin = Instant::now(); - let counter = ForegroundRunnableCounter::new(); - let (mut writer, mut collector) = test_journal(counter.clone()); - let mut model = ModelWriter::default(); - let mut at_micros = 100u64; - - for operation in operations { - if operation.advance % 16 == 0 { - at_micros += FRAME_DEADLINE.as_micros() as u64; - } else { - at_micros += u64::from(operation.advance) + 1; - } - let at = origin + Duration::from_micros(at_micros); - let window_id = WindowId::from(u64::from(operation.argument % 4)); - let window = window_id.as_u64(); - - match operation.kind % 10 { - 0 => { - writer.begin_turn(); - model.turn_depth += 1; - } - 1 if model.turn_depth > 0 => { - writer.end_turn(at); - model.end_turn(at_micros); - } - 1 => {} - 2 => { - writer.record_event(ForegroundEvent::Input(InputTiming { - kind: "test", - start: at, - end: at, - caused_invalidation: false, - })); - model.record_input(at_micros); - } - 3 => { - let duration_micros = u64::from(operation.argument % 100); - writer.fold_small_poll(task_timing( - at - Duration::from_micros(duration_micros), - at, - )); - model.fold_small_poll( - at_micros - duration_micros, - at_micros, - ); - } - 4 => { - counter.queued(); - model.runnables += 1; - } - 5 if model.runnables > 0 => { - counter.finished(); - model.runnables -= 1; - } - 5 => {} - 6 => { - writer.record_frame_pending(window_id, at); - model.record_pending(window, at_micros); - } - 7 => { - writer.record_window_closed(window_id, at); - model.record_closed(window, at_micros); - } - 8 => { - writer.record_present( - presentation_timing(window_id, at), - Some(frame_timing(window_id, at, at)), - ); - model.record_present(window, at_micros, true); - } - _ => { - writer.record_present(presentation_timing(window_id, at), None); - model.record_present(window, at_micros, false); - } - } - - let drained = collector.collect_unseen(); - prop_assert_eq!(drained.lost, 0); - prop_assert_eq!( - normalize_writer_entries(origin, drained.entries), - std::mem::take(&mut model.entries) - ); - prop_assert_eq!(writer.turn_depth, model.turn_depth); - prop_assert_eq!(writer.retained_since_boundary, model.retained_since_boundary); - prop_assert_eq!(writer.pending_frames.len(), model.pending_frames.len()); - } - } - } - - fn test_journal( - foreground_runnables: ForegroundRunnableCounter, - ) -> (ForegroundJournalWriter, ForegroundJournalCollector) { - let (journal, publisher) = ForegroundJournal::new(256, 8); - let collector = journal.collector(); - ( - ForegroundJournalWriter::new(foreground_runnables, publisher), - collector, - ) - } - - fn has_boundary_at(entries: &[ForegroundJournalEntry], at: Instant) -> bool { - entries.iter().any(|entry| { - matches!(entry, ForegroundJournalEntry::Boundary(boundary) if boundary.end_time() == at) - }) - } - - fn input_entry(start: Instant, end: Instant) -> ForegroundJournalEntry { - ForegroundJournalEntry::Event(ForegroundEvent::Input(InputTiming { - kind: "test", - start, - end, - caused_invalidation: false, - })) - } - - fn pending_frame(window_id: WindowId, dirty_at: Instant) -> ForegroundJournalEntry { - ForegroundJournalEntry::FrameState(FrameStateChange::Pending { - window_id, - dirty_at, - }) - } - - fn frame_timing(window_id: WindowId, dirty_at: Instant, draw_end: Instant) -> FrameTiming { - FrameTiming { - window_id, - dirty_at: Some(dirty_at), - invalidations: 1, - draw_start: draw_end, - draw_end, - } - } - - fn presentation_timing(window_id: WindowId, present_end: Instant) -> PresentTiming { - PresentTiming { - window_id, - present_start: present_end, - present_end, - animation_interval: None, - } - } - - fn presented_boundary( - window_id: WindowId, - dirty_at: Instant, - present_end: Instant, - ) -> IntervalBoundary { - IntervalBoundary::Presented(PresentedFrame { - frame: frame_timing(window_id, dirty_at, present_end), - presentation: presentation_timing(window_id, present_end), - }) - } - - fn task_timing(start: Instant, end: Instant) -> TaskTiming { - TaskTiming { - location: std::panic::Location::caller(), - spawned: SpawnTime(start), - start, - end: YieldTime(end), - } - } -} diff --git a/crates/gpui_pre/src/queue.rs b/crates/gpui_pre/src/queue.rs deleted file mode 100644 index 76f6d67..0000000 --- a/crates/gpui_pre/src/queue.rs +++ /dev/null @@ -1,435 +0,0 @@ -use std::{ - collections::VecDeque, - fmt, - iter::FusedIterator, - sync::{Arc, atomic::AtomicUsize}, -}; - -use rand::{Rng, SeedableRng, rngs::SmallRng}; - -use crate::Priority; - -struct PriorityQueues { - high_priority: VecDeque, - medium_priority: VecDeque, - low_priority: VecDeque, -} - -impl PriorityQueues { - fn is_empty(&self) -> bool { - self.high_priority.is_empty() - && self.medium_priority.is_empty() - && self.low_priority.is_empty() - } -} - -struct PriorityQueueState { - queues: parking_lot::Mutex>, - condvar: parking_lot::Condvar, - receiver_count: AtomicUsize, - sender_count: AtomicUsize, -} - -impl PriorityQueueState { - fn send(&self, priority: Priority, item: T) -> Result<(), SendError> { - if self - .receiver_count - .load(std::sync::atomic::Ordering::Relaxed) - == 0 - { - return Err(SendError(item)); - } - - let mut queues = self.queues.lock(); - Self::push(&mut queues, priority, item); - self.condvar.notify_one(); - Ok(()) - } - - fn spin_send(&self, priority: Priority, item: T) -> Result<(), SendError> { - if self - .receiver_count - .load(std::sync::atomic::Ordering::Relaxed) - == 0 - { - return Err(SendError(item)); - } - - let mut queues = loop { - if let Some(guard) = self.queues.try_lock() { - break guard; - } - std::hint::spin_loop(); - }; - Self::push(&mut queues, priority, item); - self.condvar.notify_one(); - Ok(()) - } - - fn push(queues: &mut PriorityQueues, priority: Priority, item: T) { - match priority { - Priority::RealtimeAudio => unreachable!( - "Realtime audio priority runs on a dedicated thread and is never queued" - ), - Priority::High => queues.high_priority.push_back(item), - Priority::Medium => queues.medium_priority.push_back(item), - Priority::Low => queues.low_priority.push_back(item), - }; - } - - fn recv<'a>(&'a self) -> Result>, RecvError> { - let mut queues = self.queues.lock(); - - let sender_count = self.sender_count.load(std::sync::atomic::Ordering::Relaxed); - if queues.is_empty() && sender_count == 0 { - return Err(crate::queue::RecvError); - } - - while queues.is_empty() { - self.condvar.wait(&mut queues); - } - - Ok(queues) - } - - fn try_recv<'a>( - &'a self, - ) -> Result>>, RecvError> { - let mut queues = self.queues.lock(); - - let sender_count = self.sender_count.load(std::sync::atomic::Ordering::Relaxed); - if queues.is_empty() && sender_count == 0 { - return Err(crate::queue::RecvError); - } - - if queues.is_empty() { - Ok(None) - } else { - Ok(Some(queues)) - } - } - - fn spin_try_recv<'a>( - &'a self, - ) -> Result>>, RecvError> { - let queues = loop { - if let Some(guard) = self.queues.try_lock() { - break guard; - } - std::hint::spin_loop(); - }; - - let sender_count = self.sender_count.load(std::sync::atomic::Ordering::Relaxed); - if queues.is_empty() && sender_count == 0 { - return Err(crate::queue::RecvError); - } - - if queues.is_empty() { - Ok(None) - } else { - Ok(Some(queues)) - } - } -} - -#[doc(hidden)] -pub struct PriorityQueueSender { - state: Arc>, -} - -impl PriorityQueueSender { - fn new(state: Arc>) -> Self { - Self { state } - } - - pub fn send(&self, priority: Priority, item: T) -> Result<(), SendError> { - self.state.send(priority, item)?; - Ok(()) - } - - pub fn spin_send(&self, priority: Priority, item: T) -> Result<(), SendError> { - self.state.spin_send(priority, item)?; - Ok(()) - } -} - -impl Drop for PriorityQueueSender { - fn drop(&mut self) { - self.state - .sender_count - .fetch_sub(1, std::sync::atomic::Ordering::AcqRel); - } -} - -#[doc(hidden)] -pub struct PriorityQueueReceiver { - state: Arc>, - rand: SmallRng, - disconnected: bool, -} - -impl Clone for PriorityQueueReceiver { - fn clone(&self) -> Self { - self.state - .receiver_count - .fetch_add(1, std::sync::atomic::Ordering::AcqRel); - Self { - state: Arc::clone(&self.state), - rand: SmallRng::seed_from_u64(0), - disconnected: self.disconnected, - } - } -} - -#[doc(hidden)] -pub struct SendError(pub T); - -impl fmt::Debug for SendError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("SendError").field(&self.0).finish() - } -} - -#[derive(Debug)] -#[doc(hidden)] -pub struct RecvError; - -#[allow(dead_code)] -impl PriorityQueueReceiver { - pub fn new() -> (PriorityQueueSender, Self) { - let state = PriorityQueueState { - queues: parking_lot::Mutex::new(PriorityQueues { - high_priority: VecDeque::new(), - medium_priority: VecDeque::new(), - low_priority: VecDeque::new(), - }), - condvar: parking_lot::Condvar::new(), - receiver_count: AtomicUsize::new(1), - sender_count: AtomicUsize::new(1), - }; - let state = Arc::new(state); - - let sender = PriorityQueueSender::new(Arc::clone(&state)); - - let receiver = PriorityQueueReceiver { - state, - rand: SmallRng::seed_from_u64(0), - disconnected: false, - }; - - (sender, receiver) - } - - /// Returns whether the queue currently contains no elements. - pub fn is_empty(&self) -> bool { - self.state.queues.lock().is_empty() - } - - /// Returns the number of queued elements across all priorities. - pub(crate) fn len(&self) -> usize { - let queues = self.state.queues.lock(); - queues.high_priority.len() + queues.medium_priority.len() + queues.low_priority.len() - } - - /// Tries to pop one element from the priority queue without blocking. - /// - /// This will early return if there are no elements in the queue. - /// - /// This method is best suited if you only intend to pop one element, for better performance - /// on large queues see [`Self::try_iter`] - /// - /// # Errors - /// - /// If the sender was dropped - pub fn try_pop(&mut self) -> Result, RecvError> { - self.pop_inner(false) - } - - pub fn spin_try_pop(&mut self) -> Result, RecvError> { - use Priority as P; - - let Some(mut queues) = self.state.spin_try_recv()? else { - return Ok(None); - }; - - let high = P::High.weight() * !queues.high_priority.is_empty() as u32; - let medium = P::Medium.weight() * !queues.medium_priority.is_empty() as u32; - let low = P::Low.weight() * !queues.low_priority.is_empty() as u32; - let mut mass = high + medium + low; - - if !queues.high_priority.is_empty() { - let flip = self.rand.random_ratio(P::High.weight(), mass); - if flip { - return Ok(queues.high_priority.pop_front()); - } - mass -= P::High.weight(); - } - - if !queues.medium_priority.is_empty() { - let flip = self.rand.random_ratio(P::Medium.weight(), mass); - if flip { - return Ok(queues.medium_priority.pop_front()); - } - mass -= P::Medium.weight(); - } - - if !queues.low_priority.is_empty() { - let flip = self.rand.random_ratio(P::Low.weight(), mass); - if flip { - return Ok(queues.low_priority.pop_front()); - } - } - - Ok(None) - } - - /// Pops an element from the priority queue blocking if necessary. - /// - /// This method is best suited if you only intend to pop one element, for better performance - /// on large queues see [`Self::iter``] - /// - /// # Errors - /// - /// If the sender was dropped - pub fn pop(&mut self) -> Result { - self.pop_inner(true).map(|e| e.unwrap()) - } - - /// Returns an iterator over the elements of the queue - /// this iterator will end when all elements have been consumed and will not wait for new ones. - pub fn try_iter(self) -> TryIter { - TryIter { - receiver: self, - ended: false, - } - } - - /// Returns an iterator over the elements of the queue - /// this iterator will wait for new elements if the queue is empty. - pub fn iter(self) -> Iter { - Iter(self) - } - - #[inline(always)] - // algorithm is the loaded die from biased coin from - // https://www.keithschwarz.com/darts-dice-coins/ - fn pop_inner(&mut self, block: bool) -> Result, RecvError> { - use Priority as P; - - let mut queues = if !block { - let Some(queues) = self.state.try_recv()? else { - return Ok(None); - }; - queues - } else { - self.state.recv()? - }; - - let high = P::High.weight() * !queues.high_priority.is_empty() as u32; - let medium = P::Medium.weight() * !queues.medium_priority.is_empty() as u32; - let low = P::Low.weight() * !queues.low_priority.is_empty() as u32; - let mut mass = high + medium + low; //% - - if !queues.high_priority.is_empty() { - let flip = self.rand.random_ratio(P::High.weight(), mass); - if flip { - return Ok(queues.high_priority.pop_front()); - } - mass -= P::High.weight(); - } - - if !queues.medium_priority.is_empty() { - let flip = self.rand.random_ratio(P::Medium.weight(), mass); - if flip { - return Ok(queues.medium_priority.pop_front()); - } - mass -= P::Medium.weight(); - } - - if !queues.low_priority.is_empty() { - let flip = self.rand.random_ratio(P::Low.weight(), mass); - if flip { - return Ok(queues.low_priority.pop_front()); - } - } - - Ok(None) - } -} - -impl Drop for PriorityQueueReceiver { - fn drop(&mut self) { - self.state - .receiver_count - .fetch_sub(1, std::sync::atomic::Ordering::AcqRel); - } -} - -#[doc(hidden)] -pub struct Iter(PriorityQueueReceiver); -impl Iterator for Iter { - type Item = T; - - fn next(&mut self) -> Option { - self.0.pop().ok() - } -} -impl FusedIterator for Iter {} - -#[doc(hidden)] -pub struct TryIter { - receiver: PriorityQueueReceiver, - ended: bool, -} -impl Iterator for TryIter { - type Item = Result; - - fn next(&mut self) -> Option { - if self.ended { - return None; - } - - let res = self.receiver.try_pop(); - self.ended = res.is_err(); - - res.transpose() - } -} -impl FusedIterator for TryIter {} - -#[cfg(test)] -mod tests { - use collections::HashSet; - - use super::*; - - #[test] - fn all_tasks_get_yielded() { - let (tx, mut rx) = PriorityQueueReceiver::new(); - tx.send(Priority::Medium, 20).unwrap(); - tx.send(Priority::High, 30).unwrap(); - tx.send(Priority::Low, 10).unwrap(); - tx.send(Priority::Medium, 21).unwrap(); - tx.send(Priority::High, 31).unwrap(); - - drop(tx); - - assert_eq!( - rx.iter().collect::>(), - [30, 31, 20, 21, 10].into_iter().collect::>() - ) - } - - #[test] - fn new_high_prio_task_get_scheduled_quickly() { - let (tx, mut rx) = PriorityQueueReceiver::new(); - for _ in 0..100 { - tx.send(Priority::Low, 1).unwrap(); - } - - assert_eq!(rx.pop().unwrap(), 1); - tx.send(Priority::High, 3).unwrap(); - assert_eq!(rx.pop().unwrap(), 3); - assert_eq!(rx.pop().unwrap(), 1); - } -} diff --git a/crates/gpui_pre/src/scene.rs b/crates/gpui_pre/src/scene.rs deleted file mode 100644 index 46c1acf..0000000 --- a/crates/gpui_pre/src/scene.rs +++ /dev/null @@ -1,1022 +0,0 @@ -// todo("windows"): remove -#![cfg_attr(windows, allow(dead_code))] - -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -use crate::{ - bounds_tree::BoundsTree, point, AtlasTextureId, AtlasTile, Background, Bounds, ContentMask, - Corners, Edges, Hsla, Pixels, Point, Radians, ScaledPixels, Size, -}; -use std::{ - fmt::Debug, - iter::Peekable, - ops::{Add, Range, Sub}, - slice, -}; - -#[allow(non_camel_case_types, unused)] -#[expect(missing_docs)] -pub type PathVertex_ScaledPixels = PathVertex; - -#[expect(missing_docs)] -pub type DrawOrder = u32; - -/// A boolean stored as a `u32` so that GPU-facing structs contain no -/// compiler-inserted padding bytes, which would be undefined behavior to -/// reinterpret as `&[u8]` when writing instance buffers. Guaranteed to be -/// `0` or `1` by construction; shaders read it as a `u32`/`uint`. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -#[repr(transparent)] -pub struct PaddedBool32(u32); - -impl From for PaddedBool32 { - fn from(value: bool) -> Self { - PaddedBool32(value as u32) - } -} - -#[derive(Default)] -#[expect(missing_docs)] -pub struct Scene { - /// Immutable rounded clip nodes referenced by primitive masks. - pub rounded_clips: Vec>, - clip_indices: std::collections::HashMap<[u32; 13], u32>, - pub(crate) paint_operations: Vec, - primitive_bounds: BoundsTree, - layer_stack: Vec, - pub shadows: Vec, - pub quads: Vec, - pub paths: Vec>, - pub underlines: Vec, - pub monochrome_sprites: Vec, - pub subpixel_sprites: Vec, - pub polychrome_sprites: Vec, - pub surfaces: Vec, -} - -#[expect(missing_docs)] -impl Scene { - pub fn clear(&mut self) { - self.rounded_clips.clear(); - self.clip_indices.clear(); - self.paint_operations.clear(); - self.primitive_bounds.clear(); - self.layer_stack.clear(); - self.paths.clear(); - self.shadows.clear(); - self.quads.clear(); - self.underlines.clear(); - self.monochrome_sprites.clear(); - self.subpixel_sprites.clear(); - self.polychrome_sprites.clear(); - self.surfaces.clear(); - } - - pub fn len(&self) -> usize { - self.paint_operations.len() - } - - /// Intern a clip node for this frame. Identical ancestor chains share nodes. - pub fn insert_clip(&mut self, clip: crate::RoundedClip) -> u32 { - assert!( - clip.parent as usize <= self.rounded_clips.len(), - "clip parent must already exist in this scene" - ); - let key = [ - clip.bounds.origin.x.0.to_bits(), - clip.bounds.origin.y.0.to_bits(), - clip.bounds.size.width.0.to_bits(), - clip.bounds.size.height.0.to_bits(), - clip.radii_x.top_left.0.to_bits(), - clip.radii_x.top_right.0.to_bits(), - clip.radii_x.bottom_right.0.to_bits(), - clip.radii_x.bottom_left.0.to_bits(), - clip.radii_y.top_left.0.to_bits(), - clip.radii_y.top_right.0.to_bits(), - clip.radii_y.bottom_right.0.to_bits(), - clip.radii_y.bottom_left.0.to_bits(), - clip.parent, - ]; - if let Some(index) = self.clip_indices.get(&key) { - return *index; - } - let index = u32::try_from(self.rounded_clips.len()).expect("scene clip index overflow") + 1; - self.rounded_clips.push(clip); - self.clip_indices.insert(key, index); - index - } - - fn import_clip(&mut self, mut index: u32, previous: &Scene) -> u32 { - let mut chain = Vec::new(); - while index != 0 { - let clip = previous.rounded_clips[index as usize - 1]; - index = clip.parent; - chain.push(clip); - } - let mut parent = 0; - for mut clip in chain.into_iter().rev() { - clip.parent = parent; - parent = self.insert_clip(clip); - } - parent - } - - pub fn push_layer(&mut self, bounds: Bounds) { - let order = self.primitive_bounds.insert(bounds); - self.layer_stack.push(order); - self.paint_operations - .push(PaintOperation::StartLayer(bounds)); - } - - pub fn pop_layer(&mut self) { - self.layer_stack.pop(); - self.paint_operations.push(PaintOperation::EndLayer); - } - - pub fn insert_primitive(&mut self, primitive: impl Into) { - let mut primitive = primitive.into(); - let clipped_bounds = primitive - .bounds() - .intersect(&primitive.content_mask().bounds); - - if clipped_bounds.is_empty() { - return; - } - - let order = self - .layer_stack - .last() - .copied() - .unwrap_or_else(|| self.primitive_bounds.insert(clipped_bounds)); - match &mut primitive { - Primitive::Shadow(shadow) => { - shadow.order = order; - self.shadows.push(*shadow); - } - Primitive::Quad(quad) => { - quad.order = order; - self.quads.push(*quad); - } - Primitive::Path(path) => { - path.order = order; - path.id = PathId(self.paths.len()); - self.paths.push(path.clone()); - } - Primitive::Underline(underline) => { - underline.order = order; - self.underlines.push(*underline); - } - Primitive::MonochromeSprite(sprite) => { - sprite.order = order; - self.monochrome_sprites.push(*sprite); - } - Primitive::SubpixelSprite(sprite) => { - sprite.order = order; - self.subpixel_sprites.push(*sprite); - } - Primitive::PolychromeSprite(sprite) => { - sprite.order = order; - self.polychrome_sprites.push(*sprite); - } - Primitive::Surface(surface) => { - surface.order = order; - self.surfaces.push(surface.clone()); - } - } - self.paint_operations - .push(PaintOperation::Primitive(primitive)); - } - - pub fn replay(&mut self, range: Range, prev_scene: &Scene) { - let mut clip_remapping = std::collections::HashMap::new(); - for operation in &prev_scene.paint_operations[range] { - match operation { - PaintOperation::Primitive(primitive) => { - let mut primitive = primitive.clone(); - let mask = primitive.content_mask_mut(); - if mask.clip_index != 0 { - mask.clip_index = *clip_remapping - .entry(mask.clip_index) - .or_insert_with(|| self.import_clip(mask.clip_index, prev_scene)); - } - self.insert_primitive(primitive); - } - PaintOperation::StartLayer(bounds) => self.push_layer(*bounds), - PaintOperation::EndLayer => self.pop_layer(), - } - } - } - - pub fn finish(&mut self) { - self.shadows.sort_by_key(|shadow| shadow.order); - self.quads.sort_by_key(|quad| quad.order); - self.paths.sort_by_key(|path| path.order); - self.underlines.sort_by_key(|underline| underline.order); - self.monochrome_sprites - .sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id)); - self.subpixel_sprites - .sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id)); - self.polychrome_sprites - .sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id)); - self.surfaces.sort_by_key(|surface| surface.order); - } - - #[cfg_attr( - all( - any(target_os = "linux", target_os = "freebsd"), - not(any(feature = "x11", feature = "wayland")) - ), - allow(dead_code) - )] - pub fn batches(&self) -> impl Iterator + '_ { - BatchIterator { - shadows_start: 0, - shadows_iter: self.shadows.iter().peekable(), - quads_start: 0, - quads_iter: self.quads.iter().peekable(), - paths_start: 0, - paths_iter: self.paths.iter().peekable(), - underlines_start: 0, - underlines_iter: self.underlines.iter().peekable(), - monochrome_sprites_start: 0, - monochrome_sprites_iter: self.monochrome_sprites.iter().peekable(), - subpixel_sprites_start: 0, - subpixel_sprites_iter: self.subpixel_sprites.iter().peekable(), - polychrome_sprites_start: 0, - polychrome_sprites_iter: self.polychrome_sprites.iter().peekable(), - surfaces_start: 0, - surfaces_iter: self.surfaces.iter().peekable(), - } - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Default)] -#[cfg_attr( - all( - any(target_os = "linux", target_os = "freebsd"), - not(any(feature = "x11", feature = "wayland")) - ), - allow(dead_code) -)] -pub(crate) enum PrimitiveKind { - Shadow, - #[default] - Quad, - Path, - Underline, - MonochromeSprite, - SubpixelSprite, - PolychromeSprite, - Surface, -} - -pub(crate) enum PaintOperation { - Primitive(Primitive), - StartLayer(Bounds), - EndLayer, -} - -#[derive(Clone)] -#[expect(missing_docs)] -pub enum Primitive { - Shadow(Shadow), - Quad(Quad), - Path(Path), - Underline(Underline), - MonochromeSprite(MonochromeSprite), - SubpixelSprite(SubpixelSprite), - PolychromeSprite(PolychromeSprite), - Surface(PaintSurface), -} - -#[expect(missing_docs)] -impl Primitive { - pub fn bounds(&self) -> &Bounds { - match self { - Primitive::Shadow(shadow) => &shadow.bounds, - Primitive::Quad(quad) => &quad.bounds, - Primitive::Path(path) => &path.bounds, - Primitive::Underline(underline) => &underline.bounds, - Primitive::MonochromeSprite(sprite) => &sprite.bounds, - Primitive::SubpixelSprite(sprite) => &sprite.bounds, - Primitive::PolychromeSprite(sprite) => &sprite.bounds, - Primitive::Surface(surface) => &surface.bounds, - } - } - - pub fn content_mask(&self) -> &ContentMask { - match self { - Primitive::Shadow(shadow) => &shadow.content_mask, - Primitive::Quad(quad) => &quad.content_mask, - Primitive::Path(path) => &path.content_mask, - Primitive::Underline(underline) => &underline.content_mask, - Primitive::MonochromeSprite(sprite) => &sprite.content_mask, - Primitive::SubpixelSprite(sprite) => &sprite.content_mask, - Primitive::PolychromeSprite(sprite) => &sprite.content_mask, - Primitive::Surface(surface) => &surface.content_mask, - } - } - - pub fn content_mask_mut(&mut self) -> &mut ContentMask { - match self { - Primitive::Shadow(shadow) => &mut shadow.content_mask, - Primitive::Quad(quad) => &mut quad.content_mask, - Primitive::Path(path) => &mut path.content_mask, - Primitive::Underline(underline) => &mut underline.content_mask, - Primitive::MonochromeSprite(sprite) => &mut sprite.content_mask, - Primitive::SubpixelSprite(sprite) => &mut sprite.content_mask, - Primitive::PolychromeSprite(sprite) => &mut sprite.content_mask, - Primitive::Surface(surface) => &mut surface.content_mask, - } - } -} - -#[cfg_attr( - all( - any(target_os = "linux", target_os = "freebsd"), - not(any(feature = "x11", feature = "wayland")) - ), - allow(dead_code) -)] -struct BatchIterator<'a> { - shadows_start: usize, - shadows_iter: Peekable>, - quads_start: usize, - quads_iter: Peekable>, - paths_start: usize, - paths_iter: Peekable>>, - underlines_start: usize, - underlines_iter: Peekable>, - monochrome_sprites_start: usize, - monochrome_sprites_iter: Peekable>, - subpixel_sprites_start: usize, - subpixel_sprites_iter: Peekable>, - polychrome_sprites_start: usize, - polychrome_sprites_iter: Peekable>, - surfaces_start: usize, - surfaces_iter: Peekable>, -} - -impl<'a> Iterator for BatchIterator<'a> { - type Item = PrimitiveBatch; - - fn next(&mut self) -> Option { - let mut orders_and_kinds = [ - ( - self.shadows_iter.peek().map(|s| s.order), - PrimitiveKind::Shadow, - ), - (self.quads_iter.peek().map(|q| q.order), PrimitiveKind::Quad), - (self.paths_iter.peek().map(|q| q.order), PrimitiveKind::Path), - ( - self.underlines_iter.peek().map(|u| u.order), - PrimitiveKind::Underline, - ), - ( - self.monochrome_sprites_iter.peek().map(|s| s.order), - PrimitiveKind::MonochromeSprite, - ), - ( - self.subpixel_sprites_iter.peek().map(|s| s.order), - PrimitiveKind::SubpixelSprite, - ), - ( - self.polychrome_sprites_iter.peek().map(|s| s.order), - PrimitiveKind::PolychromeSprite, - ), - ( - self.surfaces_iter.peek().map(|s| s.order), - PrimitiveKind::Surface, - ), - ]; - orders_and_kinds.sort_by_key(|(order, kind)| (order.unwrap_or(u32::MAX), *kind)); - - let first = orders_and_kinds[0]; - let second = orders_and_kinds[1]; - let (batch_kind, max_order_and_kind) = if first.0.is_some() { - (first.1, (second.0.unwrap_or(u32::MAX), second.1)) - } else { - return None; - }; - - match batch_kind { - PrimitiveKind::Shadow => { - let shadows_start = self.shadows_start; - let mut shadows_end = shadows_start + 1; - self.shadows_iter.next(); - while self - .shadows_iter - .next_if(|shadow| (shadow.order, batch_kind) < max_order_and_kind) - .is_some() - { - shadows_end += 1; - } - self.shadows_start = shadows_end; - Some(PrimitiveBatch::Shadows(shadows_start..shadows_end)) - } - PrimitiveKind::Quad => { - let quads_start = self.quads_start; - let mut quads_end = quads_start + 1; - self.quads_iter.next(); - while self - .quads_iter - .next_if(|quad| (quad.order, batch_kind) < max_order_and_kind) - .is_some() - { - quads_end += 1; - } - self.quads_start = quads_end; - Some(PrimitiveBatch::Quads(quads_start..quads_end)) - } - PrimitiveKind::Path => { - let paths_start = self.paths_start; - let mut paths_end = paths_start + 1; - self.paths_iter.next(); - while self - .paths_iter - .next_if(|path| (path.order, batch_kind) < max_order_and_kind) - .is_some() - { - paths_end += 1; - } - self.paths_start = paths_end; - Some(PrimitiveBatch::Paths(paths_start..paths_end)) - } - PrimitiveKind::Underline => { - let underlines_start = self.underlines_start; - let mut underlines_end = underlines_start + 1; - self.underlines_iter.next(); - while self - .underlines_iter - .next_if(|underline| (underline.order, batch_kind) < max_order_and_kind) - .is_some() - { - underlines_end += 1; - } - self.underlines_start = underlines_end; - Some(PrimitiveBatch::Underlines(underlines_start..underlines_end)) - } - PrimitiveKind::MonochromeSprite => { - let texture_id = self.monochrome_sprites_iter.peek().unwrap().tile.texture_id; - let sprites_start = self.monochrome_sprites_start; - let mut sprites_end = sprites_start + 1; - self.monochrome_sprites_iter.next(); - while self - .monochrome_sprites_iter - .next_if(|sprite| { - (sprite.order, batch_kind) < max_order_and_kind - && sprite.tile.texture_id == texture_id - }) - .is_some() - { - sprites_end += 1; - } - self.monochrome_sprites_start = sprites_end; - Some(PrimitiveBatch::MonochromeSprites { - texture_id, - range: sprites_start..sprites_end, - }) - } - PrimitiveKind::SubpixelSprite => { - let texture_id = self.subpixel_sprites_iter.peek().unwrap().tile.texture_id; - let sprites_start = self.subpixel_sprites_start; - let mut sprites_end = sprites_start + 1; - self.subpixel_sprites_iter.next(); - while self - .subpixel_sprites_iter - .next_if(|sprite| { - (sprite.order, batch_kind) < max_order_and_kind - && sprite.tile.texture_id == texture_id - }) - .is_some() - { - sprites_end += 1; - } - self.subpixel_sprites_start = sprites_end; - Some(PrimitiveBatch::SubpixelSprites { - texture_id, - range: sprites_start..sprites_end, - }) - } - PrimitiveKind::PolychromeSprite => { - let texture_id = self.polychrome_sprites_iter.peek().unwrap().tile.texture_id; - let sprites_start = self.polychrome_sprites_start; - let mut sprites_end = sprites_start + 1; - self.polychrome_sprites_iter.next(); - while self - .polychrome_sprites_iter - .next_if(|sprite| { - (sprite.order, batch_kind) < max_order_and_kind - && sprite.tile.texture_id == texture_id - }) - .is_some() - { - sprites_end += 1; - } - self.polychrome_sprites_start = sprites_end; - Some(PrimitiveBatch::PolychromeSprites { - texture_id, - range: sprites_start..sprites_end, - }) - } - PrimitiveKind::Surface => { - let surfaces_start = self.surfaces_start; - let mut surfaces_end = surfaces_start + 1; - self.surfaces_iter.next(); - while self - .surfaces_iter - .next_if(|surface| (surface.order, batch_kind) < max_order_and_kind) - .is_some() - { - surfaces_end += 1; - } - self.surfaces_start = surfaces_end; - Some(PrimitiveBatch::Surfaces(surfaces_start..surfaces_end)) - } - } - } -} - -#[derive(Debug)] -#[cfg_attr( - all( - any(target_os = "linux", target_os = "freebsd"), - not(any(feature = "x11", feature = "wayland")) - ), - allow(dead_code) -)] -#[allow(missing_docs)] -pub enum PrimitiveBatch { - Shadows(Range), - Quads(Range), - Paths(Range), - Underlines(Range), - MonochromeSprites { - texture_id: AtlasTextureId, - range: Range, - }, - #[cfg_attr(target_os = "macos", allow(dead_code))] - SubpixelSprites { - texture_id: AtlasTextureId, - range: Range, - }, - PolychromeSprites { - texture_id: AtlasTextureId, - range: Range, - }, - Surfaces(Range), -} - -impl PrimitiveBatch { - #[expect(missing_docs)] - pub fn label(&self) -> String { - match self { - Self::Shadows(range) => format!("shadows ({})", range.len()), - Self::Quads(range) => format!("quads ({})", range.len()), - Self::Paths(range) => format!("paths ({})", range.len()), - Self::Underlines(range) => format!("underlines ({})", range.len()), - Self::MonochromeSprites { texture_id, range } => { - format!( - "monochrome sprites ({}) on atlas {}", - range.len(), - texture_id.index - ) - } - Self::SubpixelSprites { texture_id, range } => { - format!( - "subpixel sprites ({}) on atlas {}", - range.len(), - texture_id.index - ) - } - Self::PolychromeSprites { texture_id, range } => { - format!( - "polychrome sprites ({}) on atlas {}", - range.len(), - texture_id.index - ) - } - Self::Surfaces(range) => format!("surfaces ({})", range.len()), - } - } -} - -#[derive(Default, Debug, Copy, Clone)] -#[repr(C)] -#[expect(missing_docs)] -pub struct Quad { - pub order: DrawOrder, - pub border_style: BorderStyle, - pub bounds: Bounds, - pub content_mask: ContentMask, - pub background: Background, - pub border_color: Hsla, - pub corner_radii: Corners, - pub border_widths: Edges, -} - -impl From for Primitive { - fn from(quad: Quad) -> Self { - Primitive::Quad(quad) - } -} - -#[derive(Debug, Copy, Clone)] -#[repr(C)] -#[expect(missing_docs)] -pub struct Underline { - pub order: DrawOrder, - pub pad: u32, // align to 8 bytes - pub bounds: Bounds, - pub content_mask: ContentMask, - pub color: Hsla, - pub thickness: ScaledPixels, - pub wavy: PaddedBool32, -} - -impl From for Primitive { - fn from(underline: Underline) -> Self { - Primitive::Underline(underline) - } -} - -#[derive(Debug, Copy, Clone)] -#[repr(C)] -#[expect(missing_docs)] -pub struct Shadow { - pub order: DrawOrder, - pub blur_radius: ScaledPixels, - pub bounds: Bounds, - pub corner_radii: Corners, - pub content_mask: ContentMask, - pub color: Hsla, - pub element_bounds: Bounds, - pub element_corner_radii: Corners, - /// 0 = drop shadow (rendered outside the element), 1 = inset shadow (rendered inside). - pub inset: u32, - pub pad: u32, // align to 8 bytes -} - -impl From for Primitive { - fn from(shadow: Shadow) -> Self { - Primitive::Shadow(shadow) - } -} - -/// The style of a border. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] -#[repr(C)] -pub enum BorderStyle { - /// A solid border. - #[default] - Solid = 0, - /// A dashed border. - Dashed = 1, -} - -/// A data type representing a 2 dimensional transformation that can be applied to an element. -#[derive(Debug, Clone, Copy, PartialEq)] -#[repr(C)] -pub struct TransformationMatrix { - /// 2x2 matrix containing rotation and scale, - /// stored row-major - pub rotation_scale: [[f32; 2]; 2], - /// translation vector - pub translation: [f32; 2], -} - -impl Eq for TransformationMatrix {} - -impl TransformationMatrix { - /// The unit matrix, has no effect. - pub fn unit() -> Self { - Self { - rotation_scale: [[1.0, 0.0], [0.0, 1.0]], - translation: [0.0, 0.0], - } - } - - /// Move the origin by a given point - pub fn translate(mut self, point: Point) -> Self { - self.compose(Self { - rotation_scale: [[1.0, 0.0], [0.0, 1.0]], - translation: [point.x.0, point.y.0], - }) - } - - /// Clockwise rotation in radians around the origin - pub fn rotate(self, angle: Radians) -> Self { - self.compose(Self { - rotation_scale: [ - [angle.0.cos(), -angle.0.sin()], - [angle.0.sin(), angle.0.cos()], - ], - translation: [0.0, 0.0], - }) - } - - /// Scale around the origin - pub fn scale(self, size: Size) -> Self { - self.compose(Self { - rotation_scale: [[size.width, 0.0], [0.0, size.height]], - translation: [0.0, 0.0], - }) - } - - /// Perform matrix multiplication with another transformation - /// to produce a new transformation that is the result of - /// applying both transformations: first, `other`, then `self`. - #[inline] - pub fn compose(self, other: TransformationMatrix) -> TransformationMatrix { - if other == Self::unit() { - return self; - } - // Perform matrix multiplication - TransformationMatrix { - rotation_scale: [ - [ - self.rotation_scale[0][0] * other.rotation_scale[0][0] - + self.rotation_scale[0][1] * other.rotation_scale[1][0], - self.rotation_scale[0][0] * other.rotation_scale[0][1] - + self.rotation_scale[0][1] * other.rotation_scale[1][1], - ], - [ - self.rotation_scale[1][0] * other.rotation_scale[0][0] - + self.rotation_scale[1][1] * other.rotation_scale[1][0], - self.rotation_scale[1][0] * other.rotation_scale[0][1] - + self.rotation_scale[1][1] * other.rotation_scale[1][1], - ], - ], - translation: [ - self.translation[0] - + self.rotation_scale[0][0] * other.translation[0] - + self.rotation_scale[0][1] * other.translation[1], - self.translation[1] - + self.rotation_scale[1][0] * other.translation[0] - + self.rotation_scale[1][1] * other.translation[1], - ], - } - } - - /// Apply transformation to a point, mainly useful for debugging - pub fn apply(&self, point: Point) -> Point { - let input = [point.x.0, point.y.0]; - let mut output = self.translation; - for (i, output_cell) in output.iter_mut().enumerate() { - for (k, input_cell) in input.iter().enumerate() { - *output_cell += self.rotation_scale[i][k] * *input_cell; - } - } - Point::new(output[0].into(), output[1].into()) - } -} - -impl Default for TransformationMatrix { - fn default() -> Self { - Self::unit() - } -} - -#[derive(Copy, Clone, Debug)] -#[repr(C)] -#[expect(missing_docs)] -pub struct MonochromeSprite { - pub order: DrawOrder, - pub pad: u32, - pub bounds: Bounds, - pub content_mask: ContentMask, - pub color: Hsla, - pub tile: AtlasTile, - pub transformation: TransformationMatrix, -} - -impl From for Primitive { - fn from(sprite: MonochromeSprite) -> Self { - Primitive::MonochromeSprite(sprite) - } -} - -#[derive(Copy, Clone, Debug)] -#[repr(C)] -#[expect(missing_docs)] -pub struct SubpixelSprite { - pub order: DrawOrder, - pub pad: u32, // align to 8 bytes - pub bounds: Bounds, - pub content_mask: ContentMask, - pub color: Hsla, - pub tile: AtlasTile, - pub transformation: TransformationMatrix, -} - -impl From for Primitive { - fn from(sprite: SubpixelSprite) -> Self { - Primitive::SubpixelSprite(sprite) - } -} - -#[derive(Copy, Clone, Debug)] -#[repr(C)] -#[expect(missing_docs)] -pub struct PolychromeSprite { - pub order: DrawOrder, - pub pad: u32, - pub grayscale: PaddedBool32, - pub opacity: f32, - pub bounds: Bounds, - pub content_mask: ContentMask, - pub corner_radii: Corners, - pub tile: AtlasTile, -} - -impl From for Primitive { - fn from(sprite: PolychromeSprite) -> Self { - Primitive::PolychromeSprite(sprite) - } -} - -#[derive(Clone, Debug)] -#[allow(missing_docs)] -pub struct PaintSurface { - pub order: DrawOrder, - pub bounds: Bounds, - pub content_mask: ContentMask, - #[cfg(target_os = "macos")] - pub image_buffer: core_video::pixel_buffer::CVPixelBuffer, -} - -impl From for Primitive { - fn from(surface: PaintSurface) -> Self { - Primitive::Surface(surface) - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -#[expect(missing_docs)] -pub struct PathId(pub usize); - -/// A line made up of a series of vertices and control points. -#[derive(Clone, Debug)] -#[expect(missing_docs)] -pub struct Path { - pub id: PathId, - pub order: DrawOrder, - pub bounds: Bounds

, - pub content_mask: ContentMask

, - pub vertices: Vec>, - pub color: Background, - start: Point

, - current: Point

, - contour_count: usize, -} - -impl Path { - /// Create a new path with the given starting point. - pub fn new(start: Point) -> Self { - Self { - id: PathId(0), - order: DrawOrder::default(), - vertices: Vec::new(), - start, - current: start, - bounds: Bounds { - origin: start, - size: Default::default(), - }, - content_mask: Default::default(), - color: Default::default(), - contour_count: 0, - } - } - - /// Scale this path by the given factor. - pub fn scale(&self, factor: f32) -> Path { - Path { - id: self.id, - order: self.order, - bounds: self.bounds.scale(factor), - content_mask: self.content_mask.scale(factor), - vertices: self - .vertices - .iter() - .map(|vertex| vertex.scale(factor)) - .collect(), - start: self.start.map(|start| start.scale(factor)), - current: self.current.scale(factor), - contour_count: self.contour_count, - color: self.color, - } - } - - /// Move the start, current point to the given point. - pub fn move_to(&mut self, to: Point) { - self.contour_count += 1; - self.start = to; - self.current = to; - } - - /// Draw a straight line from the current point to the given point. - pub fn line_to(&mut self, to: Point) { - self.contour_count += 1; - if self.contour_count > 1 { - self.push_triangle( - (self.start, self.current, to), - (point(0., 1.), point(0., 1.), point(0., 1.)), - ); - } - self.current = to; - } - - /// Draw a curve from the current point to the given point, using the given control point. - pub fn curve_to(&mut self, to: Point, ctrl: Point) { - self.contour_count += 1; - if self.contour_count > 1 { - self.push_triangle( - (self.start, self.current, to), - (point(0., 1.), point(0., 1.), point(0., 1.)), - ); - } - - self.push_triangle( - (self.current, ctrl, to), - (point(0., 0.), point(0.5, 0.), point(1., 1.)), - ); - self.current = to; - } - - /// Push a triangle to the Path. - pub fn push_triangle( - &mut self, - xy: (Point, Point, Point), - st: (Point, Point, Point), - ) { - self.bounds = self - .bounds - .union(&Bounds { - origin: xy.0, - size: Default::default(), - }) - .union(&Bounds { - origin: xy.1, - size: Default::default(), - }) - .union(&Bounds { - origin: xy.2, - size: Default::default(), - }); - - self.vertices.push(PathVertex { - xy_position: xy.0, - st_position: st.0, - content_mask: Default::default(), - }); - self.vertices.push(PathVertex { - xy_position: xy.1, - st_position: st.1, - content_mask: Default::default(), - }); - self.vertices.push(PathVertex { - xy_position: xy.2, - st_position: st.2, - content_mask: Default::default(), - }); - } -} - -impl Path -where - T: Clone + Debug + Default + PartialEq + PartialOrd + Add + Sub, -{ - #[allow(unused)] - #[expect(missing_docs)] - pub fn clipped_bounds(&self) -> Bounds { - self.bounds.intersect(&self.content_mask.bounds) - } -} - -impl From> for Primitive { - fn from(path: Path) -> Self { - Primitive::Path(path) - } -} - -#[derive(Clone, Debug)] -#[repr(C)] -#[expect(missing_docs)] -pub struct PathVertex { - pub xy_position: Point

, - pub st_position: Point, - pub content_mask: ContentMask

, -} - -#[expect(missing_docs)] -impl PathVertex { - pub fn scale(&self, factor: f32) -> PathVertex { - PathVertex { - xy_position: self.xy_position.scale(factor), - st_position: self.st_position, - content_mask: self.content_mask.scale(factor), - } - } -} diff --git a/crates/gpui_pre/src/shared_uri.rs b/crates/gpui_pre/src/shared_uri.rs deleted file mode 100644 index e257aaf..0000000 --- a/crates/gpui_pre/src/shared_uri.rs +++ /dev/null @@ -1,25 +0,0 @@ -use derive_more::{Deref, DerefMut}; - -use crate::SharedString; - -/// A [`SharedString`] containing a URI. -#[derive(Deref, DerefMut, Default, PartialEq, Eq, Hash, Clone)] -pub struct SharedUri(SharedString); - -impl std::fmt::Debug for SharedUri { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -impl std::fmt::Display for SharedUri { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0.as_ref()) - } -} - -impl> From for SharedUri { - fn from(value: T) -> Self { - Self(value.into()) - } -} diff --git a/crates/gpui_pre/src/spring.rs b/crates/gpui_pre/src/spring.rs deleted file mode 100644 index efc8780..0000000 --- a/crates/gpui_pre/src/spring.rs +++ /dev/null @@ -1,830 +0,0 @@ -use std::{ops::RangeInclusive, time::Duration}; - -use crate::{Hsla, Pixels, Rems, Rgba}; - -const CRITICAL_DAMPING_TOLERANCE: f32 = 1e-4; -const DEFAULT_SPRING_EPSILON: f32 = 0.001; - -/// The physical parameters of a damped harmonic oscillator. -/// -/// `stiffness` and `mass` must be finite and positive. `damping` must be finite -/// and non-negative. -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct SpringConfig { - /// The spring stiffness, conventionally written as $k$. - pub stiffness: f32, - /// The viscous damping coefficient, conventionally written as $c$. - pub damping: f32, - /// The moving mass, conventionally written as $m$. - pub mass: f32, -} - -impl SpringConfig { - /// Creates a spring from its physical parameters. - pub const fn new(stiffness: f32, damping: f32, mass: f32) -> Self { - Self { - stiffness, - damping, - mass, - } - } - - /// Returns the natural angular frequency and damping ratio $(\omega_0, \zeta)$. - pub fn canonical(&self) -> (f32, f32) { - let natural_frequency = (self.stiffness / self.mass).sqrt(); - let damping_ratio = self.damping / (2.0 * (self.stiffness * self.mass).sqrt()); - (natural_frequency, damping_ratio) - } - - /// Advances a spring toward a target that remains fixed for `delta_time`. - /// - /// This analytic step is independent of frame rate and preserves velocity, - /// allowing an interrupted spring to be retargeted without restarting it. - pub fn step(&self, state: SpringState, target: f32, delta_time: f32) -> SpringState { - let propagator = self.propagator(delta_time); - let displacement = state.position - target; - - SpringState { - position: target + propagator[0][0] * displacement + propagator[0][1] * state.velocity, - velocity: propagator[1][0] * displacement + propagator[1][1] * state.velocity, - } - } - - /// Advances a spring toward a target moving at a constant velocity. - /// - /// A first-order hold avoids the frame-rate-dependent lag introduced by - /// treating a dragged target as stationary between frames. - pub fn step_ramp( - &self, - state: SpringState, - target: f32, - target_velocity: f32, - delta_time: f32, - ) -> SpringState { - let (natural_frequency, damping_ratio) = self.canonical(); - let steady_state_lag = -2.0 * damping_ratio * target_velocity / natural_frequency; - let displacement = state.position - target - steady_state_lag; - let velocity = state.velocity - target_velocity; - let propagator = self.propagator(delta_time); - let target = target + target_velocity * delta_time; - - SpringState { - position: target - + steady_state_lag - + propagator[0][0] * displacement - + propagator[0][1] * velocity, - velocity: target_velocity - + propagator[1][0] * displacement - + propagator[1][1] * velocity, - } - } - - /// Returns the exact state-transition matrix for a constant target. - /// - /// Materializing this matrix is useful when many springs share the same - /// configuration and frame delta. A matrix must not be reused when the - /// frame delta changes. - pub fn propagator(&self, delta_time: f32) -> [[f32; 2]; 2] { - let (natural_frequency, damping_ratio) = self.canonical(); - - if damping_ratio < 1.0 - CRITICAL_DAMPING_TOLERANCE { - let decay = damping_ratio * natural_frequency; - let damped_frequency = natural_frequency * (1.0 - damping_ratio * damping_ratio).sqrt(); - let exponential = (-decay * delta_time).exp(); - let (sine, cosine) = (damped_frequency * delta_time).sin_cos(); - let sine_over_frequency = sine / damped_frequency; - - [ - [ - exponential * (cosine + decay * sine_over_frequency), - exponential * sine_over_frequency, - ], - [ - -exponential * natural_frequency * natural_frequency * sine_over_frequency, - exponential * (cosine - decay * sine_over_frequency), - ], - ] - } else if damping_ratio > 1.0 + CRITICAL_DAMPING_TOLERANCE { - let root = (damping_ratio * damping_ratio - 1.0).sqrt(); - let root_sum = damping_ratio + root; - let slow_root = -natural_frequency / root_sum; - let fast_root = -natural_frequency * root_sum; - let denominator = slow_root - fast_root; - let slow_exponential = (slow_root * delta_time).exp(); - let fast_exponential = (fast_root * delta_time).exp(); - - [ - [ - (-fast_root * slow_exponential + slow_root * fast_exponential) / denominator, - (slow_exponential - fast_exponential) / denominator, - ], - [ - slow_root * fast_root * (fast_exponential - slow_exponential) / denominator, - (slow_root * slow_exponential - fast_root * fast_exponential) / denominator, - ], - ] - } else { - let exponential = (-natural_frequency * delta_time).exp(); - - [ - [ - exponential * (1.0 + natural_frequency * delta_time), - exponential * delta_time, - ], - [ - -exponential * natural_frequency * natural_frequency * delta_time, - exponential * (1.0 - natural_frequency * delta_time), - ], - ] - } - } - - /// Tests both displacement and velocity against a positional tolerance. - /// - /// Velocity is compared with `epsilon * natural_frequency`, giving it the - /// corresponding animated-units-per-second scale. - pub fn is_settled(&self, state: SpringState, target: f32, epsilon: f32) -> bool { - let (natural_frequency, _) = self.canonical(); - epsilon.is_finite() - && epsilon >= 0.0 - && (state.position - target).abs() <= epsilon - && state.velocity.abs() <= epsilon * natural_frequency - } - - /// Returns a conservative time after which the spring remains settled. - /// - /// An undamped spring has no finite settling time and returns - /// [`Duration::MAX`]. - pub fn settle_time(&self, state: SpringState, target: f32, epsilon: f32) -> Duration { - let displacement = state.position - target; - if displacement == 0.0 && state.velocity == 0.0 { - return Duration::ZERO; - } - - let (natural_frequency, damping_ratio) = self.canonical(); - if !natural_frequency.is_finite() - || natural_frequency <= 0.0 - || !damping_ratio.is_finite() - || damping_ratio <= 0.0 - || !epsilon.is_finite() - || epsilon <= 0.0 - { - return Duration::MAX; - } - - let velocity_threshold = epsilon * natural_frequency; - - if damping_ratio < 1.0 - CRITICAL_DAMPING_TOLERANCE { - let decay = damping_ratio * natural_frequency; - let damped_frequency = natural_frequency * (1.0 - damping_ratio * damping_ratio).sqrt(); - let sine_coefficient = (state.velocity + decay * displacement) / damped_frequency; - let position_envelope = displacement.hypot(sine_coefficient); - let velocity_cosine = damped_frequency * sine_coefficient - decay * displacement; - let velocity_sine = -damped_frequency * displacement - decay * sine_coefficient; - let velocity_envelope = velocity_cosine.hypot(velocity_sine); - - find_settle_time( - epsilon, - velocity_threshold, - 0.0, - natural_frequency, - move |time| { - let exponential = (-decay * time).exp(); - ( - position_envelope * exponential, - velocity_envelope * exponential, - ) - }, - ) - } else if damping_ratio > 1.0 + CRITICAL_DAMPING_TOLERANCE { - let root = (damping_ratio * damping_ratio - 1.0).sqrt(); - let root_sum = damping_ratio + root; - let slow_root = -natural_frequency / root_sum; - let fast_root = -natural_frequency * root_sum; - let denominator = slow_root - fast_root; - let slow_coefficient = (state.velocity - fast_root * displacement) / denominator; - let fast_coefficient = (slow_root * displacement - state.velocity) / denominator; - - find_settle_time( - epsilon, - velocity_threshold, - 0.0, - natural_frequency, - move |time| { - let slow_term = slow_coefficient.abs() * (slow_root * time).exp(); - let fast_term = fast_coefficient.abs() * (fast_root * time).exp(); - ( - slow_term + fast_term, - slow_root.abs() * slow_term + fast_root.abs() * fast_term, - ) - }, - ) - } else { - let linear_coefficient = state.velocity + natural_frequency * displacement; - let position_constant = displacement.abs(); - let position_linear = linear_coefficient.abs(); - let velocity_constant = (linear_coefficient - natural_frequency * displacement).abs(); - let velocity_linear = natural_frequency * linear_coefficient.abs(); - let position_decay_start = - envelope_decay_start(position_constant, position_linear, natural_frequency); - let velocity_decay_start = - envelope_decay_start(velocity_constant, velocity_linear, natural_frequency); - - find_settle_time( - epsilon, - velocity_threshold, - position_decay_start.max(velocity_decay_start), - natural_frequency, - move |time| { - let exponential = (-natural_frequency * time).exp(); - ( - (position_constant + position_linear * time) * exponential, - (velocity_constant + velocity_linear * time) * exponential, - ) - }, - ) - } - } -} - -/// The instantaneous position and velocity of a spring. -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub struct SpringState { - /// The current value in the animated unit. - pub position: f32, - /// The current value's change per second. - pub velocity: f32, -} - -/// A value that can be targeted by a one-dimensional spring. -/// -/// Implementations may project the spring coordinate into a richer output, -/// allowing a discrete state or a path through a multidimensional value to be -/// driven by one spring. -pub trait SpringTarget: 'static { - /// The value supplied to the spring animator. - type Output; - - /// Returns the target in the spring's coordinate space. - fn target(&self) -> f32; - - /// Projects a spring coordinate into the animated output. - fn resolve(&self, value: f32) -> Self::Output; -} - -impl SpringTarget for f32 { - type Output = f32; - - fn target(&self) -> f32 { - *self - } - - fn resolve(&self, value: f32) -> Self::Output { - value - } -} - -impl SpringTarget for Pixels { - type Output = Pixels; - - fn target(&self) -> f32 { - self.as_f32() - } - - fn resolve(&self, value: f32) -> Self::Output { - Pixels::from(value) - } -} - -impl SpringTarget for Rems { - type Output = Rems; - - fn target(&self) -> f32 { - self.0 - } - - fn resolve(&self, value: f32) -> Self::Output { - Rems(value) - } -} - -impl SpringTarget for bool { - type Output = AnimationPhase; - - fn target(&self) -> f32 { - if *self { 1.0 } else { 0.0 } - } - - fn resolve(&self, value: f32) -> Self::Output { - AnimationPhase(value) - } -} - -/// A potentially overshooting coordinate within an animation. -/// -/// Phases are not restricted to 0..1. Multi-stage animations can assign each -/// stage its own coordinate and interpolate over ranges such as 1..=2. -#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] -pub struct AnimationPhase( - /// The unbounded phase coordinate. - pub f32, -); - -impl AnimationPhase { - /// Restricts this phase to the bounds of a range. - pub fn clamp(self, range: RangeInclusive) -> Self { - let (first, second) = range.into_inner(); - Self(self.0.clamp(first.min(second), first.max(second))) - } - - /// Interpolates between values using 0 and 1 as their phase coordinates. - pub fn interpolate(self, from: T, to: T) -> T { - T::interpolate(from, to, self.0) - } - - /// Interpolates between values without extrapolating beyond 0 and 1. - pub fn interpolate_clamped(self, from: T, to: T) -> T { - T::interpolate(from, to, self.0.clamp(0.0, 1.0)) - } - - /// Interpolates between values assigned to arbitrary phase coordinates. - pub fn interpolate_between( - self, - range: RangeInclusive, - from: T, - to: T, - ) -> T { - let (start, end) = range.into_inner(); - let phase = if start == end { - if self.0 < start { 0.0 } else { 1.0 } - } else { - (self.0 - start) / (end - start) - }; - T::interpolate(from, to, phase) - } - - /// Interpolates over arbitrary phase coordinates without extrapolating. - pub fn interpolate_between_clamped( - self, - range: RangeInclusive, - from: T, - to: T, - ) -> T { - let (start, end) = range.into_inner(); - let phase = if start == end { - if self.0 < start { 0.0 } else { 1.0 } - } else { - ((self.0 - start) / (end - start)).clamp(0.0, 1.0) - }; - T::interpolate(from, to, phase) - } -} - -impl From for AnimationPhase { - fn from(value: f32) -> Self { - Self(value) - } -} - -impl From for AnimationPhase { - fn from(value: bool) -> Self { - Self(if value { 1.0 } else { 0.0 }) - } -} - -impl SpringTarget for AnimationPhase { - type Output = AnimationPhase; - - fn target(&self) -> f32 { - self.0 - } - - fn resolve(&self, value: f32) -> Self::Output { - Self(value) - } -} - -/// A value that supports linear interpolation and extrapolation. -pub trait Interpolate: Sized { - /// Resolves the value at `phase`, where 0 is `from` and 1 is `to`. - fn interpolate(from: Self, to: Self, phase: f32) -> Self; -} - -impl Interpolate for f32 { - fn interpolate(from: Self, to: Self, phase: f32) -> Self { - from + (to - from) * phase - } -} - -impl Interpolate for Pixels { - fn interpolate(from: Self, to: Self, phase: f32) -> Self { - from + (to - from) * phase - } -} - -impl Interpolate for Rems { - fn interpolate(from: Self, to: Self, phase: f32) -> Self { - from + (to - from) * phase - } -} - -impl Interpolate for Rgba { - fn interpolate(from: Self, to: Self, phase: f32) -> Self { - Self { - r: f32::interpolate(from.r, to.r, phase), - g: f32::interpolate(from.g, to.g, phase), - b: f32::interpolate(from.b, to.b, phase), - a: f32::interpolate(from.a, to.a, phase), - } - } -} - -impl Interpolate for Hsla { - fn interpolate(from: Self, to: Self, phase: f32) -> Self { - let hue_delta = (to.h - from.h + 0.5).rem_euclid(1.0) - 0.5; - Self { - h: (from.h + hue_delta * phase).rem_euclid(1.0), - s: f32::interpolate(from.s, to.s, phase), - l: f32::interpolate(from.l, to.l, phase), - a: f32::interpolate(from.a, to.a, phase), - } - } -} - -/// Controls how a spring advances and resolves its presentation value. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum SpringPlayback { - /// Advances toward the latest target, preserving velocity across retargets. - #[default] - Running, - /// Holds the current position and velocity until playback resumes. - Paused, - /// Holds the current position and discards velocity. - Stopped, - /// Snaps to the latest target and discards velocity. - Completed, - /// Returns to the initial value and discards velocity. - Cancelled, -} - -/// A stateful spring animation targeting a value or projected path. -#[derive(Clone, Debug)] -pub struct SpringAnimation { - pub(crate) config: SpringConfig, - pub(crate) target: T, - pub(crate) epsilon: f32, - pub(crate) initial: Option, - pub(crate) playback: SpringPlayback, -} - -impl SpringAnimation<()> { - /// Creates a spring animation builder. - pub fn new(config: SpringConfig) -> Self { - Self { - config, - target: (), - epsilon: DEFAULT_SPRING_EPSILON, - initial: None, - playback: SpringPlayback::Running, - } - } - - /// Sets the value or path targeted by this spring. - pub fn to(self, target: T) -> SpringAnimation { - let SpringAnimation { - config, - target: (), - epsilon, - initial, - playback, - } = self; - SpringAnimation { - config, - target, - epsilon, - initial, - playback, - } - } -} - -impl SpringAnimation { - /// Sets the settling tolerance in the target's scalar coordinate space. - pub fn with_epsilon(mut self, epsilon: f32) -> Self { - self.epsilon = epsilon; - self - } - - /// Sets how the spring advances or resolves its current value. - pub fn playback(mut self, playback: SpringPlayback) -> Self { - self.playback = playback; - self - } -} - -impl SpringAnimation { - /// Sets the coordinate used when this element has no prior spring state. - pub fn from(mut self, initial: T) -> Self { - self.initial = Some(initial.target()); - self - } -} - -/// Adapts a spring starting at zero with no velocity to GPUI's duration-based easing API. -/// -/// The returned easing can overshoot the normalized 0..1 output range. Retargeting -/// this duration-based form restarts the spring; use [`SpringConfig::step`] when -/// preserving velocity matters. -pub fn sampled_easing(config: SpringConfig, epsilon: f32) -> (Duration, impl Fn(f32) -> f32) { - let initial_state = SpringState { - position: 0.0, - velocity: 0.0, - }; - let duration = config.settle_time(initial_state, 1.0, epsilon); - let duration_seconds = duration.as_secs_f32(); - - (duration, move |progress| { - if progress <= 0.0 { - 0.0 - } else if progress >= 1.0 { - 1.0 - } else { - config - .step(initial_state, 1.0, progress * duration_seconds) - .position - } - }) -} - -fn envelope_decay_start(constant: f32, linear: f32, decay: f32) -> f32 { - if linear == 0.0 { - 0.0 - } else { - (1.0 / decay - constant / linear).max(0.0) - } -} - -fn find_settle_time( - position_threshold: f32, - velocity_threshold: f32, - decay_start: f32, - natural_frequency: f32, - envelope: impl Fn(f32) -> (f32, f32), -) -> Duration { - let is_below_threshold = |time| { - let (position, velocity) = envelope(time); - position <= position_threshold && velocity <= velocity_threshold - }; - - if is_below_threshold(decay_start) { - return duration_from_secs(decay_start); - } - - let mut lower_bound = decay_start; - let mut upper_bound = decay_start.max(natural_frequency.recip()); - while !is_below_threshold(upper_bound) { - lower_bound = upper_bound; - upper_bound *= 2.0; - if !upper_bound.is_finite() { - return Duration::MAX; - } - } - - for _ in 0..32 { - let midpoint = (lower_bound + upper_bound) / 2.0; - if is_below_threshold(midpoint) { - upper_bound = midpoint; - } else { - lower_bound = midpoint; - } - } - - duration_from_secs(upper_bound) -} - -fn duration_from_secs(seconds: f32) -> Duration { - if !seconds.is_finite() || seconds >= Duration::MAX.as_secs_f32() { - Duration::MAX - } else if seconds <= 0.0 { - Duration::ZERO - } else { - Duration::from_secs_f32(seconds) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - const EPSILON: f32 = 1e-4; - - #[test] - fn spring_targets_resolve_typed_outputs() { - assert_eq!(12.0_f32.target(), 12.0); - assert_eq!(12.0_f32.resolve(14.0), 14.0); - assert_eq!(Pixels::from(12.0).target(), 12.0); - assert_eq!(Pixels::from(12.0).resolve(14.0), Pixels::from(14.0)); - assert_eq!(false.target(), 0.0); - assert_eq!(true.target(), 1.0); - assert_eq!(true.resolve(1.25), AnimationPhase(1.25)); - } - - #[test] - fn animation_phases_interpolate_over_arbitrary_ranges() { - let phase = AnimationPhase(1.5); - assert_eq!(phase.interpolate_between(1.0..=2.0, 10.0, 20.0), 15.0); - assert_eq!( - phase.interpolate_between_clamped(2.0..=3.0, 10.0, 20.0), - 10.0 - ); - assert_eq!( - AnimationPhase(3.5).interpolate_between(2.0..=3.0, 10.0, 20.0), - 25.0 - ); - } - - #[test] - fn hsla_interpolation_takes_the_shortest_hue_path() { - let from = Hsla { - h: 0.9, - s: 0.5, - l: 0.5, - a: 1.0, - }; - let to = Hsla { - h: 0.1, - s: 1.0, - l: 0.75, - a: 0.5, - }; - let result = AnimationPhase(0.5).interpolate(from, to); - - assert!(result.h < EPSILON || (1.0 - result.h) < EPSILON); - assert!((result.s - 0.75).abs() < EPSILON); - assert!((result.l - 0.625).abs() < EPSILON); - assert!((result.a - 0.75).abs() < EPSILON); - } - - #[test] - fn propagators_compose_and_have_expected_determinant() { - for damping_ratio in [0.4, 1.0, 1.5] { - let natural_frequency = 12.0; - let config = SpringConfig::new( - natural_frequency * natural_frequency, - 2.0 * damping_ratio * natural_frequency, - 1.0, - ); - let first = config.propagator(0.013); - let second = config.propagator(0.021); - let combined = multiply(second, first); - let direct = config.propagator(0.034); - - for row in 0..2 { - for column in 0..2 { - assert!( - (combined[row][column] - direct[row][column]).abs() < 2e-4, - "{damping_ratio}: {combined:?} != {direct:?}" - ); - } - } - - let determinant = direct[0][0] * direct[1][1] - direct[0][1] * direct[1][0]; - let expected = (-2.0 * damping_ratio * natural_frequency * 0.034).exp(); - assert!((determinant - expected).abs() < 2e-4); - } - } - - #[test] - fn step_preserves_semigroup_for_every_damping_regime() { - let state = SpringState { - position: -3.0, - velocity: 5.0, - }; - for damping in [4.0, 20.0, 40.0] { - let config = SpringConfig::new(100.0, damping, 1.0); - let stepped = config.step(config.step(state, 7.0, 0.013), 7.0, 0.021); - let direct = config.step(state, 7.0, 0.034); - - assert!((stepped.position - direct.position).abs() < 2e-4); - assert!((stepped.velocity - direct.velocity).abs() < 2e-4); - } - } - - #[test] - fn ramp_tracks_steady_state_lag() { - let natural_frequency = 10.0; - let damping_ratio = 0.8; - let target_velocity = 3.0; - let config = SpringConfig::new( - natural_frequency * natural_frequency, - 2.0 * damping_ratio * natural_frequency, - 1.0, - ); - let lag = -2.0 * damping_ratio * target_velocity / natural_frequency; - let state = SpringState { - position: lag, - velocity: target_velocity, - }; - let next = config.step_ramp(state, 0.0, target_velocity, 0.25); - - assert!((next.position - (target_velocity * 0.25 + lag)).abs() < EPSILON); - assert!((next.velocity - target_velocity).abs() < EPSILON); - } - - #[test] - fn settling_requires_low_velocity() { - let config = SpringConfig::new(100.0, 10.0, 1.0); - assert!(!config.is_settled( - SpringState { - position: 1.0, - velocity: 1.0, - }, - 1.0, - 0.01, - )); - assert!(config.is_settled( - SpringState { - position: 1.005, - velocity: 0.05, - }, - 1.0, - 0.01, - )); - } - - #[test] - fn settle_time_is_conservative_for_every_damping_regime() { - let initial_state = SpringState { - position: -2.0, - velocity: 4.0, - }; - for damping in [4.0, 20.0, 40.0] { - let config = SpringConfig::new(100.0, damping, 1.0); - let duration = config.settle_time(initial_state, 3.0, 0.001); - assert_ne!(duration, Duration::MAX); - - for additional_time in [0.0, 0.1, 1.0] { - let state = - config.step(initial_state, 3.0, duration.as_secs_f32() + additional_time); - assert!( - config.is_settled(state, 3.0, 0.001), - "{damping}: {duration:?} produced {state:?}" - ); - } - } - } - - #[test] - fn settle_time_accounts_for_motion_outside_an_instantaneous_tolerance() { - let config = SpringConfig::new(100.0, 2.0, 1.0); - let state = SpringState { - position: 1.125, - velocity: 1.25, - }; - assert!(config.is_settled(state, 1.0, 0.125)); - - let duration = config.settle_time(state, 1.0, 0.125); - assert!(duration > Duration::ZERO); - assert!(config.is_settled(config.step(state, 1.0, duration.as_secs_f32()), 1.0, 0.125,)); - } - - #[test] - fn undamped_spring_never_settles() { - let config = SpringConfig::new(100.0, 0.0, 1.0); - assert_eq!( - config.settle_time( - SpringState { - position: 0.0, - velocity: 0.0, - }, - 1.0, - 0.001, - ), - Duration::MAX - ); - } - - #[test] - fn sampled_easing_has_exact_endpoints_and_can_overshoot() { - let config = SpringConfig::new(100.0, 6.0, 1.0); - let (duration, easing) = sampled_easing(config, 0.001); - - assert_ne!(duration, Duration::MAX); - assert_eq!(easing(0.0), 0.0); - assert_eq!(easing(1.0), 1.0); - assert!((1..100).any(|step| easing(step as f32 / 100.0) > 1.0)); - } - - fn multiply(left: [[f32; 2]; 2], right: [[f32; 2]; 2]) -> [[f32; 2]; 2] { - [ - [ - left[0][0] * right[0][0] + left[0][1] * right[1][0], - left[0][0] * right[0][1] + left[0][1] * right[1][1], - ], - [ - left[1][0] * right[0][0] + left[1][1] * right[1][0], - left[1][0] * right[0][1] + left[1][1] * right[1][1], - ], - ] - } -} diff --git a/crates/gpui_pre/src/style.rs b/crates/gpui_pre/src/style.rs deleted file mode 100644 index 49e3c18..0000000 --- a/crates/gpui_pre/src/style.rs +++ /dev/null @@ -1,1583 +0,0 @@ -use std::{ - hash::{Hash, Hasher}, - iter, mem, - ops::Range, -}; - -use crate::{ - black, phi, point, px, quad, rems, size, AbsoluteLength, App, Background, BackgroundTag, - BorderStyle, Bounds, Corners, CornersRefinement, CursorStyle, DefiniteLength, DevicePixels, - Edges, EdgesRefinement, Font, FontFallbacks, FontFeatures, FontStyle, FontWeight, GridLocation, - Hsla, Length, Pixels, Point, PointRefinement, Rgba, SharedString, Size, SizeRefinement, Styled, - TextRun, Window, -}; -use collections::HashSet; -use refineable::Refineable; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -/// Use this struct for interfacing with the 'debug_below' styling from your own elements. -/// If a parent element has this style set on it, then this struct will be set as a global in -/// GPUI. -#[cfg(debug_assertions)] -pub struct DebugBelow; - -#[cfg(debug_assertions)] -impl crate::Global for DebugBelow {} - -/// How to fit the image into the bounds of the element. -pub enum ObjectFit { - /// The image will be stretched to fill the bounds of the element. - Fill, - /// The image will be scaled to fit within the bounds of the element. - Contain, - /// The image will be scaled to cover the bounds of the element. - Cover, - /// The image will be scaled down to fit within the bounds of the element. - ScaleDown, - /// The image will maintain its original size. - None, -} - -impl ObjectFit { - /// Get the bounds of the image within the given bounds. - pub fn get_bounds( - &self, - bounds: Bounds, - image_size: Size, - ) -> Bounds { - let image_size = image_size.map(|dimension| Pixels::from(u32::from(dimension))); - let image_ratio = image_size.width / image_size.height; - let bounds_ratio = bounds.size.width / bounds.size.height; - - match self { - ObjectFit::Fill => bounds, - ObjectFit::Contain => { - let new_size = if bounds_ratio > image_ratio { - size( - image_size.width * (bounds.size.height / image_size.height), - bounds.size.height, - ) - } else { - size( - bounds.size.width, - image_size.height * (bounds.size.width / image_size.width), - ) - }; - - Bounds { - origin: point( - bounds.origin.x + (bounds.size.width - new_size.width) / 2.0, - bounds.origin.y + (bounds.size.height - new_size.height) / 2.0, - ), - size: new_size, - } - } - ObjectFit::ScaleDown => { - // Check if the image is larger than the bounds in either dimension. - if image_size.width > bounds.size.width || image_size.height > bounds.size.height { - // If the image is larger, use the same logic as Contain to scale it down. - let new_size = if bounds_ratio > image_ratio { - size( - image_size.width * (bounds.size.height / image_size.height), - bounds.size.height, - ) - } else { - size( - bounds.size.width, - image_size.height * (bounds.size.width / image_size.width), - ) - }; - - Bounds { - origin: point( - bounds.origin.x + (bounds.size.width - new_size.width) / 2.0, - bounds.origin.y + (bounds.size.height - new_size.height) / 2.0, - ), - size: new_size, - } - } else { - // If the image is smaller than or equal to the container, display it at its original size, - // centered within the container. - let original_size = size(image_size.width, image_size.height); - Bounds { - origin: point( - bounds.origin.x + (bounds.size.width - original_size.width) / 2.0, - bounds.origin.y + (bounds.size.height - original_size.height) / 2.0, - ), - size: original_size, - } - } - } - ObjectFit::Cover => { - let new_size = if bounds_ratio > image_ratio { - size( - bounds.size.width, - image_size.height * (bounds.size.width / image_size.width), - ) - } else { - size( - image_size.width * (bounds.size.height / image_size.height), - bounds.size.height, - ) - }; - - Bounds { - origin: point( - bounds.origin.x + (bounds.size.width - new_size.width) / 2.0, - bounds.origin.y + (bounds.size.height - new_size.height) / 2.0, - ), - size: new_size, - } - } - ObjectFit::None => Bounds { - origin: bounds.origin, - size: image_size, - }, - } - } -} - -/// The minimum size of a column or row in a grid layout -#[derive( - Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Default, JsonSchema, Serialize, Deserialize, -)] -pub enum GridTemplateMinSize { - /// The column or row size may be 0 - #[default] - Zero, - /// The column or row size can be determined by the min content - MinContent, - /// The column or row size can be determined by the max content - MaxContent, -} - -/// A simplified representation of the grid-template-* value -#[derive( - Copy, - Clone, - Refineable, - PartialEq, - Eq, - PartialOrd, - Ord, - Debug, - Default, - JsonSchema, - Serialize, - Deserialize, -)] -pub struct GridTemplate { - /// How this template directive should be repeated - pub repeat: u16, - /// The minimum size in the repeat(<>, minmax(_, 1fr)) equation - pub min_size: GridTemplateMinSize, -} - -/// The CSS styling that can be applied to an element via the `Styled` trait -#[derive(Clone, Refineable, Debug)] -#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct Style { - /// What layout strategy should be used? - pub display: Display, - - /// Should the element be painted on screen? - pub visibility: Visibility, - - // Overflow properties - /// How children overflowing their container should affect layout - #[refineable] - pub overflow: Point, - /// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes. - pub scrollbar_width: AbsoluteLength, - /// Whether both x and y axis should be scrollable at the same time. - pub allow_concurrent_scroll: bool, - /// Whether scrolling should be restricted to the input gesture's axis. - /// - /// Pixel-based scroll gestures are locked to their initially dominant axis. The lock may be - /// released when the gesture changes direction strongly. Touch phases delimit gestures when - /// available, with a timeout fallback for platforms that only emit moved events. - /// - /// This also prevents input from being remapped to another axis. For example, horizontal input - /// will not scroll a container that only has vertical overflow enabled. Mouse wheel platforms - /// typically report ordinary wheel input on the Y axis and Shift-modified input on the X axis. - /// - /// ## Motivation - /// - /// On the web when scrolling with the mouse wheel, scrolling up and down will always scroll the Y axis, even when - /// the mouse is over a horizontally-scrollable element. - /// - /// The only way to scroll horizontally is to hold down `Shift` while scrolling, which then changes the scroll axis - /// to the X axis. - /// - /// Currently, GPUI operates differently from the web in that it will scroll an element in either the X or Y axis - /// when scrolling with just the mouse wheel. This causes problems when scrolling in a vertical list that contains - /// horizontally-scrollable elements, as when you get to the horizontally-scrollable elements the scroll will be - /// hijacked. - /// - /// Ideally we would match the web's behavior and not have a need for this, but right now we're adding this opt-in - /// style property to limit the potential blast radius. - pub restrict_scroll_to_axis: bool, - - // Position properties - /// What should the `position` value of this struct use as a base offset? - pub position: Position, - /// How should the position of this element be tweaked relative to the layout defined? - #[refineable] - pub inset: Edges, - - // Size properties - /// Sets the initial size of the item - #[refineable] - pub size: Size, - /// Controls the minimum size of the item - #[refineable] - pub min_size: Size, - /// Controls the maximum size of the item - #[refineable] - pub max_size: Size, - /// Sets the preferred aspect ratio for the item. The ratio is calculated as width divided by height. - pub aspect_ratio: Option, - - // Spacing Properties - /// How large should the margin be on each side? - #[refineable] - pub margin: Edges, - /// How large should the padding be on each side? - #[refineable] - pub padding: Edges, - /// How large should the border be on each side? - #[refineable] - pub border_widths: Edges, - - // Alignment properties - /// How this node's children aligned in the cross/block axis? - pub align_items: Option, - /// How this node should be aligned in the cross/block axis. Falls back to the parents [`AlignItems`] if not set - pub align_self: Option, - /// How should content contained within this item be aligned in the cross/block axis - pub align_content: Option, - /// How should contained within this item be aligned in the main/inline axis - pub justify_content: Option, - /// How large should the gaps between items in a flex container be? - #[refineable] - pub gap: Size, - - // Flexbox properties - /// Which direction does the main axis flow in? - pub flex_direction: FlexDirection, - /// Should elements wrap, or stay in a single line? - pub flex_wrap: FlexWrap, - /// Sets the initial main axis size of the item - pub flex_basis: Length, - /// The relative rate at which this item grows when it is expanding to fill space, 0.0 is the default value, and this value must be positive. - pub flex_grow: f32, - /// The relative rate at which this item shrinks when it is contracting to fit into space, 1.0 is the default value, and this value must be positive. - pub flex_shrink: f32, - - /// The fill color of this element - pub background: Option, - - /// The border color of this element - pub border_color: Option, - - /// The border style of this element - pub border_style: BorderStyle, - - /// The radius of the corners of this element - #[refineable] - pub corner_radii: Corners, - - /// Box shadow of the element - pub box_shadow: Vec, - - /// The text style of this element - #[refineable] - pub text: TextStyleRefinement, - - /// The mouse cursor style shown when the mouse pointer is over an element. - pub mouse_cursor: Option, - - /// The opacity of this element - pub opacity: Option, - - /// The grid columns of this element - /// Roughly equivalent to the Tailwind `grid-cols-` - pub grid_cols: Option, - - /// The row span of this element - /// Equivalent to the Tailwind `grid-rows-` - pub grid_rows: Option, - - /// The grid location of this element - pub grid_location: Option, - - /// Whether to draw a red debugging outline around this element - #[cfg(debug_assertions)] - pub debug: bool, - - /// Whether to draw a red debugging outline around this element and all of its conforming children - #[cfg(debug_assertions)] - pub debug_below: bool, -} - -impl Styled for StyleRefinement { - fn style(&mut self) -> &mut StyleRefinement { - self - } -} - -impl StyleRefinement { - /// The grid location of this element - pub fn grid_location_mut(&mut self) -> &mut GridLocation { - self.grid_location.get_or_insert_default() - } -} - -/// The value of the visibility property, similar to the CSS property `visibility` -#[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)] -pub enum Visibility { - /// The element should be drawn as normal. - #[default] - Visible, - /// The element should not be drawn, but should still take up space in the layout. - Hidden, -} - -/// The possible values of the box-shadow property -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct BoxShadow { - /// What color should the shadow have? - pub color: Hsla, - /// How should it be offset from its element? - pub offset: Point, - /// How much should the shadow be blurred? - pub blur_radius: Pixels, - /// How much should the shadow spread? - pub spread_radius: Pixels, - /// Whether this is an inset shadow (drawn inside the element's bounds). - pub inset: bool, -} - -impl BoxShadow { - /// Creates a new [`BoxShadow`] with the given offset and color, matching the order - /// of the CSS `box-shadow` property. Use the builder methods to set blur radius, - /// spread radius, and inset. - pub fn new(offset_x: Pixels, offset_y: Pixels, color: Hsla) -> Self { - Self { - color, - offset: point(offset_x, offset_y), - blur_radius: px(0.), - spread_radius: px(0.), - inset: false, - } - } - - /// Sets the shadow blur radius. - pub fn blur_radius(mut self, blur_radius: Pixels) -> Self { - self.blur_radius = blur_radius; - self - } - - /// Sets the shadow spread radius. - pub fn spread_radius(mut self, spread_radius: Pixels) -> Self { - self.spread_radius = spread_radius; - self - } - - /// Marks the shadow as inset (drawn inside the element's bounds). - pub fn inset(mut self) -> Self { - self.inset = true; - self - } -} - -/// How to handle whitespace in text -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub enum WhiteSpace { - /// Normal line wrapping when text overflows the width of the element - #[default] - Normal, - /// No line wrapping, text will overflow the width of the element - Nowrap, -} - -/// How to truncate text that overflows the width of the element -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub enum TextOverflow { - /// Truncate the text at the end when it doesn't fit, and represent this truncation by - /// displaying the provided string (e.g., "very long te…"). - Truncate(SharedString), - /// Truncate the text at the start when it doesn't fit, and represent this truncation by - /// displaying the provided string at the beginning (e.g., "…ong text here"). - /// Typically more adequate for file paths where the end is more important than the beginning. - TruncateStart(SharedString), - /// Truncate the text in the middle when it doesn't fit, preserving both the start and end - /// of the string (e.g., "long fi…name.rs"). Useful for filenames where both the prefix - /// and the extension are important context. - TruncateMiddle(SharedString), -} - -/// How to align text within the element -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub enum TextAlign { - /// Align the text to the left of the element - #[default] - Left, - - /// Center the text within the element - Center, - - /// Align the text to the right of the element - Right, -} - -/// The properties that can be used to style text in GPUI -#[derive(Refineable, Clone, Debug, PartialEq)] -#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct TextStyle { - /// The color of the text - pub color: Hsla, - - /// The font family to use - pub font_family: SharedString, - - /// The font features to use - pub font_features: FontFeatures, - - /// The fallback fonts to use - pub font_fallbacks: Option, - - /// The font size to use, in pixels or rems. - pub font_size: AbsoluteLength, - - /// The line height to use, in pixels or fractions - pub line_height: DefiniteLength, - - /// The font weight, e.g. bold - pub font_weight: FontWeight, - - /// The font style, e.g. italic - pub font_style: FontStyle, - - /// The background color of the text - pub background_color: Option, - - /// The underline style of the text - pub underline: Option, - - /// The strikethrough style of the text - pub strikethrough: Option, - - /// How to handle whitespace in the text - pub white_space: WhiteSpace, - - /// The text should be truncated if it overflows the width of the element - pub text_overflow: Option, - - /// How the text should be aligned within the element - pub text_align: TextAlign, - - /// The number of lines to display before truncating the text - pub line_clamp: Option, -} - -impl Default for TextStyle { - fn default() -> Self { - TextStyle { - color: black(), - // todo(linux) make this configurable or choose better default - font_family: ".SystemUIFont".into(), - font_features: FontFeatures::default(), - font_fallbacks: None, - font_size: rems(1.).into(), - line_height: phi(), - font_weight: FontWeight::default(), - font_style: FontStyle::default(), - background_color: None, - underline: None, - strikethrough: None, - white_space: WhiteSpace::Normal, - text_overflow: None, - text_align: TextAlign::default(), - line_clamp: None, - } - } -} - -impl TextStyle { - /// Create a new text style with the given highlighting applied. - pub fn highlight(mut self, style: impl Into) -> Self { - let style = style.into(); - if let Some(weight) = style.font_weight { - self.font_weight = weight; - } - if let Some(style) = style.font_style { - self.font_style = style; - } - - if let Some(color) = style.color { - self.color = self.color.blend(color); - } - - if let Some(factor) = style.fade_out { - self.color.fade_out(factor); - } - - if let Some(background_color) = style.background_color { - self.background_color = Some(background_color); - } - - if let Some(underline) = style.underline { - self.underline = Some(underline); - } - - if let Some(strikethrough) = style.strikethrough { - self.strikethrough = Some(strikethrough); - } - - self - } - - /// Get the font configured for this text style. - pub fn font(&self) -> Font { - Font { - family: self.font_family.clone(), - features: self.font_features.clone(), - fallbacks: self.font_fallbacks.clone(), - weight: self.font_weight, - style: self.font_style, - } - } - - /// Returns the rounded line height in pixels. - pub fn line_height_in_pixels(&self, rem_size: Pixels) -> Pixels { - self.line_height.to_pixels(self.font_size, rem_size).round() - } - - /// Convert this text style into a [`TextRun`], for the given length of the text. - pub fn to_run(&self, len: usize) -> TextRun { - TextRun { - len, - font: Font { - family: self.font_family.clone(), - features: self.font_features.clone(), - fallbacks: self.font_fallbacks.clone(), - weight: self.font_weight, - style: self.font_style, - }, - color: self.color, - background_color: self.background_color, - underline: self.underline, - strikethrough: self.strikethrough, - } - } -} - -/// A highlight style to apply, similar to a `TextStyle` except -/// for a single font, uniformly sized and spaced text. -#[derive(Copy, Clone, Debug, Default, PartialEq)] -pub struct HighlightStyle { - /// The color of the text - pub color: Option, - - /// The font weight, e.g. bold - pub font_weight: Option, - - /// The font style, e.g. italic - pub font_style: Option, - - /// The background color of the text - pub background_color: Option, - - /// The underline style of the text - pub underline: Option, - - /// The underline style of the text - pub strikethrough: Option, - - /// Similar to the CSS `opacity` property, this will cause the text to be less vibrant. - pub fade_out: Option, -} - -impl Eq for HighlightStyle {} - -impl Hash for HighlightStyle { - fn hash(&self, state: &mut H) { - self.color.hash(state); - self.font_weight.hash(state); - self.font_style.hash(state); - self.background_color.hash(state); - self.underline.hash(state); - self.strikethrough.hash(state); - state.write_u32(u32::from_be_bytes( - self.fade_out.map(|f| f.to_be_bytes()).unwrap_or_default(), - )); - } -} - -impl Style { - /// Returns true if the style is visible and the background is opaque. - pub fn has_opaque_background(&self) -> bool { - self.background - .as_ref() - .is_some_and(|fill| fill.color().is_some_and(|color| !color.is_transparent())) - } - - /// Get the text style in this element style. - pub fn text_style(&self) -> Option<&TextStyleRefinement> { - if self.text.is_some() { - Some(&self.text) - } else { - None - } - } - - /// Get the content mask for this element style, based on the given bounds. - /// If the element does not hide its overflow, this will return `None`. - pub fn overflow_mask( - &self, - bounds: Bounds, - rem_size: Pixels, - scale_factor: f32, - ) -> Option { - match self.overflow { - Point { - x: Overflow::Visible, - y: Overflow::Visible, - } => None, - _ => { - let (bounds, radii, border_widths) = - self.paint_geometry(bounds, rem_size, scale_factor); - let has_visible_border = self - .border_color - .is_some_and(|color| !color.is_transparent()); - let border = if has_visible_border { - border_widths - } else { - Edges::default() - }; - let mut min = bounds.origin; - let mut max = bounds.bottom_right(); - - min.x += border.left; - max.x -= border.right; - min.y += border.top; - max.y -= border.bottom; - - // A scroll container clips its rounded padding box even when - // only one scroll axis is enabled (for example, menu panels). - let scrolling = - self.overflow.x == Overflow::Scroll || self.overflow.y == Overflow::Scroll; - - let bounds = match ( - !scrolling && self.overflow.x == Overflow::Visible, - !scrolling && self.overflow.y == Overflow::Visible, - ) { - // x and y both visible - (true, true) => return None, - // x visible, y hidden - (true, false) => Bounds::from_corners( - point(min.x, bounds.origin.y), - point(max.x, bounds.bottom_right().y), - ), - // x hidden, y visible - (false, true) => Bounds::from_corners( - point(bounds.origin.x, min.y), - point(bounds.bottom_right().x, max.y), - ), - // both hidden - (false, false) => Bounds::from_corners(min, max), - }; - - let (radii_x, radii_y) = if scrolling - || (self.overflow.x != Overflow::Visible - && self.overflow.y != Overflow::Visible) - { - ( - Corners { - top_left: (radii.top_left - border.left).max(px(0.)), - top_right: (radii.top_right - border.right).max(px(0.)), - bottom_right: (radii.bottom_right - border.right).max(px(0.)), - bottom_left: (radii.bottom_left - border.left).max(px(0.)), - }, - Corners { - top_left: (radii.top_left - border.top).max(px(0.)), - top_right: (radii.top_right - border.top).max(px(0.)), - bottom_right: (radii.bottom_right - border.bottom).max(px(0.)), - bottom_left: (radii.bottom_left - border.bottom).max(px(0.)), - }, - ) - } else { - (Corners::default(), Corners::default()) - }; - Some(crate::ClipRegion::rounded(bounds, radii_x, radii_y)) - } - } - } - - // Background, border, and overflow must derive from the same device-pixel - // geometry. In particular, a fractional border can paint a whole pixel. - fn paint_geometry( - &self, - bounds: Bounds, - rem_size: Pixels, - scale_factor: f32, - ) -> (Bounds, Corners, Edges) { - let radii = self - .corner_radii - .to_pixels(rem_size) - .clamp_radii_for_quad_size(bounds.size); - let snap = |value: Pixels| { - px(crate::util::round_to_device_pixel(value.0, scale_factor) / scale_factor) - }; - let origin = bounds.origin.map(snap); - let far = bounds.bottom_right().map(snap); - let bounds = Bounds::from_corners(origin, point(far.x.max(origin.x), far.y.max(origin.y))); - let radii = radii.clamp_radii_for_quad_size(bounds.size); - let borders = self.border_widths.to_pixels(rem_size).map(|value| { - px(crate::util::round_stroke_to_device_pixel(value.0, scale_factor) / scale_factor) - }); - (bounds, radii, borders) - } - - /// Paints the background of an element styled with this style. - pub fn paint( - &self, - bounds: Bounds, - window: &mut Window, - cx: &mut App, - continuation: impl FnOnce(&mut Window, &mut App), - ) { - #[cfg(debug_assertions)] - if self.debug_below { - cx.set_global(DebugBelow) - } - - #[cfg(debug_assertions)] - if self.debug || cx.has_global::() { - window.paint_quad(crate::outline(bounds, crate::red(), BorderStyle::default())); - } - - let rem_size = window.rem_size(); - let (bounds, corner_radii, border_widths) = - self.paint_geometry(bounds, rem_size, window.scale_factor()); - - window.paint_drop_shadows(bounds, corner_radii, &self.box_shadow); - - let background_color = self.background.as_ref().and_then(Fill::color); - if background_color.is_some_and(|color| !color.is_transparent()) { - let mut border_color = match background_color { - Some(color) => match color.tag { - BackgroundTag::Solid - | BackgroundTag::PatternSlash - | BackgroundTag::Checkerboard => color.solid, - - BackgroundTag::LinearGradient => color - .colors - .first() - .map(|stop| stop.color) - .unwrap_or_default(), - }, - None => Hsla::default(), - }; - border_color.a = 0.; - window.paint_quad(quad( - bounds, - corner_radii, - background_color.unwrap_or_default(), - Edges::default(), - border_color, - self.border_style, - )); - } - - window.paint_inset_shadows(bounds, corner_radii, &self.box_shadow); - - continuation(window, cx); - - if self.is_border_visible() { - let mut background = self.border_color.unwrap_or_default(); - background.a = 0.; - window.paint_quad(quad( - bounds, - corner_radii, - background, - border_widths, - self.border_color.unwrap_or_default(), - self.border_style, - )); - } - - #[cfg(debug_assertions)] - if self.debug_below { - cx.remove_global::(); - } - } - - fn is_border_visible(&self) -> bool { - self.border_color - .is_some_and(|color| !color.is_transparent()) - && self.border_widths.any(|length| !length.is_zero()) - } -} - -impl Default for Style { - fn default() -> Self { - Style { - display: Display::Block, - visibility: Visibility::Visible, - overflow: Point { - x: Overflow::Visible, - y: Overflow::Visible, - }, - allow_concurrent_scroll: false, - restrict_scroll_to_axis: false, - scrollbar_width: AbsoluteLength::default(), - position: Position::Relative, - inset: Edges::auto(), - margin: Edges::::zero(), - padding: Edges::::zero(), - border_widths: Edges::::zero(), - size: Size::auto(), - min_size: Size::auto(), - max_size: Size::auto(), - aspect_ratio: None, - gap: Size::default(), - // Alignment - align_items: None, - align_self: None, - align_content: None, - justify_content: None, - // Flexbox - flex_direction: FlexDirection::Row, - flex_wrap: FlexWrap::NoWrap, - flex_grow: 0.0, - flex_shrink: 1.0, - flex_basis: Length::Auto, - background: None, - border_color: None, - border_style: BorderStyle::default(), - corner_radii: Corners::default(), - box_shadow: Default::default(), - text: TextStyleRefinement::default(), - mouse_cursor: None, - opacity: None, - grid_rows: None, - grid_cols: None, - grid_location: None, - - #[cfg(debug_assertions)] - debug: false, - #[cfg(debug_assertions)] - debug_below: false, - } - } -} - -/// The properties that can be applied to an underline. -#[derive( - Refineable, Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, -)] -pub struct UnderlineStyle { - /// The thickness of the underline. - pub thickness: Pixels, - - /// The color of the underline. - pub color: Option, - - /// Whether the underline should be wavy, like in a spell checker. - pub wavy: bool, -} - -/// The properties that can be applied to a strikethrough. -#[derive( - Refineable, Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, -)] -pub struct StrikethroughStyle { - /// The thickness of the strikethrough. - pub thickness: Pixels, - - /// The color of the strikethrough. - pub color: Option, -} - -/// The kinds of fill that can be applied to a shape. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -pub enum Fill { - /// A solid color fill. - Color(Background), -} - -impl Fill { - /// Unwrap this fill into a solid color, if it is one. - /// - /// If the fill is not a solid color, this method returns `None`. - pub fn color(&self) -> Option { - match self { - Fill::Color(color) => Some(*color), - } - } -} - -impl Default for Fill { - fn default() -> Self { - Self::Color(Background::default()) - } -} - -impl From for Fill { - fn from(color: Hsla) -> Self { - Self::Color(color.into()) - } -} - -impl From for Fill { - fn from(color: Rgba) -> Self { - Self::Color(color.into()) - } -} - -impl From for Fill { - fn from(background: Background) -> Self { - Self::Color(background) - } -} - -impl From for HighlightStyle { - fn from(other: TextStyle) -> Self { - Self::from(&other) - } -} - -impl From<&TextStyle> for HighlightStyle { - fn from(other: &TextStyle) -> Self { - Self { - color: Some(other.color), - font_weight: Some(other.font_weight), - font_style: Some(other.font_style), - background_color: other.background_color, - underline: other.underline, - strikethrough: other.strikethrough, - fade_out: None, - } - } -} - -impl HighlightStyle { - /// Create a highlight style with just a color - pub fn color(color: Hsla) -> Self { - Self { - color: Some(color), - ..Default::default() - } - } - /// Blend this highlight style with another. - /// Non-continuous properties, like font_weight and font_style, are overwritten. - #[must_use] - pub fn highlight(self, other: HighlightStyle) -> Self { - Self { - color: other - .color - .map(|other_color| { - if let Some(color) = self.color { - color.blend(other_color) - } else { - other_color - } - }) - .or(self.color), - font_weight: other.font_weight.or(self.font_weight), - font_style: other.font_style.or(self.font_style), - background_color: other.background_color.or(self.background_color), - underline: other.underline.or(self.underline), - strikethrough: other.strikethrough.or(self.strikethrough), - fade_out: other - .fade_out - .map(|source_fade| { - self.fade_out - .map(|dest_fade| (dest_fade * (1. + source_fade)).clamp(0., 1.)) - .unwrap_or(source_fade) - }) - .or(self.fade_out), - } - } -} - -impl From for HighlightStyle { - fn from(color: Hsla) -> Self { - Self { - color: Some(color), - ..Default::default() - } - } -} - -impl From for HighlightStyle { - fn from(font_weight: FontWeight) -> Self { - Self { - font_weight: Some(font_weight), - ..Default::default() - } - } -} - -impl From for HighlightStyle { - fn from(font_style: FontStyle) -> Self { - Self { - font_style: Some(font_style), - ..Default::default() - } - } -} - -impl From for HighlightStyle { - fn from(color: Rgba) -> Self { - Self { - color: Some(color.into()), - ..Default::default() - } - } -} - -/// Combine and merge the highlights and ranges in the two iterators. -pub fn combine_highlights( - a: impl IntoIterator, HighlightStyle)>, - b: impl IntoIterator, HighlightStyle)>, -) -> impl Iterator, HighlightStyle)> { - let mut endpoints = Vec::new(); - let mut highlights = Vec::new(); - for (range, highlight) in a.into_iter().chain(b) { - if !range.is_empty() { - let highlight_id = highlights.len(); - endpoints.push((range.start, highlight_id, true)); - endpoints.push((range.end, highlight_id, false)); - highlights.push(highlight); - } - } - endpoints.sort_unstable_by_key(|(position, _, _)| *position); - let mut endpoints = endpoints.into_iter().peekable(); - - let mut active_styles = HashSet::default(); - let mut ix = 0; - iter::from_fn(move || { - while let Some((endpoint_ix, highlight_id, is_start)) = endpoints.peek() { - let prev_index = mem::replace(&mut ix, *endpoint_ix); - if ix > prev_index && !active_styles.is_empty() { - let current_style = active_styles - .iter() - .fold(HighlightStyle::default(), |acc, highlight_id| { - acc.highlight(highlights[*highlight_id]) - }); - return Some((prev_index..ix, current_style)); - } - - if *is_start { - active_styles.insert(*highlight_id); - } else { - active_styles.remove(highlight_id); - } - endpoints.next(); - } - None - }) -} - -/// Used to control how child nodes are aligned. -/// For Flexbox it controls alignment in the cross axis -/// For Grid it controls alignment in the block axis -/// -/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-items) -#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)] -// Copy of taffy::style type of the same name, to derive JsonSchema. -pub enum AlignItems { - /// Items are packed toward the start of the axis - Start, - /// Items are packed toward the end of the axis - End, - /// Items are packed towards the flex-relative start of the axis. - /// - /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent - /// to End. In all other cases it is equivalent to Start. - FlexStart, - /// Items are packed towards the flex-relative end of the axis. - /// - /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent - /// to Start. In all other cases it is equivalent to End. - FlexEnd, - /// Items are packed along the center of the cross axis - Center, - /// Items are aligned such as their baselines align - Baseline, - /// Stretch to fill the container - Stretch, -} -/// Used to control how child nodes are aligned. -/// Does not apply to Flexbox, and will be ignored if specified on a flex container -/// For Grid it controls alignment in the inline axis -/// -/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-items) -pub type JustifyItems = AlignItems; -/// Used to control how the specified nodes is aligned. -/// Overrides the parent Node's `AlignItems` property. -/// For Flexbox it controls alignment in the cross axis -/// For Grid it controls alignment in the block axis -/// -/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-self) -pub type AlignSelf = AlignItems; -/// Used to control how the specified nodes is aligned. -/// Overrides the parent Node's `JustifyItems` property. -/// Does not apply to Flexbox, and will be ignored if specified on a flex child -/// For Grid it controls alignment in the inline axis -/// -/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-self) -pub type JustifySelf = AlignItems; - -/// Sets the distribution of space between and around content items -/// For Flexbox it controls alignment in the cross axis -/// For Grid it controls alignment in the block axis -/// -/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-content) -#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)] -// Copy of taffy::style type of the same name, to derive JsonSchema. -pub enum AlignContent { - /// Items are packed toward the start of the axis - Start, - /// Items are packed toward the end of the axis - End, - /// Items are packed towards the flex-relative start of the axis. - /// - /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent - /// to End. In all other cases it is equivalent to Start. - FlexStart, - /// Items are packed towards the flex-relative end of the axis. - /// - /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent - /// to Start. In all other cases it is equivalent to End. - FlexEnd, - /// Items are centered around the middle of the axis - Center, - /// Items are stretched to fill the container - Stretch, - /// The first and last items are aligned flush with the edges of the container (no gap) - /// The gap between items is distributed evenly. - SpaceBetween, - /// The gap between the first and last items is exactly THE SAME as the gap between items. - /// The gaps are distributed evenly - SpaceEvenly, - /// The gap between the first and last items is exactly HALF the gap between items. - /// The gaps are distributed evenly in proportion to these ratios. - SpaceAround, -} - -/// Sets the distribution of space between and around content items -/// For Flexbox it controls alignment in the main axis -/// For Grid it controls alignment in the inline axis -/// -/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content) -pub type JustifyContent = AlignContent; - -/// Sets the layout used for the children of this node -/// -/// The default values depends on on which feature flags are enabled. The order of precedence is: Flex, Grid, Block, None. -#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)] -// Copy of taffy::style type of the same name, to derive JsonSchema. -pub enum Display { - /// The children will follow the block layout algorithm - Block, - /// The children will follow the flexbox layout algorithm - #[default] - Flex, - /// The children will follow the CSS Grid layout algorithm - Grid, - /// The children will not be laid out, and will follow absolute positioning - None, -} - -/// Controls whether flex items are forced onto one line or can wrap onto multiple lines. -/// -/// Defaults to [`FlexWrap::NoWrap`] -/// -/// [Specification](https://www.w3.org/TR/css-flexbox-1/#flex-wrap-property) -#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)] -// Copy of taffy::style type of the same name, to derive JsonSchema. -pub enum FlexWrap { - /// Items will not wrap and stay on a single line - #[default] - NoWrap, - /// Items will wrap according to this item's [`FlexDirection`] - Wrap, - /// Items will wrap in the opposite direction to this item's [`FlexDirection`] - WrapReverse, -} - -/// The direction of the flexbox layout main axis. -/// -/// There are always two perpendicular layout axes: main (or primary) and cross (or secondary). -/// Adding items will cause them to be positioned adjacent to each other along the main axis. -/// By varying this value throughout your tree, you can create complex axis-aligned layouts. -/// -/// Items are always aligned relative to the cross axis, and justified relative to the main axis. -/// -/// The default behavior is [`FlexDirection::Row`]. -/// -/// [Specification](https://www.w3.org/TR/css-flexbox-1/#flex-direction-property) -#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)] -// Copy of taffy::style type of the same name, to derive JsonSchema. -pub enum FlexDirection { - /// Defines +x as the main axis - /// - /// Items will be added from left to right in a row. - #[default] - Row, - /// Defines +y as the main axis - /// - /// Items will be added from top to bottom in a column. - Column, - /// Defines -x as the main axis - /// - /// Items will be added from right to left in a row. - RowReverse, - /// Defines -y as the main axis - /// - /// Items will be added from bottom to top in a column. - ColumnReverse, -} - -/// How children overflowing their container should affect layout -/// -/// In CSS the primary effect of this property is to control whether contents of a parent container that overflow that container should -/// be displayed anyway, be clipped, or trigger the container to become a scroll container. However it also has secondary effects on layout, -/// the main ones being: -/// -/// - The automatic minimum size Flexbox/CSS Grid items with non-`Visible` overflow is `0` rather than being content based -/// - `Overflow::Scroll` nodes have space in the layout reserved for a scrollbar (width controlled by the `scrollbar_width` property) -/// -/// In Taffy, we only implement the layout related secondary effects as we are not concerned with drawing/painting. The amount of space reserved for -/// a scrollbar is controlled by the `scrollbar_width` property. If this is `0` then `Scroll` behaves identically to `Hidden`. -/// -/// -#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)] -// Copy of taffy::style type of the same name, to derive JsonSchema. -pub enum Overflow { - /// The automatic minimum size of this node as a flexbox/grid item should be based on the size of its content. - /// Content that overflows this node *should* contribute to the scroll region of its parent. - #[default] - Visible, - /// The automatic minimum size of this node as a flexbox/grid item should be based on the size of its content. - /// Content that overflows this node should *not* contribute to the scroll region of its parent. - Clip, - /// The automatic minimum size of this node as a flexbox/grid item should be `0`. - /// Content that overflows this node should *not* contribute to the scroll region of its parent. - Hidden, - /// The automatic minimum size of this node as a flexbox/grid item should be `0`. Additionally, space should be reserved - /// for a scrollbar. The amount of space reserved is controlled by the `scrollbar_width` property. - /// Content that overflows this node should *not* contribute to the scroll region of its parent. - Scroll, -} - -/// The positioning strategy for this item. -/// -/// This controls both how the origin is determined for the [`Style::position`] field, -/// and whether or not the item will be controlled by flexbox's layout algorithm. -/// -/// WARNING: this enum follows the behavior of [CSS's `position` property](https://developer.mozilla.org/en-US/docs/Web/CSS/position), -/// which can be unintuitive. -/// -/// [`Position::Relative`] is the default value, in contrast to the default behavior in CSS. -#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)] -// Copy of taffy::style type of the same name, to derive JsonSchema. -pub enum Position { - /// The offset is computed relative to the final position given by the layout algorithm. - /// Offsets do not affect the position of any other items; they are effectively a correction factor applied at the end. - #[default] - Relative, - /// The offset is computed relative to this item's closest positioned ancestor, if any. - /// Otherwise, it is placed relative to the origin. - /// No space is created for the item in the page layout, and its size will not be altered. - /// - /// WARNING: to opt-out of layouting entirely, you must use [`Display::None`] instead on your [`Style`] object. - Absolute, -} - -impl From for taffy::style::AlignItems { - fn from(value: AlignItems) -> Self { - match value { - AlignItems::Start => Self::START, - AlignItems::End => Self::END, - AlignItems::FlexStart => Self::FLEX_START, - AlignItems::FlexEnd => Self::FLEX_END, - AlignItems::Center => Self::CENTER, - AlignItems::Baseline => Self::BASELINE, - AlignItems::Stretch => Self::STRETCH, - } - } -} - -impl From for taffy::style::AlignContent { - fn from(value: AlignContent) -> Self { - match value { - AlignContent::Start => Self::START, - AlignContent::End => Self::END, - AlignContent::FlexStart => Self::FLEX_START, - AlignContent::FlexEnd => Self::FLEX_END, - AlignContent::Center => Self::CENTER, - AlignContent::Stretch => Self::STRETCH, - AlignContent::SpaceBetween => Self::SPACE_BETWEEN, - AlignContent::SpaceEvenly => Self::SPACE_EVENLY, - AlignContent::SpaceAround => Self::SPACE_AROUND, - } - } -} - -impl From for taffy::style::Display { - fn from(value: Display) -> Self { - match value { - Display::Block => Self::Block, - Display::Flex => Self::Flex, - Display::Grid => Self::Grid, - Display::None => Self::None, - } - } -} - -impl From for taffy::style::FlexWrap { - fn from(value: FlexWrap) -> Self { - match value { - FlexWrap::NoWrap => Self::NoWrap, - FlexWrap::Wrap => Self::Wrap, - FlexWrap::WrapReverse => Self::WrapReverse, - } - } -} - -impl From for taffy::style::FlexDirection { - fn from(value: FlexDirection) -> Self { - match value { - FlexDirection::Row => Self::Row, - FlexDirection::Column => Self::Column, - FlexDirection::RowReverse => Self::RowReverse, - FlexDirection::ColumnReverse => Self::ColumnReverse, - } - } -} - -impl From for taffy::style::Overflow { - fn from(value: Overflow) -> Self { - match value { - Overflow::Visible => Self::Visible, - Overflow::Clip => Self::Clip, - Overflow::Hidden => Self::Hidden, - Overflow::Scroll => Self::Scroll, - } - } -} - -impl From for taffy::style::Position { - fn from(value: Position) -> Self { - match value { - Position::Relative => Self::Relative, - Position::Absolute => Self::Absolute, - } - } -} - -#[cfg(test)] -mod tests { - use crate::{blue, green, px, red, yellow}; - - use super::*; - - use util_macros::perf; - - #[perf] - fn test_basic_highlight_style_combination() { - let style_a = HighlightStyle::default(); - let style_b = HighlightStyle::default(); - let style_a = style_a.highlight(style_b); - assert_eq!( - style_a, - HighlightStyle::default(), - "Combining empty styles should not produce a non-empty style." - ); - - let mut style_b = HighlightStyle { - color: Some(red()), - strikethrough: Some(StrikethroughStyle { - thickness: px(2.), - color: Some(blue()), - }), - fade_out: Some(0.), - font_style: Some(FontStyle::Italic), - font_weight: Some(FontWeight(300.)), - background_color: Some(yellow()), - underline: Some(UnderlineStyle { - thickness: px(2.), - color: Some(red()), - wavy: true, - }), - }; - let expected_style = style_b; - - let style_a = style_a.highlight(style_b); - assert_eq!( - style_a, expected_style, - "Blending an empty style with another style should return the other style" - ); - - let style_b = style_b.highlight(Default::default()); - assert_eq!( - style_b, expected_style, - "Blending a style with an empty style should not change the style." - ); - - let mut style_c = expected_style; - - let style_d = HighlightStyle { - color: Some(blue().alpha(0.7)), - strikethrough: Some(StrikethroughStyle { - thickness: px(4.), - color: Some(crate::red()), - }), - fade_out: Some(0.), - font_style: Some(FontStyle::Oblique), - font_weight: Some(FontWeight(800.)), - background_color: Some(green()), - underline: Some(UnderlineStyle { - thickness: px(4.), - color: None, - wavy: false, - }), - }; - - let expected_style = HighlightStyle { - color: Some(red().blend(blue().alpha(0.7))), - strikethrough: Some(StrikethroughStyle { - thickness: px(4.), - color: Some(red()), - }), - // TODO this does not seem right - fade_out: Some(0.), - font_style: Some(FontStyle::Oblique), - font_weight: Some(FontWeight(800.)), - background_color: Some(green()), - underline: Some(UnderlineStyle { - thickness: px(4.), - color: None, - wavy: false, - }), - }; - - let style_c = style_c.highlight(style_d); - assert_eq!( - style_c, expected_style, - "Blending styles should blend properties where possible and override all others" - ); - } - - #[perf] - fn test_combine_highlights() { - assert_eq!( - combine_highlights( - [ - (0..5, green().into()), - (4..10, FontWeight::BOLD.into()), - (15..20, yellow().into()), - ], - [ - (2..6, FontStyle::Italic.into()), - (1..3, blue().into()), - (21..23, red().into()), - ] - ) - .collect::>(), - [ - ( - 0..1, - HighlightStyle { - color: Some(green()), - ..Default::default() - } - ), - ( - 1..2, - HighlightStyle { - color: Some(blue()), - ..Default::default() - } - ), - ( - 2..3, - HighlightStyle { - color: Some(blue()), - font_style: Some(FontStyle::Italic), - ..Default::default() - } - ), - ( - 3..4, - HighlightStyle { - color: Some(green()), - font_style: Some(FontStyle::Italic), - ..Default::default() - } - ), - ( - 4..5, - HighlightStyle { - color: Some(green()), - font_weight: Some(FontWeight::BOLD), - font_style: Some(FontStyle::Italic), - ..Default::default() - } - ), - ( - 5..6, - HighlightStyle { - font_weight: Some(FontWeight::BOLD), - font_style: Some(FontStyle::Italic), - ..Default::default() - } - ), - ( - 6..10, - HighlightStyle { - font_weight: Some(FontWeight::BOLD), - ..Default::default() - } - ), - ( - 15..20, - HighlightStyle { - color: Some(yellow()), - ..Default::default() - } - ), - ( - 21..23, - HighlightStyle { - color: Some(red()), - ..Default::default() - } - ) - ] - ); - } - - #[perf] - fn test_text_style_refinement() { - let mut style = Style::default(); - style.refine(&StyleRefinement::default().text_size(px(20.0))); - style.refine(&StyleRefinement::default().font_weight(FontWeight::SEMIBOLD)); - - assert_eq!( - Some(AbsoluteLength::from(px(20.0))), - style.text_style().unwrap().font_size - ); - - assert_eq!( - Some(FontWeight::SEMIBOLD), - style.text_style().unwrap().font_weight - ); - } -} diff --git a/crates/gpui_pre/src/styled.rs b/crates/gpui_pre/src/styled.rs deleted file mode 100644 index a24982a..0000000 --- a/crates/gpui_pre/src/styled.rs +++ /dev/null @@ -1,904 +0,0 @@ -use crate::{ - self as gpui, AbsoluteLength, AlignContent, AlignItems, AlignSelf, BorderStyle, CursorStyle, - DefiniteLength, Display, Fill, FlexDirection, FlexWrap, Font, FontFeatures, FontStyle, - FontWeight, GridPlacement, GridTemplate, GridTemplateMinSize, Hsla, JustifyContent, Length, - SharedString, StrikethroughStyle, StyleRefinement, TextAlign, TextOverflow, - TextStyleRefinement, UnderlineStyle, WhiteSpace, px, relative, rems, -}; -pub use gpui_macros::{ - border_style_methods, box_shadow_style_methods, cursor_style_methods, margin_style_methods, - overflow_style_methods, padding_style_methods, position_style_methods, - visibility_style_methods, -}; -const ELLIPSIS: SharedString = SharedString::new_static("…"); - -/// A trait for elements that can be styled. -/// Use this to opt-in to a utility CSS-like styling API. -// gate on rust-analyzer so rust-analyzer never needs to expand this macro, it takes up to 10 seconds to expand due to inefficiencies in rust-analyzers proc-macro srv -#[cfg_attr( - all(any(feature = "inspector", debug_assertions), not(rust_analyzer)), - gpui_macros::derive_inspector_reflection -)] -pub trait Styled: Sized { - /// Returns a reference to the style memory of this element. - fn style(&mut self) -> &mut StyleRefinement; - - gpui_macros::style_helpers!(); - gpui_macros::visibility_style_methods!(); - gpui_macros::margin_style_methods!(); - gpui_macros::padding_style_methods!(); - gpui_macros::position_style_methods!(); - gpui_macros::overflow_style_methods!(); - gpui_macros::cursor_style_methods!(); - gpui_macros::border_style_methods!(); - gpui_macros::box_shadow_style_methods!(); - - /// Sets the display type of the element to `block`. - /// [Docs](https://tailwindcss.com/docs/display) - fn block(mut self) -> Self { - self.style().display = Some(Display::Block); - self - } - - /// Sets the display type of the element to `flex`. - /// [Docs](https://tailwindcss.com/docs/display) - fn flex(mut self) -> Self { - self.style().display = Some(Display::Flex); - self - } - - /// Sets the display type of the element to `grid`. - /// [Docs](https://tailwindcss.com/docs/display) - fn grid(mut self) -> Self { - self.style().display = Some(Display::Grid); - self - } - - /// Sets the display type of the element to `none`. - /// [Docs](https://tailwindcss.com/docs/display) - fn hidden(mut self) -> Self { - self.style().display = Some(Display::None); - self - } - - /// Set the space to be reserved for rendering the scrollbar. - /// - /// This will only affect the layout of the element when overflow for this element is set to - /// `Overflow::Scroll`. - fn scrollbar_width(mut self, width: impl Into) -> Self { - self.style().scrollbar_width = Some(width.into()); - self - } - - /// Sets the whitespace of the element to `normal`. - /// [Docs](https://tailwindcss.com/docs/whitespace#normal) - fn whitespace_normal(mut self) -> Self { - self.text_style().white_space = Some(WhiteSpace::Normal); - self - } - - /// Sets the whitespace of the element to `nowrap`. - /// [Docs](https://tailwindcss.com/docs/whitespace#nowrap) - fn whitespace_nowrap(mut self) -> Self { - self.text_style().white_space = Some(WhiteSpace::Nowrap); - self - } - - /// Sets the truncate overflowing text with an ellipsis (…) at the end if needed. - /// [Docs](https://tailwindcss.com/docs/text-overflow#ellipsis) - fn text_ellipsis(mut self) -> Self { - self.text_style().text_overflow = Some(TextOverflow::Truncate(ELLIPSIS)); - self - } - - /// Sets the truncate overflowing text with an ellipsis (…) at the start if needed. - /// Typically more adequate for file paths where the end is more important than the beginning. - /// Note: This doesn't exist in Tailwind CSS. - fn text_ellipsis_start(mut self) -> Self { - self.text_style().text_overflow = Some(TextOverflow::TruncateStart(ELLIPSIS)); - self - } - - /// Sets the truncate overflowing text with an ellipsis (…) in the middle if needed. - /// Preserves the beginning and end of the text. Useful for filenames. - /// Note: This doesn't exist in Tailwind CSS. - fn text_ellipsis_middle(mut self) -> Self { - self.text_style().text_overflow = Some(TextOverflow::TruncateMiddle(ELLIPSIS)); - self - } - - /// Sets the text overflow behavior of the element. - fn text_overflow(mut self, overflow: TextOverflow) -> Self { - self.text_style().text_overflow = Some(overflow); - self - } - - /// Set the text alignment of the element. - fn text_align(mut self, align: TextAlign) -> Self { - self.text_style().text_align = Some(align); - self - } - - /// Sets the text alignment to left - fn text_left(mut self) -> Self { - self.text_align(TextAlign::Left) - } - - /// Sets the text alignment to center - fn text_center(mut self) -> Self { - self.text_align(TextAlign::Center) - } - - /// Sets the text alignment to right - fn text_right(mut self) -> Self { - self.text_align(TextAlign::Right) - } - - /// Sets the truncate to prevent text from wrapping and truncate overflowing text with an ellipsis (…) if needed. - /// [Docs](https://tailwindcss.com/docs/text-overflow#truncate) - fn truncate(mut self) -> Self { - self.overflow_hidden().whitespace_nowrap().text_ellipsis() - } - - /// Sets number of lines to show before truncating the text. - /// [Docs](https://tailwindcss.com/docs/line-clamp) - fn line_clamp(mut self, lines: usize) -> Self { - let mut text_style = self.text_style(); - text_style.line_clamp = Some(lines); - self.overflow_hidden() - } - - /// Sets the flex direction of the element to `column`. - /// [Docs](https://tailwindcss.com/docs/flex-direction#column) - fn flex_col(mut self) -> Self { - self.style().flex_direction = Some(FlexDirection::Column); - self - } - - /// Sets the flex direction of the element to `column-reverse`. - /// [Docs](https://tailwindcss.com/docs/flex-direction#column-reverse) - fn flex_col_reverse(mut self) -> Self { - self.style().flex_direction = Some(FlexDirection::ColumnReverse); - self - } - - /// Sets the flex direction of the element to `row`. - /// [Docs](https://tailwindcss.com/docs/flex-direction#row) - fn flex_row(mut self) -> Self { - self.style().flex_direction = Some(FlexDirection::Row); - self - } - - /// Sets the flex direction of the element to `row-reverse`. - /// [Docs](https://tailwindcss.com/docs/flex-direction#row-reverse) - fn flex_row_reverse(mut self) -> Self { - self.style().flex_direction = Some(FlexDirection::RowReverse); - self - } - - /// Sets the element to allow a flex item to grow and shrink as needed, ignoring its initial size. - /// [Docs](https://tailwindcss.com/docs/flex#flex-1) - fn flex_1(mut self) -> Self { - self.style().flex_grow = Some(1.); - self.style().flex_shrink = Some(1.); - self.style().flex_basis = Some(relative(0.).into()); - self - } - - /// Sets the element to allow a flex item to grow and shrink, taking into account its initial size. - /// [Docs](https://tailwindcss.com/docs/flex#auto) - fn flex_auto(mut self) -> Self { - self.style().flex_grow = Some(1.); - self.style().flex_shrink = Some(1.); - self.style().flex_basis = Some(Length::Auto); - self - } - - /// Sets the element to allow a flex item to shrink but not grow, taking into account its initial size. - /// [Docs](https://tailwindcss.com/docs/flex#initial) - fn flex_initial(mut self) -> Self { - self.style().flex_grow = Some(0.); - self.style().flex_shrink = Some(1.); - self.style().flex_basis = Some(Length::Auto); - self - } - - /// Sets the element to prevent a flex item from growing or shrinking. - /// [Docs](https://tailwindcss.com/docs/flex#none) - fn flex_none(mut self) -> Self { - self.style().flex_grow = Some(0.); - self.style().flex_shrink = Some(0.); - self.style().flex_basis = Some(Length::Auto); - self - } - - /// Sets the initial size of flex items for this element. - /// [Docs](https://tailwindcss.com/docs/flex-basis) - fn flex_basis(mut self, basis: impl Into) -> Self { - self.style().flex_basis = Some(basis.into()); - self - } - - /// Sets the flex item's grow factor. - /// [Docs](https://tailwindcss.com/docs/flex-grow) - fn flex_grow(mut self, grow: f32) -> Self { - self.style().flex_grow = Some(grow); - self - } - - /// Disables flex item growth (flex-grow: 0). - /// [Docs](https://tailwindcss.com/docs/flex-grow#dont-grow) - fn flex_grow_0(mut self) -> Self { - self.style().flex_grow = Some(0.); - self - } - - /// Enables flex item growth (flex-grow: 1). - /// [Docs](https://tailwindcss.com/docs/flex-grow#grow-1) - fn flex_grow_1(mut self) -> Self { - self.style().flex_grow = Some(1.); - self - } - - /// Sets the flex item's shrink factor. - /// [Docs](https://tailwindcss.com/docs/flex-shrink) - fn flex_shrink(mut self, shrink: f32) -> Self { - self.style().flex_shrink = Some(shrink); - self - } - - /// Disables flex item shrinking (flex-shrink: 0). - /// [Docs](https://tailwindcss.com/docs/flex-shrink#dont-shrink) - fn flex_shrink_0(mut self) -> Self { - self.style().flex_shrink = Some(0.); - self - } - - /// Enables flex item shrinking (flex-shrink: 1). - /// [Docs](https://tailwindcss.com/docs/flex-shrink#shrink-1) - fn flex_shrink_1(mut self) -> Self { - self.style().flex_shrink = Some(1.); - self - } - - /// Sets the element to allow flex items to wrap. - /// [Docs](https://tailwindcss.com/docs/flex-wrap#wrap-normally) - fn flex_wrap(mut self) -> Self { - self.style().flex_wrap = Some(FlexWrap::Wrap); - self - } - - /// Sets the element wrap flex items in the reverse direction. - /// [Docs](https://tailwindcss.com/docs/flex-wrap#wrap-reversed) - fn flex_wrap_reverse(mut self) -> Self { - self.style().flex_wrap = Some(FlexWrap::WrapReverse); - self - } - - /// Sets the element to prevent flex items from wrapping, causing inflexible items to overflow the container if necessary. - /// [Docs](https://tailwindcss.com/docs/flex-wrap#dont-wrap) - fn flex_nowrap(mut self) -> Self { - self.style().flex_wrap = Some(FlexWrap::NoWrap); - self - } - - /// Sets the element to align flex items to the start of the container's cross axis. - /// [Docs](https://tailwindcss.com/docs/align-items#start) - fn items_start(mut self) -> Self { - self.style().align_items = Some(AlignItems::FlexStart); - self - } - - /// Sets the element to align flex items to the end of the container's cross axis. - /// [Docs](https://tailwindcss.com/docs/align-items#end) - fn items_end(mut self) -> Self { - self.style().align_items = Some(AlignItems::FlexEnd); - self - } - - /// Sets the element to align flex items along the center of the container's cross axis. - /// [Docs](https://tailwindcss.com/docs/align-items#center) - fn items_center(mut self) -> Self { - self.style().align_items = Some(AlignItems::Center); - self - } - - /// Sets the element to align flex items along the baseline of the container's cross axis. - /// [Docs](https://tailwindcss.com/docs/align-items#baseline) - fn items_baseline(mut self) -> Self { - self.style().align_items = Some(AlignItems::Baseline); - self - } - - /// Sets the element to stretch flex items to fill the available space along the container's cross axis. - /// [Docs](https://tailwindcss.com/docs/align-items#stretch) - fn items_stretch(mut self) -> Self { - self.style().align_items = Some(AlignItems::Stretch); - self - } - - /// Sets how this specific element is aligned along the container's cross axis. - /// [Docs](https://tailwindcss.com/docs/align-self#start) - fn self_start(mut self) -> Self { - self.style().align_self = Some(AlignSelf::Start); - self - } - - /// Sets this element to align against the end of the container's cross axis. - /// [Docs](https://tailwindcss.com/docs/align-self#end) - fn self_end(mut self) -> Self { - self.style().align_self = Some(AlignSelf::End); - self - } - - /// Sets this element to align against the start of the container's cross axis. - /// [Docs](https://tailwindcss.com/docs/align-self#start) - fn self_flex_start(mut self) -> Self { - self.style().align_self = Some(AlignSelf::FlexStart); - self - } - - /// Sets this element to align against the end of the container's cross axis. - /// [Docs](https://tailwindcss.com/docs/align-self#end) - fn self_flex_end(mut self) -> Self { - self.style().align_self = Some(AlignSelf::FlexEnd); - self - } - - /// Sets this element to align along the center of the container's cross axis. - /// [Docs](https://tailwindcss.com/docs/align-self#center) - fn self_center(mut self) -> Self { - self.style().align_self = Some(AlignSelf::Center); - self - } - - /// Sets this element to align along the baseline of the container's cross axis. - /// [Docs](https://tailwindcss.com/docs/align-self#baseline) - fn self_baseline(mut self) -> Self { - self.style().align_self = Some(AlignSelf::Baseline); - self - } - - /// Sets this element to stretch to fill the available space along the container's cross axis. - /// [Docs](https://tailwindcss.com/docs/align-self#stretch) - fn self_stretch(mut self) -> Self { - self.style().align_self = Some(AlignSelf::Stretch); - self - } - - /// Sets the element to justify flex items against the start of the container's main axis. - /// [Docs](https://tailwindcss.com/docs/justify-content#start) - fn justify_start(mut self) -> Self { - self.style().justify_content = Some(JustifyContent::Start); - self - } - - /// Sets the element to justify flex items against the end of the container's main axis. - /// [Docs](https://tailwindcss.com/docs/justify-content#end) - fn justify_end(mut self) -> Self { - self.style().justify_content = Some(JustifyContent::End); - self - } - - /// Sets the element to justify flex items along the center of the container's main axis. - /// [Docs](https://tailwindcss.com/docs/justify-content#center) - fn justify_center(mut self) -> Self { - self.style().justify_content = Some(JustifyContent::Center); - self - } - - /// Sets the element to justify flex items along the container's main axis - /// such that there is an equal amount of space between each item. - /// [Docs](https://tailwindcss.com/docs/justify-content#space-between) - fn justify_between(mut self) -> Self { - self.style().justify_content = Some(JustifyContent::SpaceBetween); - self - } - - /// Sets the element to justify items along the container's main axis such - /// that there is an equal amount of space on each side of each item. - /// [Docs](https://tailwindcss.com/docs/justify-content#space-around) - fn justify_around(mut self) -> Self { - self.style().justify_content = Some(JustifyContent::SpaceAround); - self - } - - /// Sets the element to justify items along the container's main axis such - /// that there is an equal amount of space around each item, but also - /// accounting for the doubling of space you would normally see between - /// each item when using justify-around. - /// [Docs](https://tailwindcss.com/docs/justify-content#space-evenly) - fn justify_evenly(mut self) -> Self { - self.style().justify_content = Some(JustifyContent::SpaceEvenly); - self - } - - /// Sets the element to pack content items in their default position as if no align-content value was set. - /// [Docs](https://tailwindcss.com/docs/align-content#normal) - fn content_normal(mut self) -> Self { - self.style().align_content = None; - self - } - - /// Sets the element to pack content items in the center of the container's cross axis. - /// [Docs](https://tailwindcss.com/docs/align-content#center) - fn content_center(mut self) -> Self { - self.style().align_content = Some(AlignContent::Center); - self - } - - /// Sets the element to pack content items against the start of the container's cross axis. - /// [Docs](https://tailwindcss.com/docs/align-content#start) - fn content_start(mut self) -> Self { - self.style().align_content = Some(AlignContent::FlexStart); - self - } - - /// Sets the element to pack content items against the end of the container's cross axis. - /// [Docs](https://tailwindcss.com/docs/align-content#end) - fn content_end(mut self) -> Self { - self.style().align_content = Some(AlignContent::FlexEnd); - self - } - - /// Sets the element to pack content items along the container's cross axis - /// such that there is an equal amount of space between each item. - /// [Docs](https://tailwindcss.com/docs/align-content#space-between) - fn content_between(mut self) -> Self { - self.style().align_content = Some(AlignContent::SpaceBetween); - self - } - - /// Sets the element to pack content items along the container's cross axis - /// such that there is an equal amount of space on each side of each item. - /// [Docs](https://tailwindcss.com/docs/align-content#space-around) - fn content_around(mut self) -> Self { - self.style().align_content = Some(AlignContent::SpaceAround); - self - } - - /// Sets the element to pack content items along the container's cross axis - /// such that there is an equal amount of space between each item. - /// [Docs](https://tailwindcss.com/docs/align-content#space-evenly) - fn content_evenly(mut self) -> Self { - self.style().align_content = Some(AlignContent::SpaceEvenly); - self - } - - /// Sets the element to allow content items to fill the available space along the container's cross axis. - /// [Docs](https://tailwindcss.com/docs/align-content#stretch) - fn content_stretch(mut self) -> Self { - self.style().align_content = Some(AlignContent::Stretch); - self - } - - /// Sets the aspect ratio of the element. - /// [Docs](https://tailwindcss.com/docs/aspect-ratio) - fn aspect_ratio(mut self, ratio: f32) -> Self { - self.style().aspect_ratio = Some(ratio); - self - } - - /// Sets the aspect ratio of the element to 1/1 – equal width and height. - /// [Docs](https://tailwindcss.com/docs/aspect-ratio) - fn aspect_square(mut self) -> Self { - self.style().aspect_ratio = Some(1.0); - self - } - - /// Sets the background color of the element. - fn bg(mut self, fill: F) -> Self - where - F: Into, - Self: Sized, - { - self.style().background = Some(fill.into()); - self - } - - /// Sets the border style of the element. - fn border_dashed(mut self) -> Self { - self.style().border_style = Some(BorderStyle::Dashed); - self - } - - /// Returns a mutable reference to the text style that has been configured on this element. - fn text_style(&mut self) -> &mut TextStyleRefinement { - let style: &mut StyleRefinement = self.style(); - &mut style.text - } - - /// Sets the text color of this element. - /// - /// This value cascades to its child elements. - fn text_color(mut self, color: impl Into) -> Self { - self.text_style().color = Some(color.into()); - self - } - - /// Sets the font weight of this element - /// - /// This value cascades to its child elements. - fn font_weight(mut self, weight: FontWeight) -> Self { - self.text_style().font_weight = Some(weight); - self - } - - /// Sets the background color of this element. - /// - /// This value cascades to its child elements. - fn text_bg(mut self, bg: impl Into) -> Self { - self.text_style().background_color = Some(bg.into()); - self - } - - /// Sets the text size of this element. - /// - /// This value cascades to its child elements. - fn text_size(mut self, size: impl Into) -> Self { - self.text_style().font_size = Some(size.into()); - self - } - - /// Sets the text size to 'extra small'. - /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) - fn text_xs(mut self) -> Self { - self.text_style().font_size = Some(rems(0.75).into()); - self - } - - /// Sets the text size to 'small'. - /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) - fn text_sm(mut self) -> Self { - self.text_style().font_size = Some(rems(0.875).into()); - self - } - - /// Sets the text size to 'base'. - /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) - fn text_base(mut self) -> Self { - self.text_style().font_size = Some(rems(1.0).into()); - self - } - - /// Sets the text size to 'large'. - /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) - fn text_lg(mut self) -> Self { - self.text_style().font_size = Some(rems(1.125).into()); - self - } - - /// Sets the text size to 'extra large'. - /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) - fn text_xl(mut self) -> Self { - self.text_style().font_size = Some(rems(1.25).into()); - self - } - - /// Sets the text size to 'extra extra large'. - /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) - fn text_2xl(mut self) -> Self { - self.text_style().font_size = Some(rems(1.5).into()); - self - } - - /// Sets the text size to 'extra extra extra large'. - /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) - fn text_3xl(mut self) -> Self { - self.text_style().font_size = Some(rems(1.875).into()); - self - } - - /// Sets the font style of the element to italic. - /// [Docs](https://tailwindcss.com/docs/font-style#italicizing-text) - fn italic(mut self) -> Self { - self.text_style().font_style = Some(FontStyle::Italic); - self - } - - /// Sets the font style of the element to normal (not italic). - /// [Docs](https://tailwindcss.com/docs/font-style#displaying-text-normally) - fn not_italic(mut self) -> Self { - self.text_style().font_style = Some(FontStyle::Normal); - self - } - - /// Sets the text decoration to underline. - /// [Docs](https://tailwindcss.com/docs/text-decoration-line#underling-text) - fn underline(mut self) -> Self { - let style = self.text_style(); - style.underline = Some(UnderlineStyle { - thickness: px(1.), - ..Default::default() - }); - self - } - - /// Sets the decoration of the text to have a line through it. - /// [Docs](https://tailwindcss.com/docs/text-decoration-line#adding-a-line-through-text) - fn line_through(mut self) -> Self { - let style = self.text_style(); - style.strikethrough = Some(StrikethroughStyle { - thickness: px(1.), - ..Default::default() - }); - self - } - - /// Removes the text decoration on this element. - /// - /// This value cascades to its child elements. - fn text_decoration_none(mut self) -> Self { - self.text_style().underline = None; - self - } - - /// Sets the color for the underline on this element - fn text_decoration_color(mut self, color: impl Into) -> Self { - let style = self.text_style(); - let underline = style.underline.get_or_insert_with(Default::default); - underline.color = Some(color.into()); - self - } - - /// Sets the text decoration style to a solid line. - /// [Docs](https://tailwindcss.com/docs/text-decoration-style) - fn text_decoration_solid(mut self) -> Self { - let style = self.text_style(); - let underline = style.underline.get_or_insert_with(Default::default); - underline.wavy = false; - self - } - - /// Sets the text decoration style to a wavy line. - /// [Docs](https://tailwindcss.com/docs/text-decoration-style) - fn text_decoration_wavy(mut self) -> Self { - let style = self.text_style(); - let underline = style.underline.get_or_insert_with(Default::default); - underline.wavy = true; - self - } - - /// Sets the text decoration to be 0px thick. - /// [Docs](https://tailwindcss.com/docs/text-decoration-thickness) - fn text_decoration_0(mut self) -> Self { - let style = self.text_style(); - let underline = style.underline.get_or_insert_with(Default::default); - underline.thickness = px(0.); - self - } - - /// Sets the text decoration to be 1px thick. - /// [Docs](https://tailwindcss.com/docs/text-decoration-thickness) - fn text_decoration_1(mut self) -> Self { - let style = self.text_style(); - let underline = style.underline.get_or_insert_with(Default::default); - underline.thickness = px(1.); - self - } - - /// Sets the text decoration to be 2px thick. - /// [Docs](https://tailwindcss.com/docs/text-decoration-thickness) - fn text_decoration_2(mut self) -> Self { - let style = self.text_style(); - let underline = style.underline.get_or_insert_with(Default::default); - underline.thickness = px(2.); - self - } - - /// Sets the text decoration to be 4px thick. - /// [Docs](https://tailwindcss.com/docs/text-decoration-thickness) - fn text_decoration_4(mut self) -> Self { - let style = self.text_style(); - let underline = style.underline.get_or_insert_with(Default::default); - underline.thickness = px(4.); - self - } - - /// Sets the text decoration to be 8px thick. - /// [Docs](https://tailwindcss.com/docs/text-decoration-thickness) - fn text_decoration_8(mut self) -> Self { - let style = self.text_style(); - let underline = style.underline.get_or_insert_with(Default::default); - underline.thickness = px(8.); - self - } - - /// Sets the font family of this element and its children. - fn font_family(mut self, family_name: impl Into) -> Self { - self.text_style().font_family = Some(family_name.into()); - self - } - - /// Sets the font features of this element and its children. - fn font_features(mut self, features: FontFeatures) -> Self { - self.text_style().font_features = Some(features); - self - } - - /// Sets the font of this element and its children. - fn font(mut self, font: Font) -> Self { - let Font { - family, - features, - fallbacks, - weight, - style, - } = font; - - let text_style = self.text_style(); - text_style.font_family = Some(family); - text_style.font_features = Some(features); - text_style.font_weight = Some(weight); - text_style.font_style = Some(style); - text_style.font_fallbacks = fallbacks; - - self - } - - /// Sets the line height of this element and its children. - fn line_height(mut self, line_height: impl Into) -> Self { - self.text_style().line_height = Some(line_height.into()); - self - } - - /// Sets the opacity of this element and its children. - fn opacity(mut self, opacity: f32) -> Self { - self.style().opacity = Some(opacity); - self - } - - /// Sets the grid columns of this element. - fn grid_cols(mut self, cols: u16) -> Self { - self.style().grid_cols = Some(GridTemplate { - repeat: cols, - min_size: GridTemplateMinSize::Zero, - }); - self - } - - /// Sets the grid columns with min-content minimum sizing. - /// Unlike grid_cols, it won't shrink to width 0 in AvailableSpace::MinContent constraints. - fn grid_cols_min_content(mut self, cols: u16) -> Self { - self.style().grid_cols = Some(GridTemplate { - repeat: cols, - min_size: GridTemplateMinSize::MinContent, - }); - self - } - - /// Sets the grid columns with max-content maximum sizing for content-based column widths. - fn grid_cols_max_content(mut self, cols: u16) -> Self { - self.style().grid_cols = Some(GridTemplate { - repeat: cols, - min_size: GridTemplateMinSize::MaxContent, - }); - self - } - - /// Sets the grid rows of this element. - fn grid_rows(mut self, rows: u16) -> Self { - self.style().grid_rows = Some(GridTemplate { - repeat: rows, - min_size: GridTemplateMinSize::Zero, - }); - self - } - - /// Sets the grid rows with min-content minimum sizing. - /// Unlike grid_rows, it won't shrink to height 0 in AvailableSpace::MinContent constraints. - fn grid_rows_min_content(mut self, rows: u16) -> Self { - self.style().grid_rows = Some(GridTemplate { - repeat: rows, - min_size: GridTemplateMinSize::MinContent, - }); - self - } - - /// Sets the grid rows with max-content maximum sizing for content-based row heights. - fn grid_rows_max_content(mut self, rows: u16) -> Self { - self.style().grid_rows = Some(GridTemplate { - repeat: rows, - min_size: GridTemplateMinSize::MaxContent, - }); - self - } - - /// Sets the column start of this element. - fn col_start(mut self, start: i16) -> Self { - let grid_location = self.style().grid_location_mut(); - grid_location.column.start = GridPlacement::Line(start); - self - } - - /// Sets the column start of this element to auto. - fn col_start_auto(mut self) -> Self { - let grid_location = self.style().grid_location_mut(); - grid_location.column.start = GridPlacement::Auto; - self - } - - /// Sets the column end of this element. - fn col_end(mut self, end: i16) -> Self { - let grid_location = self.style().grid_location_mut(); - grid_location.column.end = GridPlacement::Line(end); - self - } - - /// Sets the column end of this element to auto. - fn col_end_auto(mut self) -> Self { - let grid_location = self.style().grid_location_mut(); - grid_location.column.end = GridPlacement::Auto; - self - } - - /// Sets the column span of this element. - fn col_span(mut self, span: u16) -> Self { - let grid_location = self.style().grid_location_mut(); - grid_location.column = GridPlacement::Span(span)..GridPlacement::Span(span); - self - } - - /// Sets the row span of this element. - fn col_span_full(mut self) -> Self { - let grid_location = self.style().grid_location_mut(); - grid_location.column = GridPlacement::Line(1)..GridPlacement::Line(-1); - self - } - - /// Sets the row start of this element. - fn row_start(mut self, start: i16) -> Self { - let grid_location = self.style().grid_location_mut(); - grid_location.row.start = GridPlacement::Line(start); - self - } - - /// Sets the row start of this element to "auto" - fn row_start_auto(mut self) -> Self { - let grid_location = self.style().grid_location_mut(); - grid_location.row.start = GridPlacement::Auto; - self - } - - /// Sets the row end of this element. - fn row_end(mut self, end: i16) -> Self { - let grid_location = self.style().grid_location_mut(); - grid_location.row.end = GridPlacement::Line(end); - self - } - - /// Sets the row end of this element to "auto" - fn row_end_auto(mut self) -> Self { - let grid_location = self.style().grid_location_mut(); - grid_location.row.end = GridPlacement::Auto; - self - } - - /// Sets the row span of this element. - fn row_span(mut self, span: u16) -> Self { - let grid_location = self.style().grid_location_mut(); - grid_location.row = GridPlacement::Span(span)..GridPlacement::Span(span); - self - } - - /// Sets the row span of this element. - fn row_span_full(mut self) -> Self { - let grid_location = self.style().grid_location_mut(); - grid_location.row = GridPlacement::Line(1)..GridPlacement::Line(-1); - self - } - - /// Draws a debug border around this element. - #[cfg(debug_assertions)] - fn debug(mut self) -> Self { - self.style().debug = Some(true); - self - } - - /// Draws a debug border on all conforming elements below this element. - #[cfg(debug_assertions)] - fn debug_below(mut self) -> Self { - self.style().debug_below = Some(true); - self - } -} diff --git a/crates/gpui_pre/src/subscription.rs b/crates/gpui_pre/src/subscription.rs deleted file mode 100644 index b0c55a3..0000000 --- a/crates/gpui_pre/src/subscription.rs +++ /dev/null @@ -1,351 +0,0 @@ -use collections::BTreeMap; -use gpui_util::post_inc; -use std::{ - cell::{Cell, RefCell}, - fmt::Debug, - rc::Rc, -}; - -pub(crate) struct SubscriberSet( - Rc>>, -); - -impl Clone for SubscriberSet { - fn clone(&self) -> Self { - SubscriberSet(self.0.clone()) - } -} - -struct SubscriberSetState { - subscribers: BTreeMap>>>, - next_subscriber_id: usize, -} - -struct Subscriber { - active: Rc>, - dropped: Rc>, - callback: Callback, -} - -impl SubscriberSet -where - EmitterKey: 'static + Ord + Clone + Debug, - Callback: 'static, -{ - pub fn new() -> Self { - Self(Rc::new(RefCell::new(SubscriberSetState { - subscribers: Default::default(), - next_subscriber_id: 0, - }))) - } - - /// Inserts a new [`Subscription`] for the given `emitter_key`. By default, subscriptions - /// are inert, meaning that they won't be listed when calling `[SubscriberSet::remove]` or `[SubscriberSet::retain]`. - /// This method returns a tuple of a [`Subscription`] and an `impl FnOnce`, and you can use the latter - /// to activate the [`Subscription`]. - pub fn insert( - &self, - emitter_key: EmitterKey, - callback: Callback, - ) -> (Subscription, impl FnOnce() + use) { - let active = Rc::new(Cell::new(false)); - let dropped = Rc::new(Cell::new(false)); - let mut lock = self.0.borrow_mut(); - let subscriber_id = post_inc(&mut lock.next_subscriber_id); - lock.subscribers - .entry(emitter_key.clone()) - .or_default() - .get_or_insert_with(Default::default) - .insert( - subscriber_id, - Subscriber { - active: active.clone(), - dropped: dropped.clone(), - callback, - }, - ); - let this = self.0.clone(); - - let subscription = Subscription { - unsubscribe: Some(Box::new(move || { - dropped.set(true); - - let mut lock = this.borrow_mut(); - let Some(subscribers) = lock.subscribers.get_mut(&emitter_key) else { - return; - }; - - if let Some(subscribers) = subscribers { - subscribers.remove(&subscriber_id); - if subscribers.is_empty() { - lock.subscribers.remove(&emitter_key); - } - } - })), - }; - (subscription, move || active.set(true)) - } - - pub fn remove( - &self, - emitter: &EmitterKey, - ) -> impl IntoIterator + use { - let subscribers = self.0.borrow_mut().subscribers.remove(emitter); - subscribers - .unwrap_or_default() - .map(|s| s.into_values()) - .into_iter() - .flatten() - .filter_map(|subscriber| { - if subscriber.active.get() { - Some(subscriber.callback) - } else { - None - } - }) - } - - /// Call the given callback for each subscriber to the given emitter. - /// If the callback returns false, the subscriber is removed. - pub fn retain(&self, emitter: &EmitterKey, mut f: F) - where - F: FnMut(&mut Callback) -> bool, - { - let Some(mut subscribers) = self - .0 - .borrow_mut() - .subscribers - .get_mut(emitter) - .and_then(|s| s.take()) - else { - return; - }; - - subscribers.retain(|_, subscriber| { - if !subscriber.active.get() { - return true; - } - if subscriber.dropped.get() { - return false; - } - let keep = f(&mut subscriber.callback); - keep && !subscriber.dropped.get() - }); - let mut lock = self.0.borrow_mut(); - - // Add any new subscribers that were added while invoking the callback. - if let Some(Some(new_subscribers)) = lock.subscribers.remove(emitter) { - subscribers.extend(new_subscribers); - } - - if !subscribers.is_empty() { - lock.subscribers.insert(emitter.clone(), Some(subscribers)); - } - } -} - -/// A handle to a subscription created by GPUI. When dropped, the subscription -/// is cancelled and the callback will no longer be invoked. -#[must_use] -pub struct Subscription { - unsubscribe: Option>, -} - -impl Subscription { - /// Creates a new subscription with a callback that gets invoked when - /// this subscription is dropped. - pub fn new(unsubscribe: impl 'static + FnOnce()) -> Self { - Self { - unsubscribe: Some(Box::new(unsubscribe)), - } - } - - /// Detaches the subscription from this handle. The callback will - /// continue to be invoked until the entities it has been - /// subscribed to are dropped - pub fn detach(mut self) { - self.unsubscribe.take(); - } - - /// Joins two subscriptions into a single subscription. Detach will - /// detach both interior subscriptions. - pub fn join(mut subscription_a: Self, mut subscription_b: Self) -> Self { - let a_unsubscribe = subscription_a.unsubscribe.take(); - let b_unsubscribe = subscription_b.unsubscribe.take(); - Self { - unsubscribe: Some(Box::new(move || { - if let Some(self_unsubscribe) = a_unsubscribe { - self_unsubscribe(); - } - if let Some(other_unsubscribe) = b_unsubscribe { - other_unsubscribe(); - } - })), - } - } -} - -impl Drop for Subscription { - fn drop(&mut self) { - if let Some(unsubscribe) = self.unsubscribe.take() { - unsubscribe(); - } - } -} - -impl std::fmt::Debug for Subscription { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Subscription").finish() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{Global, TestApp}; - - #[test] - fn test_unsubscribe_during_callback_with_insert() { - struct TestGlobal; - impl Global for TestGlobal {} - - let mut app = TestApp::new(); - app.set_global(TestGlobal); - - let observer_a_count = Rc::new(Cell::new(0usize)); - let observer_b_count = Rc::new(Cell::new(0usize)); - - let sub_a: Rc>> = Default::default(); - let sub_b: Rc>> = Default::default(); - - // Observer A fires first (lower subscriber_id). It drops itself and - // inserts a new observer for the same global. - *sub_a.borrow_mut() = Some(app.update({ - let count = observer_a_count.clone(); - let sub_a = sub_a.clone(); - move |cx| { - cx.observe_global::(move |cx| { - count.set(count.get() + 1); - sub_a.borrow_mut().take(); - cx.observe_global::(|_| {}).detach(); - }) - } - })); - - // Observer B fires second. It just drops itself. - *sub_b.borrow_mut() = Some(app.update({ - let count = observer_b_count.clone(); - let sub_b = sub_b.clone(); - move |cx| { - cx.observe_global::(move |_cx| { - count.set(count.get() + 1); - sub_b.borrow_mut().take(); - }) - } - })); - - // Both fire once. - app.update(|cx| cx.set_global(TestGlobal)); - assert_eq!(observer_a_count.get(), 1); - assert_eq!(observer_b_count.get(), 1); - - // Neither should fire again — both dropped their subscriptions. - app.update(|cx| cx.set_global(TestGlobal)); - assert_eq!(observer_a_count.get(), 1); - assert_eq!(observer_b_count.get(), 1, "orphaned subscriber fired again"); - } - - #[test] - fn test_callback_dropped_by_earlier_callback_does_not_fire() { - struct TestGlobal; - impl Global for TestGlobal {} - - let mut app = TestApp::new(); - app.set_global(TestGlobal); - - let observer_b_count = Rc::new(Cell::new(0usize)); - let sub_b: Rc>> = Default::default(); - - // Observer A fires first and drops B's subscription. - app.update({ - let sub_b = sub_b.clone(); - move |cx| { - cx.observe_global::(move |_cx| { - sub_b.borrow_mut().take(); - }) - .detach(); - } - }); - - // Observer B fires second — but A already dropped it. - *sub_b.borrow_mut() = Some(app.update({ - let count = observer_b_count.clone(); - move |cx| { - cx.observe_global::(move |_cx| { - count.set(count.get() + 1); - }) - } - })); - - app.update(|cx| cx.set_global(TestGlobal)); - assert_eq!( - observer_b_count.get(), - 0, - "B should not fire — A dropped its subscription" - ); - } - - #[test] - fn test_self_drop_during_callback() { - struct TestGlobal; - impl Global for TestGlobal {} - - let mut app = TestApp::new(); - app.set_global(TestGlobal); - - let count = Rc::new(Cell::new(0usize)); - let sub: Rc>> = Default::default(); - - *sub.borrow_mut() = Some(app.update({ - let count = count.clone(); - let sub = sub.clone(); - move |cx| { - cx.observe_global::(move |_cx| { - count.set(count.get() + 1); - sub.borrow_mut().take(); - }) - } - })); - - app.update(|cx| cx.set_global(TestGlobal)); - assert_eq!(count.get(), 1); - - app.update(|cx| cx.set_global(TestGlobal)); - assert_eq!(count.get(), 1, "should not fire after self-drop"); - } - - #[test] - fn test_subscription_drop() { - struct TestGlobal; - impl Global for TestGlobal {} - - let mut app = TestApp::new(); - app.set_global(TestGlobal); - - let count = Rc::new(Cell::new(0usize)); - - let subscription = app.update({ - let count = count.clone(); - move |cx| { - cx.observe_global::(move |_cx| { - count.set(count.get() + 1); - }) - } - }); - - drop(subscription); - - app.update(|cx| cx.set_global(TestGlobal)); - assert_eq!(count.get(), 0, "should not fire after drop"); - } -} diff --git a/crates/gpui_pre/src/svg_renderer.rs b/crates/gpui_pre/src/svg_renderer.rs deleted file mode 100644 index d06a4b0..0000000 --- a/crates/gpui_pre/src/svg_renderer.rs +++ /dev/null @@ -1,527 +0,0 @@ -use crate::{ - AssetSource, DevicePixels, IsZero, RenderImage, Result, SharedString, Size, - swap_rgba_pa_to_bgra, -}; -use image::Frame; -use resvg::tiny_skia::Pixmap; -use smallvec::SmallVec; -use std::{ - hash::Hash, - sync::{Arc, LazyLock, OnceLock}, -}; - -#[cfg(target_os = "macos")] -const EMOJI_FONT_FAMILIES: &[&str] = &["Apple Color Emoji", ".AppleColorEmojiUI"]; - -#[cfg(target_os = "windows")] -const EMOJI_FONT_FAMILIES: &[&str] = &["Segoe UI Emoji", "Segoe UI Symbol"]; - -#[cfg(any(target_os = "linux", target_os = "freebsd"))] -const EMOJI_FONT_FAMILIES: &[&str] = &[ - "Noto Color Emoji", - "Emoji One", - "Twitter Color Emoji", - "JoyPixels", -]; - -#[cfg(not(any( - target_os = "macos", - target_os = "windows", - target_os = "linux", - target_os = "freebsd", -)))] -const EMOJI_FONT_FAMILIES: &[&str] = &[]; - -fn is_emoji_presentation(c: char) -> bool { - static EMOJI_PRESENTATION_REGEX: LazyLock = - LazyLock::new(|| regex::Regex::new("\\p{Emoji_Presentation}").unwrap()); - let mut buf = [0u8; 4]; - EMOJI_PRESENTATION_REGEX.is_match(c.encode_utf8(&mut buf)) -} - -fn font_has_char(db: &usvg::fontdb::Database, id: usvg::fontdb::ID, ch: char) -> bool { - db.with_face_data(id, |font_data, face_index| { - ttf_parser::Face::parse(font_data, face_index) - .ok() - .and_then(|face| face.glyph_index(ch)) - .is_some() - }) - .unwrap_or(false) -} - -fn select_emoji_font( - ch: char, - fonts: &[usvg::fontdb::ID], - db: &usvg::fontdb::Database, - families: &[&str], -) -> Option { - for family_name in families { - let query = usvg::fontdb::Query { - families: &[usvg::fontdb::Family::Name(family_name)], - weight: usvg::fontdb::Weight(400), - stretch: usvg::fontdb::Stretch::Normal, - style: usvg::fontdb::Style::Normal, - }; - - let Some(id) = db.query(&query) else { - continue; - }; - - if fonts.contains(&id) || !font_has_char(db, id, ch) { - continue; - } - - return Some(id); - } - - None -} - -/// When rendering SVGs, we render them at twice the size to get a higher-quality result. -pub const SMOOTH_SVG_SCALE_FACTOR: f32 = 2.; - -#[derive(Clone, PartialEq, Hash, Eq)] -#[expect(missing_docs)] -pub struct RenderSvgParams { - pub path: SharedString, - pub size: Size, -} - -#[derive(Clone)] -/// A struct holding everything necessary to render SVGs. -pub struct SvgRenderer { - asset_source: Arc, - usvg_options: Arc>, -} - -/// A parsed SVG document that can be rasterized at any scale. -/// -/// Produced by [`SvgRenderer::parse_svg`] and rasterized by -/// [`SvgRenderer::render_parsed`]. Parsing resolves fonts and converts text -/// to paths, so callers that need to rasterize the same SVG at multiple -/// scales should retain this value to avoid re-paying the parse cost. -pub struct ParsedSvg(usvg::Tree); - -/// The size in which to rasterize the SVG. -#[derive(Clone, Copy)] -pub enum SvgSize { - /// A width in device pixels. The SVG retains its aspect ratio. - Size(Size), - /// An exact width and height in device pixels. - ExactSize(Size), - /// A logical scaling factor to apply to the size provided by the SVG. - ScaleFactor(f32), -} - -impl From for SvgSize { - fn from(scale_factor: f32) -> Self { - Self::ScaleFactor(scale_factor) - } -} - -impl SvgRenderer { - /// Creates a new SVG renderer with the provided asset source. - pub fn new(asset_source: Arc) -> Self { - static SYSTEM_FONT_DB: LazyLock> = LazyLock::new(|| { - let mut db = usvg::fontdb::Database::new(); - db.load_system_fonts(); - Arc::new(db) - }); - - // Build the enriched font DB lazily on first SVG render rather than - // eagerly at construction time. This avoids the expensive deep-clone - // of the system font database for code paths that never render SVGs - // (e.g. tests). - let enriched_fontdb: Arc>> = Arc::new(OnceLock::new()); - - let default_font_resolver = usvg::FontResolver::default_font_selector(); - let font_resolver = Box::new({ - let asset_source = asset_source.clone(); - move |font: &usvg::Font, db: &mut Arc| { - if db.is_empty() { - let fontdb = enriched_fontdb.get_or_init(|| { - let mut db = (**SYSTEM_FONT_DB).clone(); - load_bundled_fonts(&*asset_source, &mut db); - fix_generic_font_families(&mut db); - Arc::new(db) - }); - *db = fontdb.clone(); - } - if let Some(id) = default_font_resolver(font, db) { - return Some(id); - } - // fontdb doesn't recognize CSS system font keywords like "system-ui" - // or "ui-sans-serif", so fall back to sans-serif before any face. - let sans_query = usvg::fontdb::Query { - families: &[usvg::fontdb::Family::SansSerif], - ..Default::default() - }; - db.query(&sans_query) - .or_else(|| db.faces().next().map(|f| f.id)) - } - }); - let default_fallback_selection = usvg::FontResolver::default_fallback_selector(); - let fallback_selection = Box::new( - move |ch: char, fonts: &[usvg::fontdb::ID], db: &mut Arc| { - if is_emoji_presentation(ch) { - if let Some(id) = select_emoji_font(ch, fonts, db.as_ref(), EMOJI_FONT_FAMILIES) - { - return Some(id); - } - } - - default_fallback_selection(ch, fonts, db) - }, - ); - let options = usvg::Options { - font_resolver: usvg::FontResolver { - select_font: font_resolver, - select_fallback: fallback_selection, - }, - ..Default::default() - }; - Self { - asset_source, - usvg_options: Arc::new(options), - } - } - - /// Parses SVG data into a [`ParsedSvg`] that can be rasterized at any scale. - #[ztracing::instrument(skip_all)] - pub fn parse_svg(&self, bytes: &[u8]) -> Result { - usvg::Tree::from_data(bytes, &self.usvg_options).map(ParsedSvg) - } - - /// Rasterizes a previously parsed SVG into an image buffer. - #[ztracing::instrument(skip_all)] - pub fn render_parsed( - &self, - svg: &ParsedSvg, - size: impl Into, - ) -> Result, usvg::Error> { - let (size, image_scale_factor) = match size.into() { - SvgSize::Size(size) => (SvgSize::Size(size), 1.0), - SvgSize::ExactSize(size) => (SvgSize::ExactSize(size), 1.0), - SvgSize::ScaleFactor(scale_factor) => ( - SvgSize::ScaleFactor(scale_factor * SMOOTH_SVG_SCALE_FACTOR), - SMOOTH_SVG_SCALE_FACTOR, - ), - }; - let pixmap = rasterize_tree(&svg.0, size)?; - let mut buffer = - image::ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap(); - - for pixel in buffer.chunks_exact_mut(4) { - swap_rgba_pa_to_bgra(pixel); - } - - let mut image = RenderImage::new(SmallVec::from_const([Frame::new(buffer)])); - image.scale_factor = image_scale_factor; - Ok(Arc::new(image)) - } - - /// Renders the given bytes into an image buffer. - pub fn render_single_frame( - &self, - bytes: &[u8], - scale_factor: f32, - ) -> Result, usvg::Error> { - let svg = self.parse_svg(bytes)?; - self.render_parsed(&svg, scale_factor) - } - - pub(crate) fn render_alpha_mask( - &self, - params: &RenderSvgParams, - bytes: Option<&[u8]>, - ) -> Result, Vec)>> { - anyhow::ensure!(!params.size.is_zero(), "can't render at a zero size"); - - let render_pixmap = |bytes| { - let pixmap = self.render_pixmap(bytes, SvgSize::Size(params.size))?; - - // Convert the pixmap's pixels into an alpha mask. - let size = Size::new( - DevicePixels(pixmap.width() as i32), - DevicePixels(pixmap.height() as i32), - ); - let alpha_mask = pixmap - .pixels() - .iter() - .map(|p| p.alpha()) - .collect::>(); - - Ok(Some((size, alpha_mask))) - }; - - if let Some(bytes) = bytes { - render_pixmap(bytes) - } else if let Some(bytes) = self.asset_source.load(¶ms.path)? { - render_pixmap(&bytes) - } else { - Ok(None) - } - } - - fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result { - let tree = usvg::Tree::from_data(bytes, &self.usvg_options)?; - rasterize_tree(&tree, size) - } -} - -fn rasterize_tree(tree: &usvg::Tree, size: SvgSize) -> Result { - // Cap the size of the rendered pixmap to avoid texture allocation panics - // Related issue: #56466 - const MAX_SIZE: f32 = 8192.0; - - let svg_size = tree.size(); - let (mut width, mut height) = match size { - SvgSize::Size(size) => { - let scale = i32::from(size.width) as f32 / svg_size.width(); - (svg_size.width() * scale, svg_size.height() * scale) - } - SvgSize::ExactSize(size) => (i32::from(size.width) as f32, i32::from(size.height) as f32), - SvgSize::ScaleFactor(scale) => (svg_size.width() * scale, svg_size.height() * scale), - }; - - if width > MAX_SIZE { - log::warn!("Attempted to render pixmap where width ({width}) > MAX_SIZE ({MAX_SIZE})"); - } - if height > MAX_SIZE { - log::warn!("Attempted to render pixmap where height ({height}) > MAX_SIZE ({MAX_SIZE})"); - } - let scale = (MAX_SIZE / width).min(MAX_SIZE / height).min(1.0); - width *= scale; - height *= scale; - - // Render the SVG to a pixmap with the specified width and height. - let mut pixmap = resvg::tiny_skia::Pixmap::new(width as u32, height as u32) - .ok_or(usvg::Error::InvalidSize)?; - - let transform = resvg::tiny_skia::Transform::from_scale( - width / svg_size.width(), - height / svg_size.height(), - ); - - resvg::render(tree, transform, &mut pixmap.as_mut()); - - Ok(pixmap) -} - -fn load_bundled_fonts(asset_source: &dyn AssetSource, db: &mut usvg::fontdb::Database) { - let font_paths = [ - "fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf", - "fonts/lilex/Lilex-Regular.ttf", - ]; - for path in font_paths { - match asset_source.load(path) { - Ok(Some(data)) => db.load_font_data(data.into_owned()), - Ok(None) => log::warn!("Bundled font not found: {path}"), - Err(error) => log::warn!("Failed to load bundled font {path}: {error}"), - } - } -} - -// fontdb defaults generic families to Microsoft fonts ("Arial", "Times New Roman") -// which aren't installed on most Linux systems. fontconfig normally overrides these, -// but when it fails the defaults remain and all generic family queries return None. -fn fix_generic_font_families(db: &mut usvg::fontdb::Database) { - use usvg::fontdb::{Family, Query}; - - let families_and_fallbacks: &[(Family<'_>, &str)] = &[ - (Family::SansSerif, "IBM Plex Sans"), - // No serif font bundled; use sans-serif as best available fallback. - (Family::Serif, "IBM Plex Sans"), - (Family::Monospace, "Lilex"), - (Family::Cursive, "IBM Plex Sans"), - (Family::Fantasy, "IBM Plex Sans"), - ]; - - for (family, fallback_name) in families_and_fallbacks { - let query = Query { - families: &[*family], - ..Default::default() - }; - if db.query(&query).is_none() { - match family { - Family::SansSerif => db.set_sans_serif_family(*fallback_name), - Family::Serif => db.set_serif_family(*fallback_name), - Family::Monospace => db.set_monospace_family(*fallback_name), - Family::Cursive => db.set_cursive_family(*fallback_name), - Family::Fantasy => db.set_fantasy_family(*fallback_name), - _ => {} - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use usvg::fontdb::{Database, Family, Query}; - - const IBM_PLEX_REGULAR: &[u8] = - include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf"); - const LILEX_REGULAR: &[u8] = include_bytes!("../../../assets/fonts/lilex/Lilex-Regular.ttf"); - - #[test] - fn renders_parsed_svg_at_requested_size() -> Result<()> { - let renderer = SvgRenderer::new(Arc::new(())); - let svg = renderer.parse_svg( - br#""#, - )?; - let requested_size = Size::new(DevicePixels(24), DevicePixels(12)); - let image = renderer.render_parsed(&svg, SvgSize::ExactSize(requested_size))?; - - assert_eq!(image.size(0), requested_size); - Ok(()) - } - - #[test] - fn preserves_aspect_ratio_for_width_constrained_size() -> Result<()> { - let renderer = SvgRenderer::new(Arc::new(())); - let svg = renderer.parse_svg( - br#""#, - )?; - let image = renderer.render_parsed( - &svg, - SvgSize::Size(Size::new(DevicePixels(24), DevicePixels(24))), - )?; - - assert_eq!(image.size(0), Size::new(DevicePixels(24), DevicePixels(12))); - Ok(()) - } - - fn db_with_bundled_fonts() -> Database { - let mut db = Database::new(); - db.load_font_data(IBM_PLEX_REGULAR.to_vec()); - db.load_font_data(LILEX_REGULAR.to_vec()); - db - } - - #[test] - fn text_with_split_glyph_clusters_in_mixed_fonts_does_not_panic() { - let mut db = Database::new(); - db.load_font_data(IBM_PLEX_REGULAR.to_vec()); - db.load_font_data(LILEX_REGULAR.to_vec()); - let options = usvg::Options { - fontdb: std::sync::Arc::new(db), - ..Default::default() - }; - - // A base letter followed by a stack of combining marks. Under HarfBuzz's - // default cluster merging every mark glyph shares the base's byte index, - // which is the "glyph splitting" condition that triggered the panic. The - // chunk must use two different fonts so the buggy merge path runs. - let zalgo = "e\u{0301}\u{0302}\u{0303}\u{0304}\u{0306}\u{0307}\u{0308}\u{030a}"; - let svg = format!( - r#"{zalgo}{zalgo}"# - ); - - // Before the fix this aborts via panic with a message like - // "removal index (is 5) should be < len (is 5)". - usvg::Tree::from_data(svg.as_bytes(), &options) - .expect("SVG with mixed-font text should parse"); - } - - #[test] - fn test_is_emoji_presentation() { - let cases = [ - ("a", false), - ("Z", false), - ("1", false), - ("#", false), - ("*", false), - ("漢", false), - ("中", false), - ("カ", false), - ("©", false), - ("♥", false), - ("😀", true), - ("✅", true), - ("🇺🇸", true), - // SVG fallback is not cluster-aware yet - ("©️", false), - ("♥️", false), - ("1️⃣", false), - ]; - for (s, expected) in cases { - assert_eq!( - is_emoji_presentation(s.chars().next().unwrap()), - expected, - "for char {:?}", - s - ); - } - } - - #[test] - fn fix_generic_font_families_sets_all_families() { - let mut db = db_with_bundled_fonts(); - fix_generic_font_families(&mut db); - - let families = [ - Family::SansSerif, - Family::Serif, - Family::Monospace, - Family::Cursive, - Family::Fantasy, - ]; - - for family in families { - let query = Query { - families: &[family], - ..Default::default() - }; - assert!( - db.query(&query).is_some(), - "Expected generic family {family:?} to resolve after fix_generic_font_families" - ); - } - } - - #[test] - fn test_select_emoji_font_skips_family_without_glyph() { - let mut db = db_with_bundled_fonts(); - - let ibm_plex_sans = db - .query(&usvg::fontdb::Query { - families: &[usvg::fontdb::Family::Name("IBM Plex Sans")], - weight: usvg::fontdb::Weight(400), - stretch: usvg::fontdb::Stretch::Normal, - style: usvg::fontdb::Style::Normal, - }) - .unwrap(); - let lilex = db - .query(&usvg::fontdb::Query { - families: &[usvg::fontdb::Family::Name("Lilex")], - weight: usvg::fontdb::Weight(400), - stretch: usvg::fontdb::Stretch::Normal, - style: usvg::fontdb::Style::Normal, - }) - .unwrap(); - let selected = select_emoji_font('│', &[], &db, &["IBM Plex Sans", "Lilex"]).unwrap(); - - assert_eq!(selected, lilex); - assert!(!font_has_char(&db, ibm_plex_sans, '│')); - assert!(font_has_char(&db, selected, '│')); - } - - #[test] - fn fix_generic_font_families_monospace_resolves_to_lilex() { - let mut db = db_with_bundled_fonts(); - fix_generic_font_families(&mut db); - - let query = Query { - families: &[Family::Monospace], - ..Default::default() - }; - let id = db.query(&query).expect("Monospace should resolve"); - let face = db.face(id).expect("Face should exist"); - assert!( - face.families.iter().any(|(name, _)| name.contains("Lilex")), - "Monospace should map to Lilex, got {:?}", - face.families - ); - } -} diff --git a/crates/gpui_pre/src/tab_stop.rs b/crates/gpui_pre/src/tab_stop.rs deleted file mode 100644 index bde651a..0000000 --- a/crates/gpui_pre/src/tab_stop.rs +++ /dev/null @@ -1,615 +0,0 @@ -use std::fmt::Debug; - -use ::sum_tree::SumTree; -use collections::FxHashMap; -use sum_tree::Bias; - -use crate::{FocusHandle, FocusId}; - -/// Represents a collection of focus handles using the tab-index APIs. -#[derive(Debug)] -pub(crate) struct TabStopMap { - current_path: TabStopPath, - pub(crate) insertion_history: Vec, - by_id: FxHashMap, - order: SumTree, -} - -#[derive(Debug, Clone)] -pub enum TabStopOperation { - Insert(FocusHandle), - Group(TabIndex), - GroupEnd, -} - -impl TabStopOperation { - fn focus_handle(&self) -> Option<&FocusHandle> { - match self { - TabStopOperation::Insert(focus_handle) => Some(focus_handle), - _ => None, - } - } -} - -type TabIndex = isize; - -#[derive(Debug, Default, PartialEq, Eq, Clone, Ord, PartialOrd)] -struct TabStopPath(smallvec::SmallVec<[TabIndex; 6]>); - -#[derive(Clone, Debug, Default, Eq, PartialEq)] -struct TabStopNode { - /// Path to access the node in the tree - /// The final node in the list is a leaf node corresponding to an actual focus handle, - /// all other nodes are group nodes - path: TabStopPath, - /// index into the backing array of nodes. Corresponds to insertion order - node_insertion_index: usize, - - /// Whether this node is a tab stop - tab_stop: bool, -} - -impl Ord for TabStopNode { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.path - .cmp(&other.path) - .then(self.node_insertion_index.cmp(&other.node_insertion_index)) - } -} - -impl PartialOrd for TabStopNode { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(&other)) - } -} - -impl Default for TabStopMap { - fn default() -> Self { - Self { - current_path: TabStopPath::default(), - insertion_history: Vec::new(), - by_id: FxHashMap::default(), - order: SumTree::new(()), - } - } -} - -impl TabStopMap { - pub fn insert(&mut self, focus_handle: &FocusHandle) { - self.insertion_history - .push(TabStopOperation::Insert(focus_handle.clone())); - let mut path = self.current_path.clone(); - path.0.push(focus_handle.tab_index); - let order = TabStopNode { - node_insertion_index: self.insertion_history.len() - 1, - tab_stop: focus_handle.tab_stop, - path, - }; - self.by_id.insert(focus_handle.id, order.clone()); - self.order.insert_or_replace(order, ()); - } - - pub fn begin_group(&mut self, tab_index: isize) { - self.insertion_history - .push(TabStopOperation::Group(tab_index)); - self.current_path.0.push(tab_index); - } - - pub fn end_group(&mut self) { - self.insertion_history.push(TabStopOperation::GroupEnd); - self.current_path.0.pop(); - } - - pub fn clear(&mut self) { - *self = Self::default(); - self.current_path.0.clear(); - self.insertion_history.clear(); - self.by_id.clear(); - self.order = SumTree::new(()); - } - - pub fn next(&self, focused_id: Option<&FocusId>) -> Option { - let Some(focused_id) = focused_id else { - let first = self.order.first()?; - if first.tab_stop { - return self.focus_handle_for_order(first); - } else { - return self - .next_inner(first) - .and_then(|order| self.focus_handle_for_order(order)); - } - }; - - let Some(node) = self.tab_node_for_focus_id(focused_id) else { - return self.next(None); - }; - let item = self.next_inner(node); - - if let Some(item) = item { - self.focus_handle_for_order(&item) - } else { - self.next(None) - } - } - - fn next_inner(&self, node: &TabStopNode) -> Option<&TabStopNode> { - let mut cursor = self.order.cursor::(()); - cursor.seek(&node, Bias::Left); - cursor.next(); - while let Some(item) = cursor.item() - && !item.tab_stop - { - cursor.next(); - } - - cursor.item() - } - - pub fn prev(&self, focused_id: Option<&FocusId>) -> Option { - let Some(focused_id) = focused_id else { - let last = self.order.last()?; - if last.tab_stop { - return self.focus_handle_for_order(last); - } else { - return self - .prev_inner(last) - .and_then(|order| self.focus_handle_for_order(order)); - } - }; - - let Some(node) = self.tab_node_for_focus_id(focused_id) else { - return self.prev(None); - }; - let item = self.prev_inner(node); - - if let Some(item) = item { - self.focus_handle_for_order(&item) - } else { - self.prev(None) - } - } - - fn prev_inner(&self, node: &TabStopNode) -> Option<&TabStopNode> { - let mut cursor = self.order.cursor::(()); - cursor.seek(&node, Bias::Left); - cursor.prev(); - while let Some(item) = cursor.item() - && !item.tab_stop - { - cursor.prev(); - } - - cursor.item() - } - - pub fn replay(&mut self, nodes: &[TabStopOperation]) { - for node in nodes { - match node { - TabStopOperation::Insert(focus_handle) => self.insert(focus_handle), - TabStopOperation::Group(tab_index) => self.begin_group(*tab_index), - TabStopOperation::GroupEnd => self.end_group(), - } - } - } - - pub fn paint_index(&self) -> usize { - self.insertion_history.len() - } - - pub(crate) fn tab_stop_count(&self) -> usize { - self.by_id.values().filter(|node| node.tab_stop).count() - } - - fn focus_handle_for_order(&self, order: &TabStopNode) -> Option { - let handle = self.insertion_history[order.node_insertion_index].focus_handle(); - debug_assert!( - handle.is_some(), - "The order node did not correspond to an element, this is a GPUI bug" - ); - handle.cloned() - } - - fn tab_node_for_focus_id(&self, focused_id: &FocusId) -> Option<&TabStopNode> { - let Some(order) = self.by_id.get(focused_id) else { - return None; - }; - Some(order) - } -} - -mod sum_tree_impl { - use sum_tree::SeekTarget; - - use crate::tab_stop::{TabStopNode, TabStopPath}; - - #[derive(Clone, Debug)] - pub struct TabStopOrderNodeSummary { - max_index: usize, - max_path: TabStopPath, - pub tab_stops: usize, - } - - pub type TabStopCount = usize; - - impl sum_tree::ContextLessSummary for TabStopOrderNodeSummary { - fn zero() -> Self { - TabStopOrderNodeSummary { - max_index: 0, - max_path: TabStopPath::default(), - tab_stops: 0, - } - } - - fn add_summary(&mut self, summary: &Self) { - self.max_index = summary.max_index; - self.max_path = summary.max_path.clone(); - self.tab_stops += summary.tab_stops; - } - } - - impl sum_tree::KeyedItem for TabStopNode { - type Key = Self; - - fn key(&self) -> Self::Key { - self.clone() - } - } - - impl sum_tree::Item for TabStopNode { - type Summary = TabStopOrderNodeSummary; - - fn summary(&self, _cx: ::Context<'_>) -> Self::Summary { - TabStopOrderNodeSummary { - max_index: self.node_insertion_index, - max_path: self.path.clone(), - tab_stops: if self.tab_stop { 1 } else { 0 }, - } - } - } - - impl<'a> sum_tree::Dimension<'a, TabStopOrderNodeSummary> for TabStopCount { - fn zero(_: ::Context<'_>) -> Self { - 0 - } - - fn add_summary( - &mut self, - summary: &'a TabStopOrderNodeSummary, - _: ::Context<'_>, - ) { - *self += summary.tab_stops; - } - } - - impl<'a> sum_tree::Dimension<'a, TabStopOrderNodeSummary> for TabStopNode { - fn zero(_: ::Context<'_>) -> Self { - TabStopNode::default() - } - - fn add_summary( - &mut self, - summary: &'a TabStopOrderNodeSummary, - _: ::Context<'_>, - ) { - self.node_insertion_index = summary.max_index; - self.path = summary.max_path.clone(); - } - } - - impl<'a, 'b> SeekTarget<'a, TabStopOrderNodeSummary, TabStopNode> for &'b TabStopNode { - fn cmp( - &self, - cursor_location: &TabStopNode, - _: ::Context<'_>, - ) -> std::cmp::Ordering { - Iterator::cmp(self.path.0.iter(), cursor_location.path.0.iter()).then( - ::cmp( - &self.node_insertion_index, - &cursor_location.node_insertion_index, - ), - ) - } - } -} - -#[cfg(test)] -mod tests { - use itertools::Itertools as _; - - use crate::{FocusHandle, FocusId, FocusMap, TabStopMap}; - use std::sync::Arc; - - #[test] - fn test_tab_handles() { - let focus_map = Arc::new(FocusMap::default()); - let mut tab_index_map = TabStopMap::default(); - - let focus_handles = [ - FocusHandle::new(&focus_map).tab_stop(true).tab_index(0), - FocusHandle::new(&focus_map).tab_stop(true).tab_index(1), - FocusHandle::new(&focus_map).tab_stop(true).tab_index(1), - FocusHandle::new(&focus_map), - FocusHandle::new(&focus_map).tab_index(2), - FocusHandle::new(&focus_map).tab_stop(true).tab_index(0), - FocusHandle::new(&focus_map).tab_stop(true).tab_index(2), - ]; - - for handle in focus_handles.iter() { - tab_index_map.insert(handle); - } - let expected = [ - focus_handles[0].clone(), - focus_handles[5].clone(), - focus_handles[1].clone(), - focus_handles[2].clone(), - focus_handles[6].clone(), - ]; - - let mut prev = None; - let mut found = vec![]; - for _ in 0..expected.len() { - let handle = tab_index_map.next(prev.as_ref()).unwrap(); - prev = Some(handle.id); - found.push(handle.id); - } - - assert_eq!( - found, - expected.iter().map(|handle| handle.id).collect::>() - ); - - // Select first tab index if no handle is currently focused. - assert_eq!(tab_index_map.next(None), Some(expected[0].clone())); - // Select last tab index if no handle is currently focused. - assert_eq!(tab_index_map.prev(None), expected.last().cloned(),); - - assert_eq!( - tab_index_map.next(Some(&expected[0].id)), - Some(expected[1].clone()) - ); - assert_eq!( - tab_index_map.next(Some(&expected[1].id)), - Some(expected[2].clone()) - ); - assert_eq!( - tab_index_map.next(Some(&expected[2].id)), - Some(expected[3].clone()) - ); - assert_eq!( - tab_index_map.next(Some(&expected[3].id)), - Some(expected[4].clone()) - ); - assert_eq!( - tab_index_map.next(Some(&expected[4].id)), - Some(expected[0].clone()) - ); - - // prev - assert_eq!(tab_index_map.prev(None), Some(expected[4].clone())); - assert_eq!( - tab_index_map.prev(Some(&expected[0].id)), - Some(expected[4].clone()) - ); - assert_eq!( - tab_index_map.prev(Some(&expected[1].id)), - Some(expected[0].clone()) - ); - assert_eq!( - tab_index_map.prev(Some(&expected[2].id)), - Some(expected[1].clone()) - ); - assert_eq!( - tab_index_map.prev(Some(&expected[3].id)), - Some(expected[2].clone()) - ); - assert_eq!( - tab_index_map.prev(Some(&expected[4].id)), - Some(expected[3].clone()) - ); - } - - #[test] - fn test_tab_non_stop_filtering() { - let focus_map = Arc::new(FocusMap::default()); - let mut tab_index_map = TabStopMap::default(); - - // Check that we can query next from a non-stop tab - let tab_non_stop_1 = FocusHandle::new(&focus_map).tab_stop(false).tab_index(1); - let tab_stop_2 = FocusHandle::new(&focus_map).tab_stop(true).tab_index(2); - tab_index_map.insert(&tab_non_stop_1); - tab_index_map.insert(&tab_stop_2); - let result = tab_index_map.next(Some(&tab_non_stop_1.id)).unwrap(); - assert_eq!(result.id, tab_stop_2.id); - - // Check that we skip over non-stop tabs - let tab_stop_0 = FocusHandle::new(&focus_map).tab_stop(true).tab_index(0); - let tab_non_stop_0 = FocusHandle::new(&focus_map).tab_stop(false).tab_index(0); - tab_index_map.insert(&tab_stop_0); - tab_index_map.insert(&tab_non_stop_0); - let result = tab_index_map.next(Some(&tab_stop_0.id)).unwrap(); - assert_eq!(result.id, tab_stop_2.id); - } - - #[must_use] - struct TabStopMapTest { - tab_map: TabStopMap, - focus_map: Arc, - expected: Vec<(usize, FocusId)>, - } - - impl TabStopMapTest { - #[must_use] - fn new() -> Self { - Self { - tab_map: TabStopMap::default(), - focus_map: Arc::new(FocusMap::default()), - expected: Vec::default(), - } - } - - #[must_use] - fn tab_non_stop(mut self, index: isize) -> Self { - let handle = FocusHandle::new(&self.focus_map) - .tab_stop(false) - .tab_index(index); - self.tab_map.insert(&handle); - self - } - - #[must_use] - fn tab_stop(mut self, index: isize, expected: usize) -> Self { - let handle = FocusHandle::new(&self.focus_map) - .tab_stop(true) - .tab_index(index); - self.tab_map.insert(&handle); - self.expected.push((expected, handle.id)); - self.expected.sort_by_key(|(expected, _)| *expected); - self - } - - #[must_use] - fn tab_group(mut self, tab_index: isize, children: impl FnOnce(Self) -> Self) -> Self { - self.tab_map.begin_group(tab_index); - self = children(self); - self.tab_map.end_group(); - self - } - - fn traverse_tab_map( - &self, - traverse: impl Fn(&TabStopMap, Option<&FocusId>) -> Option, - ) -> Vec { - let mut last_focus_id = None; - let mut found = vec![]; - for _ in 0..self.expected.len() { - let handle = traverse(&self.tab_map, last_focus_id.as_ref()).unwrap(); - last_focus_id = Some(handle.id); - found.push(handle.id); - } - found - } - - fn assert(self) { - let mut expected = self.expected.iter().map(|(_, id)| *id).collect_vec(); - - // Check next order - let forward_found = self.traverse_tab_map(|tab_map, prev| tab_map.next(prev)); - assert_eq!(forward_found, expected); - - // Test overflow. Last to first - assert_eq!( - self.tab_map - .next(forward_found.last()) - .map(|handle| handle.id), - expected.first().cloned() - ); - - // Check previous order - let reversed_found = self.traverse_tab_map(|tab_map, prev| tab_map.prev(prev)); - expected.reverse(); - assert_eq!(reversed_found, expected); - - // Test overflow. First to last - assert_eq!( - self.tab_map - .prev(reversed_found.last()) - .map(|handle| handle.id), - expected.first().cloned(), - ); - } - } - - #[test] - fn test_with_disabled_tab_stop() { - TabStopMapTest::new() - .tab_stop(0, 0) - .tab_non_stop(1) - .tab_stop(2, 1) - .tab_stop(3, 2) - .assert(); - } - - #[test] - fn test_with_multiple_disabled_tab_stops() { - TabStopMapTest::new() - .tab_non_stop(0) - .tab_stop(1, 0) - .tab_non_stop(3) - .tab_stop(3, 1) - .tab_non_stop(4) - .assert(); - } - - #[test] - fn test_tab_group_functionality() { - TabStopMapTest::new() - .tab_stop(0, 0) - .tab_stop(0, 1) - .tab_group(2, |t| t.tab_stop(0, 2).tab_stop(1, 3)) - .tab_stop(3, 4) - .tab_stop(4, 5) - .assert() - } - - #[test] - fn test_sibling_groups() { - TabStopMapTest::new() - .tab_stop(0, 0) - .tab_stop(1, 1) - .tab_group(2, |test| test.tab_stop(0, 2).tab_stop(1, 3)) - .tab_stop(3, 4) - .tab_stop(4, 5) - .tab_group(6, |test| test.tab_stop(0, 6).tab_stop(1, 7)) - .tab_stop(7, 8) - .tab_stop(8, 9) - .assert(); - } - - #[test] - fn test_nested_group() { - TabStopMapTest::new() - .tab_stop(0, 0) - .tab_stop(1, 1) - .tab_group(2, |t| { - t.tab_group(0, |t| t.tab_stop(0, 2).tab_stop(1, 3)) - .tab_stop(1, 4) - }) - .tab_stop(3, 5) - .tab_stop(4, 6) - .assert(); - } - - #[test] - fn test_sibling_nested_groups() { - TabStopMapTest::new() - .tab_stop(0, 0) - .tab_stop(1, 1) - .tab_group(2, |builder| { - builder - .tab_stop(0, 2) - .tab_stop(2, 5) - .tab_group(1, |builder| builder.tab_stop(0, 3).tab_stop(1, 4)) - .tab_group(3, |builder| builder.tab_stop(0, 6).tab_stop(1, 7)) - }) - .tab_stop(3, 8) - .tab_stop(4, 9) - .assert(); - } - - #[test] - fn test_sibling_nested_groups_out_of_order() { - TabStopMapTest::new() - .tab_stop(9, 9) - .tab_stop(8, 8) - .tab_group(7, |builder| { - builder - .tab_stop(0, 2) - .tab_stop(2, 5) - .tab_group(3, |builder| builder.tab_stop(1, 7).tab_stop(0, 6)) - .tab_group(1, |builder| builder.tab_stop(0, 3).tab_stop(1, 4)) - }) - .tab_stop(3, 0) - .tab_stop(4, 1) - .assert(); - } -} diff --git a/crates/gpui_pre/src/taffy.rs b/crates/gpui_pre/src/taffy.rs deleted file mode 100644 index b9e583a..0000000 --- a/crates/gpui_pre/src/taffy.rs +++ /dev/null @@ -1,776 +0,0 @@ -use crate::{ - AbsoluteLength, App, Bounds, DefiniteLength, Edges, GridTemplate, Length, Pixels, Point, Size, - Style, Window, size, - util::{ - ceil_to_device_pixel, round_half_toward_zero, round_stroke_to_device_pixel, - round_to_device_pixel, - }, -}; -use collections::{FxHashMap, FxHashSet}; -use std::{fmt::Debug, ops::Range}; -use taffy::{ - TaffyTree, TraversePartialTree as _, - geometry::{Point as TaffyPoint, Rect as TaffyRect, Size as TaffySize}, - prelude::{max_content, min_content}, - style::AvailableSpace as TaffyAvailableSpace, - tree::NodeId, -}; - -#[cfg(feature = "stacker")] -type StackSafe = stacksafe::StackSafe; -#[cfg(not(feature = "stacker"))] -type StackSafe = T; - -type MeasureFn = - dyn FnMut(Size>, Size, &mut Window, &mut App) -> Size; -type NodeMeasureFn = StackSafe>; - -struct NodeContext { - measure: NodeMeasureFn, -} -pub struct TaffyLayoutEngine { - taffy: TaffyTree, - absolute_layout_bounds: FxHashMap>, - /// Unrounded absolute border-box top-left per-node coordinate in device pixels. - absolute_outer_origins: FxHashMap>, - computed_layouts: FxHashSet, - layout_bounds_scratch_space: Vec, -} - -const EXPECT_MESSAGE: &str = "we should avoid taffy layout errors by construction if possible"; - -impl TaffyLayoutEngine { - pub fn new() -> Self { - let mut taffy = TaffyTree::new(); - taffy.disable_rounding(); - TaffyLayoutEngine { - taffy, - absolute_layout_bounds: FxHashMap::default(), - absolute_outer_origins: FxHashMap::default(), - computed_layouts: FxHashSet::default(), - layout_bounds_scratch_space: Vec::new(), - } - } - - pub fn clear(&mut self) { - self.taffy.clear(); - self.absolute_layout_bounds.clear(); - self.absolute_outer_origins.clear(); - self.computed_layouts.clear(); - } - - pub fn request_layout( - &mut self, - style: Style, - rem_size: Pixels, - scale_factor: f32, - children: &[LayoutId], - ) -> LayoutId { - let taffy_style = style.to_taffy(rem_size, scale_factor); - - if children.is_empty() { - self.taffy - .new_leaf(taffy_style) - .expect(EXPECT_MESSAGE) - .into() - } else { - self.taffy - // This is safe because LayoutId is repr(transparent) to taffy::tree::NodeId. - .new_with_children(taffy_style, LayoutId::to_taffy_slice(children)) - .expect(EXPECT_MESSAGE) - .into() - } - } - - pub fn request_measured_layout( - &mut self, - style: Style, - rem_size: Pixels, - scale_factor: f32, - measure: impl FnMut( - Size>, - Size, - &mut Window, - &mut App, - ) -> Size - + 'static, - ) -> LayoutId { - let taffy_style = style.to_taffy(rem_size, scale_factor); - let measure = Box::new(measure) as Box; - #[cfg(feature = "stacker")] - let measure = StackSafe::new(measure); - - self.taffy - .new_leaf_with_context(taffy_style, NodeContext { measure }) - .expect(EXPECT_MESSAGE) - .into() - } - - /// Treats any `auto` dimension of the given node's style as filling `size`. - /// - /// This is applied to window roots before layout so they behave like the - /// root element on the web, which stretches to fill the initial containing - /// block (the viewport) unless given an explicit size. Explicitly styled - /// dimensions are preserved. - pub fn stretch_auto_size_to_fill( - &mut self, - id: LayoutId, - size: Size, - scale_factor: f32, - ) { - let style = self.taffy.style(id.0).expect(EXPECT_MESSAGE); - let stretch_width = style.size.width.is_auto(); - let stretch_height = style.size.height.is_auto(); - if !stretch_width && !stretch_height { - return; - } - let mut style = style.clone(); - if stretch_width { - style.size.width = - taffy::style::Dimension::length(round_to_device_pixel(size.width.0, scale_factor)); - } - if stretch_height { - style.size.height = - taffy::style::Dimension::length(round_to_device_pixel(size.height.0, scale_factor)); - } - self.taffy.set_style(id.0, style).expect(EXPECT_MESSAGE); - } - - // Used to understand performance - #[allow(dead_code)] - fn count_all_children(&self, parent: LayoutId) -> anyhow::Result { - let mut count = 0; - - for child in self.taffy.children(parent.0)? { - // Count this child. - count += 1; - - // Count all of this child's children. - count += self.count_all_children(LayoutId(child))? - } - - Ok(count) - } - - // Used to understand performance - #[allow(dead_code)] - fn max_depth(&self, depth: u32, parent: LayoutId) -> anyhow::Result { - println!( - "{parent:?} at depth {depth} has {} children", - self.taffy.child_count(parent.0) - ); - - let mut max_child_depth = 0; - - for child in self.taffy.children(parent.0)? { - max_child_depth = std::cmp::max(max_child_depth, self.max_depth(0, LayoutId(child))?); - } - - Ok(depth + 1 + max_child_depth) - } - - // Used to understand performance - #[allow(dead_code)] - fn get_edges(&self, parent: LayoutId) -> anyhow::Result> { - let mut edges = Vec::new(); - - for child in self.taffy.children(parent.0)? { - edges.push((parent, LayoutId(child))); - - edges.extend(self.get_edges(LayoutId(child))?); - } - - Ok(edges) - } - - #[cfg_attr(feature = "stacker", stacksafe::stacksafe)] - pub fn compute_layout( - &mut self, - id: LayoutId, - available_space: Size, - window: &mut Window, - cx: &mut App, - ) { - // Leaving this here until we have a better instrumentation approach. - // println!("Laying out {} children", self.count_all_children(id)?); - // println!("Max layout depth: {}", self.max_depth(0, id)?); - - // Output the edges (branches) of the tree in Mermaid format for visualization. - // println!("Edges:"); - // for (a, b) in self.get_edges(id)? { - // println!("N{} --> N{}", u64::from(a), u64::from(b)); - // } - // - - if !self.computed_layouts.insert(id) { - let stack = &mut self.layout_bounds_scratch_space; - stack.push(id); - while let Some(id) = stack.pop() { - self.absolute_layout_bounds.remove(&id); - self.absolute_outer_origins.remove(&id); - stack.extend( - self.taffy - .children(id.into()) - .expect(EXPECT_MESSAGE) - .into_iter() - .map(LayoutId::from), - ); - } - } - - let scale_factor = window.scale_factor(); - - let transform = |v: AvailableSpace| match v { - AvailableSpace::Definite(pixels) => { - AvailableSpace::Definite(Pixels(pixels.0 * scale_factor)) - } - AvailableSpace::MinContent => AvailableSpace::MinContent, - AvailableSpace::MaxContent => AvailableSpace::MaxContent, - }; - let available_space = size( - transform(available_space.width), - transform(available_space.height), - ); - - self.taffy - .compute_layout_with_measure( - id.into(), - available_space.into(), - |known_dimensions, available_space, _id, node_context, _style| { - let Some(node_context) = node_context else { - return taffy::geometry::Size::default(); - }; - - let known_dimensions = Size { - width: known_dimensions.width.map(|e| Pixels(e / scale_factor)), - height: known_dimensions.height.map(|e| Pixels(e / scale_factor)), - }; - - let available_space: Size = available_space.into(); - let untransform = |ev: AvailableSpace| match ev { - AvailableSpace::Definite(pixels) => { - AvailableSpace::Definite(Pixels(pixels.0 / scale_factor)) - } - AvailableSpace::MinContent => AvailableSpace::MinContent, - AvailableSpace::MaxContent => AvailableSpace::MaxContent, - }; - let available_space = size( - untransform(available_space.width), - untransform(available_space.height), - ); - - let measured_size: Size = - (node_context.measure)(known_dimensions, available_space, window, cx); - snap_measured_size_to_device_pixels(measured_size, scale_factor).into() - }, - ) - .expect(EXPECT_MESSAGE); - } - - // Pixel snapping - // - // Painting primitives at non-integer pixel coordinates produces blurry - // output. Pixel snapping converts layout coordinates into integer - // device-pixel coordinates so painted edges land exactly on physical - // pixel boundaries. - // - // Non-integer coordinates can arise for several reasons, including: - // - flex distribution, percentages, centering, and text measurement - // can produce fractional element sizes and positions; - // - at fractional scale factors (for example 125% or 150%), integer - // logical-pixel values can map to non-integer device-pixel values. - // - // We pixel-snap by rounding in device-pixel space, after multiplying - // by `scale_factor`, so that snapping targets physical pixels. Bounds - // are divided by `scale_factor` before being returned to GPUI. - // - // Midpoints are rounded toward zero. This is a stylistic choice: a - // 1-logical-pixel line at 150% scale should render as 1 dp rather than - // 2 dp. - // - // Pixel snapping is done in two phases: - // - // 1. Pre-layout metric snapping. Before Taffy computes layout, all - // authored absolute lengths are rounded in `to_taffy`. This - // includes borders, padding, gaps, and explicit sizes. - // Custom-measured leaf nodes have their measured sizes rounded up - // to integer device-pixel lengths. - // - // 2. Post-layout edge snapping. After Taffy resolves the tree, layout - // relationships such as flex shares, grid tracks, percentages, and - // centering can produce new fractional edge positions. Boxes now - // have edges in absolute coordinates, and snapping must decide - // where those edges land on the device-pixel grid. - // - // Ideally, post-layout snapping would satisfy: - // - // - Edge closure. Two raw layout edges at the same absolute position - // should snap to the same pixel column. - // - Translation stability. A component's internal geometry should not - // change when it moves to a new absolute position. - // - // These goals are in tension because rounding is not associative. - // The simple local schemes make different tradeoffs: - // - // - Absolute edge rounding gives each window coordinate one answer, - // so coincident edges always close globally. But a span's snapped - // length is `round(far) - round(near)`, which may change by 1 dp - // as its absolute origin moves. - // - // - Parent-relative edge rounding rounds each child inside its - // parent's coordinate space. This guarantees translation stability, - // but a shared edge reached through different parents can - // accumulate different rounding, causing non-closure between - // cousins. - // - // - Length rounding rounds each width, height, and thickness - // independently and then places boxes from those rounded lengths. - // Sizes stay stable under translation, but neighboring boxes derive - // their shared boundary from different sources, so closure is not - // guaranteed. - // - // We apply absolute edge rounding for each element's outer box in - // post-layout rounding to preserve closure. Border and padding widths - // are not touched by post-layout rounding; they keep their pre-layout - // rounded value so that they remain stable under translation. - // - // This gives both closure and translation stability in the case that - // all local metrics are integer device-pixel lengths. Pre-layout - // rounding covers that in most cases. The exception is metrics - // resolved by layout relationships, such as percentages. Outer box - // edges will still close globally, and painted border widths are still - // snapped independently, but the raw content-box origin can carry a - // 1dp residual into descendants. - - pub fn layout_bounds(&mut self, id: LayoutId, scale_factor: f32) -> Bounds { - if let Some(layout) = self.absolute_layout_bounds.get(&id).cloned() { - return layout; - } - - let layout = self.taffy.layout(id.into()).expect(EXPECT_MESSAGE); - let layout_location = layout.location; - let layout_size = layout.size; - let parent = self.taffy.parent(id.0); - - let absolute_outer_origin = match parent { - Some(parent_id) => { - let parent_id = LayoutId::from(parent_id); - self.layout_bounds(parent_id, scale_factor); - let parent_origin = *self - .absolute_outer_origins - .get(&parent_id) - .expect("parent absolute outer origin should be cached"); - parent_origin + Point::from(layout_location) - } - None => Point::from(layout_location), - }; - self.absolute_outer_origins - .insert(id, absolute_outer_origin); - - let absolute_far = absolute_outer_origin + Point::from(Size::from(layout_size)); - let snapped_bounds = Bounds::from_corners( - absolute_outer_origin.map(round_half_toward_zero), - absolute_far.map(round_half_toward_zero), - ); - - let bounds = (snapped_bounds / scale_factor).map(Pixels); - self.absolute_layout_bounds.insert(id, bounds); - bounds - } -} - -/// A unique identifier for a layout node, generated when requesting a layout from Taffy -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -#[repr(transparent)] -pub struct LayoutId(NodeId); - -impl LayoutId { - fn to_taffy_slice(node_ids: &[Self]) -> &[taffy::NodeId] { - // SAFETY: LayoutId is repr(transparent) to taffy::tree::NodeId. - unsafe { std::mem::transmute::<&[LayoutId], &[taffy::NodeId]>(node_ids) } - } -} - -impl std::hash::Hash for LayoutId { - fn hash(&self, state: &mut H) { - u64::from(self.0).hash(state); - } -} - -impl From for LayoutId { - fn from(node_id: NodeId) -> Self { - Self(node_id) - } -} - -impl From for NodeId { - fn from(layout_id: LayoutId) -> NodeId { - layout_id.0 - } -} - -fn snap_measured_size_to_device_pixels(size: Size, scale_factor: f32) -> Size { - size.map(|d| ceil_to_device_pixel(d.0.max(0.0), scale_factor)) -} - -fn border_widths_to_taffy( - widths: &Edges, - rem_size: Pixels, - scale_factor: f32, -) -> TaffyRect { - let snap = |w: &AbsoluteLength| { - taffy::style::LengthPercentage::length(round_stroke_to_device_pixel( - w.to_pixels(rem_size).0, - scale_factor, - )) - }; - TaffyRect { - top: snap(&widths.top), - right: snap(&widths.right), - bottom: snap(&widths.bottom), - left: snap(&widths.left), - } -} - -trait ToTaffy { - fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> Output; -} - -impl ToTaffy for Style { - fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::Style { - use taffy::style_helpers::{fr, length, minmax, repeat}; - - fn to_grid_line( - placement: &Range, - ) -> taffy::Line { - taffy::Line { - start: placement.start.into(), - end: placement.end.into(), - } - } - - fn to_grid_repeat( - unit: &Option, - ) -> Vec> { - unit.map(|template| { - match template.min_size { - // grid-template-*: repeat(, minmax(0, 1fr)); - crate::GridTemplateMinSize::Zero => { - vec![repeat( - template.repeat, - vec![minmax(length(0.0_f32), fr(1.0_f32))], - )] - } - // grid-template-*: repeat(, minmax(min-content, 1fr)); - crate::GridTemplateMinSize::MinContent => { - vec![repeat( - template.repeat, - vec![minmax(min_content(), fr(1.0_f32))], - )] - } - // grid-template-*: repeat(, minmax(0, max-content)) - crate::GridTemplateMinSize::MaxContent => { - vec![repeat( - template.repeat, - vec![minmax(length(0.0_f32), max_content())], - )] - } - } - }) - .unwrap_or_default() - } - - taffy::style::Style { - display: self.display.into(), - overflow: self.overflow.into(), - scrollbar_width: self.scrollbar_width.to_taffy(rem_size, scale_factor), - position: self.position.into(), - inset: self.inset.to_taffy(rem_size, scale_factor), - size: self.size.to_taffy(rem_size, scale_factor), - min_size: self.min_size.to_taffy(rem_size, scale_factor), - max_size: self.max_size.to_taffy(rem_size, scale_factor), - aspect_ratio: self.aspect_ratio, - margin: self.margin.to_taffy(rem_size, scale_factor), - padding: self.padding.to_taffy(rem_size, scale_factor), - border: border_widths_to_taffy(&self.border_widths, rem_size, scale_factor), - align_items: self.align_items.map(|x| x.into()), - align_self: self.align_self.map(|x| x.into()), - align_content: self.align_content.map(|x| x.into()), - justify_content: self.justify_content.map(|x| x.into()), - gap: self.gap.to_taffy(rem_size, scale_factor), - flex_direction: self.flex_direction.into(), - flex_wrap: self.flex_wrap.into(), - flex_basis: self.flex_basis.to_taffy(rem_size, scale_factor), - flex_grow: self.flex_grow, - flex_shrink: self.flex_shrink, - grid_template_rows: to_grid_repeat(&self.grid_rows), - grid_template_columns: to_grid_repeat(&self.grid_cols), - grid_row: self - .grid_location - .as_ref() - .map(|location| to_grid_line(&location.row)) - .unwrap_or_default(), - grid_column: self - .grid_location - .as_ref() - .map(|location| to_grid_line(&location.column)) - .unwrap_or_default(), - ..Default::default() - } - } -} - -impl ToTaffy for AbsoluteLength { - fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> f32 { - round_to_device_pixel(self.to_pixels(rem_size).0, scale_factor) - } -} - -impl ToTaffy for Length { - fn to_taffy( - &self, - rem_size: Pixels, - scale_factor: f32, - ) -> taffy::prelude::LengthPercentageAuto { - match self { - Length::Definite(length) => length.to_taffy(rem_size, scale_factor), - Length::Auto => taffy::prelude::LengthPercentageAuto::auto(), - } - } -} - -impl ToTaffy for Length { - fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::prelude::Dimension { - match self { - Length::Definite(length) => length.to_taffy(rem_size, scale_factor), - Length::Auto => taffy::prelude::Dimension::auto(), - } - } -} - -impl ToTaffy for DefiniteLength { - fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::LengthPercentage { - match self { - DefiniteLength::Absolute(length) => length.to_taffy(rem_size, scale_factor), - DefiniteLength::Fraction(fraction) => { - taffy::style::LengthPercentage::percent(*fraction) - } - } - } -} - -impl ToTaffy for DefiniteLength { - fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::LengthPercentageAuto { - match self { - DefiniteLength::Absolute(length) => length.to_taffy(rem_size, scale_factor), - DefiniteLength::Fraction(fraction) => { - taffy::style::LengthPercentageAuto::percent(*fraction) - } - } - } -} - -impl ToTaffy for DefiniteLength { - fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::Dimension { - match self { - DefiniteLength::Absolute(length) => length.to_taffy(rem_size, scale_factor), - DefiniteLength::Fraction(fraction) => taffy::style::Dimension::percent(*fraction), - } - } -} - -impl ToTaffy for AbsoluteLength { - fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::LengthPercentage { - taffy::style::LengthPercentage::length(self.to_taffy(rem_size, scale_factor)) - } -} - -impl ToTaffy for AbsoluteLength { - fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::LengthPercentageAuto { - taffy::style::LengthPercentageAuto::length(self.to_taffy(rem_size, scale_factor)) - } -} - -impl ToTaffy for AbsoluteLength { - fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::Dimension { - taffy::style::Dimension::length(self.to_taffy(rem_size, scale_factor)) - } -} - -impl From> for Point -where - T: Into, - T2: Clone + Debug + Default + PartialEq, -{ - fn from(point: TaffyPoint) -> Point { - Point { - x: point.x.into(), - y: point.y.into(), - } - } -} - -impl From> for TaffyPoint -where - T: Into + Clone + Debug + Default + PartialEq, -{ - fn from(val: Point) -> Self { - TaffyPoint { - x: val.x.into(), - y: val.y.into(), - } - } -} - -impl ToTaffy> for Size -where - T: ToTaffy + Clone + Debug + Default + PartialEq, -{ - fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> TaffySize { - TaffySize { - width: self.width.to_taffy(rem_size, scale_factor), - height: self.height.to_taffy(rem_size, scale_factor), - } - } -} - -impl ToTaffy> for Edges -where - T: ToTaffy + Clone + Debug + Default + PartialEq, -{ - fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> TaffyRect { - TaffyRect { - top: self.top.to_taffy(rem_size, scale_factor), - right: self.right.to_taffy(rem_size, scale_factor), - bottom: self.bottom.to_taffy(rem_size, scale_factor), - left: self.left.to_taffy(rem_size, scale_factor), - } - } -} - -impl From> for Size -where - T: Into, - U: Clone + Debug + Default + PartialEq, -{ - fn from(taffy_size: TaffySize) -> Self { - Size { - width: taffy_size.width.into(), - height: taffy_size.height.into(), - } - } -} - -impl From> for TaffySize -where - T: Into + Clone + Debug + Default + PartialEq, -{ - fn from(size: Size) -> Self { - TaffySize { - width: size.width.into(), - height: size.height.into(), - } - } -} - -/// The space available for an element to be laid out in -#[derive(Copy, Clone, Default, Debug, Eq, PartialEq)] -pub enum AvailableSpace { - /// The amount of space available is the specified number of pixels - Definite(Pixels), - /// The amount of space available is indefinite and the node should be laid out under a min-content constraint - #[default] - MinContent, - /// The amount of space available is indefinite and the node should be laid out under a max-content constraint - MaxContent, -} - -impl AvailableSpace { - /// Returns a `Size` with both width and height set to `AvailableSpace::MinContent`. - /// - /// This function is useful when you want to create a `Size` with the minimum content constraints - /// for both dimensions. - /// - /// # Examples - /// - /// ``` - /// use gpui::AvailableSpace; - /// let min_content_size = AvailableSpace::min_size(); - /// assert_eq!(min_content_size.width, AvailableSpace::MinContent); - /// assert_eq!(min_content_size.height, AvailableSpace::MinContent); - /// ``` - pub const fn min_size() -> Size { - Size { - width: Self::MinContent, - height: Self::MinContent, - } - } -} - -impl From for TaffyAvailableSpace { - fn from(space: AvailableSpace) -> TaffyAvailableSpace { - match space { - AvailableSpace::Definite(Pixels(value)) => TaffyAvailableSpace::Definite(value), - AvailableSpace::MinContent => TaffyAvailableSpace::MinContent, - AvailableSpace::MaxContent => TaffyAvailableSpace::MaxContent, - } - } -} - -impl From for AvailableSpace { - fn from(space: TaffyAvailableSpace) -> AvailableSpace { - match space { - TaffyAvailableSpace::Definite(value) => AvailableSpace::Definite(Pixels(value)), - TaffyAvailableSpace::MinContent => AvailableSpace::MinContent, - TaffyAvailableSpace::MaxContent => AvailableSpace::MaxContent, - } - } -} - -impl From for AvailableSpace { - fn from(pixels: Pixels) -> Self { - AvailableSpace::Definite(pixels) - } -} - -impl From> for Size { - fn from(size: Size) -> Self { - Size { - width: AvailableSpace::Definite(size.width), - height: AvailableSpace::Definite(size.height), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn border_widths_to_taffy_use_stroke_snapping() { - let border_widths = Edges { - top: Pixels(0.0).into(), - right: Pixels(0.4).into(), - bottom: Pixels(0.5).into(), - left: Pixels(1.6).into(), - }; - let taffy_border = border_widths_to_taffy(&border_widths, Pixels(16.0), 1.0); - - assert_eq!( - taffy_border.top, - taffy::style::LengthPercentage::length(0.0) - ); - assert_eq!( - taffy_border.right, - taffy::style::LengthPercentage::length(1.0) - ); - assert_eq!( - taffy_border.bottom, - taffy::style::LengthPercentage::length(1.0) - ); - assert_eq!( - taffy_border.left, - taffy::style::LengthPercentage::length(2.0) - ); - } -} diff --git a/crates/gpui_pre/src/test.rs b/crates/gpui_pre/src/test.rs deleted file mode 100644 index a93424e..0000000 --- a/crates/gpui_pre/src/test.rs +++ /dev/null @@ -1,220 +0,0 @@ -//! Test support for GPUI. -//! -//! GPUI provides first-class support for testing, which includes a macro to run test that rely on having a context, -//! and a test implementation of the `ForegroundExecutor` and `BackgroundExecutor` which ensure that your tests run -//! deterministically even in the face of arbitrary parallelism. -//! -//! The output of the `gpui::test` macro is understood by other rust test runners, so you can use it with `cargo test` -//! or `cargo-nextest`, or another runner of your choice. -//! -//! To make it possible to test collaborative user interfaces (like Zed) you can ask for as many different contexts -//! as you need. -//! -//! ## Example -//! -//! ``` -//! use gpui; -//! -//! #[gpui::test] -//! async fn test_example(cx: &TestAppContext) { -//! assert!(true) -//! } -//! -//! #[gpui::test] -//! async fn test_collaboration_example(cx_a: &TestAppContext, cx_b: &TestAppContext) { -//! assert!(true) -//! } -//! ``` -use crate::{Entity, Subscription, TestAppContext, TestDispatcher}; -use futures::StreamExt as _; -use proptest::prelude::{Just, Strategy, any}; -use std::{ - env, - panic::{self, RefUnwindSafe, UnwindSafe}, - pin::Pin, -}; - -/// Strategy injected into `#[gpui::property_test]` tests to control the seed -/// given to the scheduler. Doesn't shrink, since all scheduler seeds are -/// equivalent in complexity. If `$SEED` is set, it always uses that value. -/// -/// Note: this function is not intended to be used directly. Rather, it is -/// public so that it can be used from the `property_test` macro. -pub fn seed_strategy() -> impl Strategy { - match std::env::var("SEED") { - Ok(val) => Just(val.parse().unwrap()).boxed(), - Err(_) => any::().no_shrink().boxed(), - } -} - -/// Applies a fixed RNG seed to a proptest config so that case generation -/// is deterministic. Uses `$SEED` if set, otherwise defaults to `0`. -/// This bridges the GPUI `SEED` env var to proptest's RNG seed, so that -/// a single variable controls both the scheduler seed and case generation. -/// -/// Note: this function is not intended to be used directly. Rather, it is -/// public so that it can be used from the `property_test` macro. -pub fn apply_seed_to_proptest_config( - mut config: proptest::test_runner::Config, -) -> proptest::test_runner::Config { - let seed = env::var("SEED") - .ok() - .and_then(|val| val.parse::().ok()) - .unwrap_or(0); - config.rng_seed = proptest::test_runner::RngSeed::Fixed(seed); - config -} - -/// Similar to [`run_test`], but only runs the callback once, allowing -/// [`FnOnce`] callbacks. This is intended for use with the -/// `gpui::property_test` macro and generally should not be used directly. -/// -/// Doesn't support many features of [`run_test`], since these are provided by -/// proptest. -pub fn run_test_once( - seed: u64, - test_fn: Box R>, -) -> R { - let result = panic::catch_unwind(|| { - let dispatcher = TestDispatcher::new(seed); - let scheduler = dispatcher.scheduler().clone(); - let res = test_fn(dispatcher); - scheduler.end_test(); - res - }); - - match result { - Ok(r) => r, - Err(e) => panic::resume_unwind(e), - } -} - -/// Run the given test function with the configured parameters. -/// This is intended for use with the `gpui::test` macro -/// and generally should not be used directly. -pub fn run_test( - num_iterations: usize, - explicit_seeds: &[u64], - max_retries: usize, - test_fn: &mut (dyn RefUnwindSafe + Fn(TestDispatcher, u64)), - on_fail_fn: Option, -) { - let (seeds, is_multiple_runs) = calculate_seeds(num_iterations as u64, explicit_seeds); - - for seed in seeds { - let mut attempt = 0; - loop { - if is_multiple_runs { - eprintln!("seed = {seed}"); - } - let result = panic::catch_unwind(|| { - let dispatcher = TestDispatcher::new(seed); - let scheduler = dispatcher.scheduler().clone(); - test_fn(dispatcher, seed); - scheduler.end_test(); - }); - - match result { - Ok(_) => break, - Err(error) => { - if attempt < max_retries { - println!("attempt {} failed, retrying", attempt); - attempt += 1; - // The panic payload might itself trigger an unwind on drop: - // https://doc.rust-lang.org/std/panic/fn.catch_unwind.html#notes - std::mem::forget(error); - } else { - if is_multiple_runs { - eprintln!("failing seed: {seed}"); - eprintln!( - "You can rerun from this seed by setting the environmental variable SEED to {seed}" - ); - } - if let Some(on_fail_fn) = on_fail_fn { - on_fail_fn() - } - panic::resume_unwind(error); - } - } - } - } - } -} - -fn calculate_seeds( - iterations: u64, - explicit_seeds: &[u64], -) -> (impl Iterator + '_, bool) { - let iterations = env::var("ITERATIONS") - .ok() - .map(|var| var.parse().expect("invalid ITERATIONS variable")) - .unwrap_or(iterations); - - let env_num = env::var("SEED") - .map(|seed| seed.parse().expect("invalid SEED variable as integer")) - .ok(); - - let empty_range = || 0..0; - - let iter = { - let env_range = if let Some(env_num) = env_num { - env_num..env_num + 1 - } else { - empty_range() - }; - - // if `iterations` is 1 and !(`explicit_seeds` is non-empty || `SEED` is set), then add the run `0` - // if `iterations` is 1 and (`explicit_seeds` is non-empty || `SEED` is set), then discard the run `0` - // if `iterations` isn't 1 and `SEED` is set, do `SEED..SEED+iterations` - // otherwise, do `0..iterations` - let iterations_range = match (iterations, env_num) { - (1, None) if explicit_seeds.is_empty() => 0..1, - (1, None) | (1, Some(_)) => empty_range(), - (iterations, Some(env)) => env..env + iterations, - (iterations, None) => 0..iterations, - }; - - // if `SEED` is set, ignore `explicit_seeds` - let explicit_seeds = if env_num.is_some() { - &[] - } else { - explicit_seeds - }; - - env_range - .chain(iterations_range) - .chain(explicit_seeds.iter().copied()) - }; - let is_multiple_runs = iter.clone().nth(1).is_some(); - (iter, is_multiple_runs) -} - -/// A test struct for converting an observation callback into a stream. -pub struct Observation { - rx: Pin>>, - _subscription: Subscription, -} - -impl futures::Stream for Observation { - type Item = T; - - fn poll_next( - mut self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - self.rx.poll_next_unpin(cx) - } -} - -/// observe returns a stream of the change events from the given `Entity` -pub fn observe(entity: &Entity, cx: &mut TestAppContext) -> Observation<()> { - let (tx, rx) = async_channel::unbounded(); - let _subscription = cx.update(|cx| { - cx.observe(entity, move |_, _| { - let _ = gpui::block_on(tx.send(())); - }) - }); - let rx = Box::pin(rx); - - Observation { rx, _subscription } -} diff --git a/crates/gpui_pre/src/text_system.rs b/crates/gpui_pre/src/text_system.rs deleted file mode 100644 index 3346389..0000000 --- a/crates/gpui_pre/src/text_system.rs +++ /dev/null @@ -1,1222 +0,0 @@ -mod font_fallbacks; -mod font_features; -mod line; -mod line_layout; -mod line_wrapper; - -pub use font_fallbacks::*; -pub use font_features::*; -pub use line::*; -pub use line_layout::*; -pub use line_wrapper::*; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -use crate::{ - Bounds, DevicePixels, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size, - StrikethroughStyle, TextRenderingMode, UnderlineStyle, px, -}; -use anyhow::{Context as _, anyhow}; -use collections::FxHashMap; -use core::fmt; -use derive_more::{Add, Deref, FromStr, Sub}; -use itertools::Itertools; -use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; -use smallvec::{SmallVec, smallvec}; -use std::{ - borrow::Cow, - cmp, - fmt::{Debug, Display, Formatter}, - hash::{Hash, Hasher}, - ops::{Deref, DerefMut, Range}, - sync::Arc, -}; - -/// An opaque identifier for a specific font. -#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)] -#[repr(C)] -pub struct FontId(pub usize); - -/// An opaque identifier for a specific font family. -#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)] -pub struct FontFamilyId(pub usize); - -/// Number of subpixel glyph variants along the X axis. -pub const SUBPIXEL_VARIANTS_X: u8 = 4; - -/// Number of subpixel glyph variants along the Y axis. -pub const SUBPIXEL_VARIANTS_Y: u8 = 1; - -/// The GPUI text rendering sub system. -pub struct TextSystem { - platform_text_system: Arc, - font_ids_by_font: RwLock>>, - font_metrics: RwLock>, - raster_bounds: RwLock>>, - wrapper_pool: Mutex>>, - font_runs_pool: Mutex>>, - fallback_font_stack: SmallVec<[Font; 2]>, -} - -impl TextSystem { - /// Create a new TextSystem with the given platform text system. - pub fn new(platform_text_system: Arc) -> Self { - TextSystem { - platform_text_system, - font_metrics: RwLock::default(), - raster_bounds: RwLock::default(), - font_ids_by_font: RwLock::default(), - wrapper_pool: Mutex::default(), - font_runs_pool: Mutex::default(), - fallback_font_stack: smallvec![ - // TODO: Remove this when Linux have implemented setting fallbacks. - font(".ZedMono"), - font(".ZedSans"), - font("Helvetica"), - font("Segoe UI"), // Windows - font("Ubuntu"), // Gnome (Ubuntu) - font("Adwaita Sans"), // Gnome 47 - font("Cantarell"), // Gnome - font("Noto Sans"), // KDE - font("DejaVu Sans"), - font("Arial"), // macOS, Windows - ], - } - } - - /// Get a list of all available font names from the operating system. - pub fn all_font_names(&self) -> Vec { - let mut names = self.platform_text_system.all_font_names(); - names.extend( - self.fallback_font_stack - .iter() - .map(|font| font.family.to_string()), - ); - names.push(".SystemUIFont".to_string()); - names.sort_unstable(); - names.dedup(); - names - } - - /// Add a font's data to the text system. - pub fn add_fonts(&self, fonts: Vec>) -> Result<()> { - self.platform_text_system.add_fonts(fonts) - } - - /// Get the FontId for the configure font family and style. - fn font_id(&self, font: &Font) -> Result { - fn clone_font_id_result(font_id: &Result) -> Result { - match font_id { - Ok(font_id) => Ok(*font_id), - Err(err) => Err(anyhow!("{err}")), - } - } - - let font_id = self - .font_ids_by_font - .read() - .get(font) - .map(clone_font_id_result); - if let Some(font_id) = font_id { - font_id - } else { - let font_id = self.platform_text_system.font_id(font); - self.font_ids_by_font - .write() - .insert(font.clone(), clone_font_id_result(&font_id)); - font_id - } - } - - /// Get the Font for the Font Id. - pub fn get_font_for_id(&self, id: FontId) -> Option { - let lock = self.font_ids_by_font.read(); - lock.iter() - .filter_map(|(font, result)| match result { - Ok(font_id) if *font_id == id => Some(font.clone()), - _ => None, - }) - .next() - } - - /// Resolves the specified font, falling back to the default font stack if - /// the font fails to load. - /// - /// # Panics - /// - /// Panics if the font and none of the fallbacks can be resolved. - pub fn resolve_font(&self, font: &Font) -> FontId { - if let Ok(font_id) = self.font_id(font) { - return font_id; - } - for fallback in &self.fallback_font_stack { - if let Ok(font_id) = self.font_id(fallback) { - return font_id; - } - } - - panic!( - "failed to resolve font '{}' or any of the fallbacks: {}", - font.family, - self.fallback_font_stack - .iter() - .map(|fallback| &fallback.family) - .join(", ") - ); - } - - /// Prewarm any system font caches needed to shape text. - /// - /// This may be expensive, so callers should generally invoke it on a - /// background executor. Missing entries are still populated on demand by - /// the normal shaping path. - pub fn prewarm_fonts(&self, fonts: &[Font]) { - let mut font_ids = SmallVec::<[FontId; 8]>::new(); - for font in fonts { - let font_id = self.resolve_font(font); - if !font_ids.contains(&font_id) { - font_ids.push(font_id); - } - } - self.platform_text_system.prewarm_fonts(&font_ids); - } - - /// Get the bounding box for the given font and font size. - /// A font's bounding box is the smallest rectangle that could enclose all glyphs - /// in the font. superimposed over one another. - pub fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Bounds { - self.read_metrics(font_id, |metrics| metrics.bounding_box(font_size)) - } - - /// Get the typographic bounds for the given character, in the given font and size. - pub fn typographic_bounds( - &self, - font_id: FontId, - font_size: Pixels, - character: char, - ) -> Result> { - let glyph_id = self - .platform_text_system - .glyph_for_char(font_id, character) - .with_context(|| format!("glyph not found for character '{character}'"))?; - let bounds = self - .platform_text_system - .typographic_bounds(font_id, glyph_id)?; - Ok(self.read_metrics(font_id, |metrics| { - (bounds / metrics.units_per_em as f32 * font_size.0).map(px) - })) - } - - /// Get the advance width for the given character, in the given font and size. - pub fn advance(&self, font_id: FontId, font_size: Pixels, ch: char) -> Result> { - let glyph_id = self - .platform_text_system - .glyph_for_char(font_id, ch) - .with_context(|| format!("glyph not found for character '{ch}'"))?; - let result = self.platform_text_system.advance(font_id, glyph_id)? - / self.units_per_em(font_id) as f32; - - Ok(result * font_size) - } - - // Consider removing this? - /// Returns the shaped layout width of for the given character, in the given font and size. - pub fn layout_width(&self, font_id: FontId, font_size: Pixels, ch: char) -> Pixels { - let mut buffer = [0; 4]; - let buffer = ch.encode_utf8(&mut buffer); - self.platform_text_system - .layout_line( - buffer, - font_size, - &[FontRun { - len: buffer.len(), - font_id, - }], - ) - .width - } - - /// Returns the width of an `em`. - /// - /// Uses the width of the `m` character in the given font and size. - pub fn em_width(&self, font_id: FontId, font_size: Pixels) -> Result { - Ok(self.typographic_bounds(font_id, font_size, 'm')?.size.width) - } - - /// Returns the advance width of an `em`. - /// - /// Uses the advance width of the `m` character in the given font and size. - pub fn em_advance(&self, font_id: FontId, font_size: Pixels) -> Result { - Ok(self.advance(font_id, font_size, 'm')?.width) - } - - /// Returns the width of an `ch`. - /// - /// Uses the width of the `0` character in the given font and size. - pub fn ch_width(&self, font_id: FontId, font_size: Pixels) -> Result { - Ok(self.typographic_bounds(font_id, font_size, '0')?.size.width) - } - - /// Returns the advance width of an `ch`. - /// - /// Uses the advance width of the `0` character in the given font and size. - pub fn ch_advance(&self, font_id: FontId, font_size: Pixels) -> Result { - Ok(self.advance(font_id, font_size, '0')?.width) - } - - /// Get the number of font size units per 'em square', - /// Per MDN: "an abstract square whose height is the intended distance between - /// lines of type in the same type size" - pub fn units_per_em(&self, font_id: FontId) -> u32 { - self.read_metrics(font_id, |metrics| metrics.units_per_em) - } - - /// Get the height of a capital letter in the given font and size. - pub fn cap_height(&self, font_id: FontId, font_size: Pixels) -> Pixels { - self.read_metrics(font_id, |metrics| metrics.cap_height(font_size)) - } - - /// Get the height of the x character in the given font and size. - pub fn x_height(&self, font_id: FontId, font_size: Pixels) -> Pixels { - self.read_metrics(font_id, |metrics| metrics.x_height(font_size)) - } - - /// Get the recommended distance from the baseline for the given font - pub fn ascent(&self, font_id: FontId, font_size: Pixels) -> Pixels { - self.read_metrics(font_id, |metrics| metrics.ascent(font_size)) - } - - /// Get the recommended distance below the baseline for the given font, - /// in single spaced text. - pub fn descent(&self, font_id: FontId, font_size: Pixels) -> Pixels { - self.read_metrics(font_id, |metrics| metrics.descent(font_size)) - } - - /// Get the recommended baseline offset for the given font and line height. - pub fn baseline_offset( - &self, - font_id: FontId, - font_size: Pixels, - line_height: Pixels, - ) -> Pixels { - let ascent = self.ascent(font_id, font_size); - let descent = self.descent(font_id, font_size); - let padding_top = (line_height - ascent - descent) / 2.; - padding_top + ascent - } - - fn read_metrics(&self, font_id: FontId, read: impl FnOnce(&FontMetrics) -> T) -> T { - let lock = self.font_metrics.upgradable_read(); - - if let Some(metrics) = lock.get(&font_id) { - read(metrics) - } else { - let mut lock = RwLockUpgradableReadGuard::upgrade(lock); - let metrics = lock - .entry(font_id) - .or_insert_with(|| self.platform_text_system.font_metrics(font_id)); - read(metrics) - } - } - - /// Returns a handle to a line wrapper, for the given font and font size. - pub fn line_wrapper(self: &Arc, font: Font, font_size: Pixels) -> LineWrapperHandle { - let lock = &mut self.wrapper_pool.lock(); - let font_id = self.resolve_font(&font); - let wrappers = lock - .entry(FontIdWithSize { font_id, font_size }) - .or_default(); - let wrapper = wrappers - .pop() - .unwrap_or_else(|| LineWrapper::new(font_id, font_size, self.clone())); - - LineWrapperHandle { - wrapper: Some(wrapper), - text_system: self.clone(), - } - } - - /// Get the rasterized size and location of a specific, rendered glyph. - pub(crate) fn raster_bounds(&self, params: &RenderGlyphParams) -> Result> { - let raster_bounds = self.raster_bounds.upgradable_read(); - if let Some(bounds) = raster_bounds.get(params) { - Ok(*bounds) - } else { - let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds); - let bounds = self.platform_text_system.glyph_raster_bounds(params)?; - raster_bounds.insert(params.clone(), bounds); - Ok(bounds) - } - } - - pub(crate) fn rasterize_glyph( - &self, - params: &RenderGlyphParams, - ) -> Result<(Size, Vec)> { - let raster_bounds = self.raster_bounds(params)?; - self.platform_text_system - .rasterize_glyph(params, raster_bounds) - } - - /// Returns the dilation level to use for a glyph painted in the given color. - pub(crate) fn glyph_dilation_for_color(&self, color: Hsla) -> u8 { - self.platform_text_system.glyph_dilation_for_color(color) - } - - /// Returns the text rendering mode recommended by the platform for the given font and size. - /// The return value will never be [`TextRenderingMode::PlatformDefault`]. - pub(crate) fn recommended_rendering_mode( - &self, - font_id: FontId, - font_size: Pixels, - ) -> TextRenderingMode { - self.platform_text_system - .recommended_rendering_mode(font_id, font_size) - } -} - -/// The GPUI text layout subsystem. -#[derive(Deref)] -pub struct WindowTextSystem { - line_layout_cache: LineLayoutCache, - #[deref] - text_system: Arc, -} - -impl WindowTextSystem { - /// Create a new WindowTextSystem with the given TextSystem. - pub fn new(text_system: Arc) -> Self { - Self { - line_layout_cache: LineLayoutCache::new(text_system.platform_text_system.clone()), - text_system, - } - } - - pub(crate) fn layout_index(&self) -> LineLayoutIndex { - self.line_layout_cache.layout_index() - } - - pub(crate) fn reuse_layouts(&self, index: Range) { - self.line_layout_cache.reuse_layouts(index) - } - - pub(crate) fn truncate_layouts(&self, index: LineLayoutIndex) { - self.line_layout_cache.truncate_layouts(index) - } - - /// Shape the given line, at the given font_size, for painting to the screen. - /// Subsets of the line can be styled independently with the `runs` parameter. - /// - /// Note that this method can only shape a single line of text. It will panic - /// if the text contains newlines. If you need to shape multiple lines of text, - /// use [`Self::shape_text`] instead. - pub fn shape_line( - &self, - text: SharedString, - font_size: Pixels, - runs: &[TextRun], - force_width: Option, - ) -> ShapedLine { - debug_assert!( - text.find('\n').is_none(), - "text argument should not contain newlines" - ); - - let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new(); - for run in runs { - if let Some(last_run) = decoration_runs.last_mut() - && last_run.color == run.color - && last_run.underline == run.underline - && last_run.strikethrough == run.strikethrough - && last_run.background_color == run.background_color - { - last_run.len += run.len as u32; - continue; - } - decoration_runs.push(DecorationRun { - len: run.len as u32, - color: run.color, - background_color: run.background_color, - underline: run.underline, - strikethrough: run.strikethrough, - }); - } - - let layout = self.layout_line(&text, font_size, runs, force_width); - - ShapedLine { - layout, - text, - decoration_runs, - } - } - - /// Shape the given line using a caller-provided content hash as the cache key. - /// - /// This enables cache hits without materializing a contiguous `SharedString` for the text. - /// If the cache misses, `materialize_text` is invoked to produce the `SharedString` for shaping. - /// - /// Contract (caller enforced): - /// - Same `text_hash` implies identical text content (collision risk accepted by caller). - /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions). - /// - /// Like [`Self::shape_line`], this must be used only for single-line text (no `\n`). - pub fn shape_line_by_hash( - &self, - text_hash: u64, - text_len: usize, - font_size: Pixels, - runs: &[TextRun], - force_width: Option, - materialize_text: impl FnOnce() -> SharedString, - ) -> ShapedLine { - let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new(); - for run in runs { - if let Some(last_run) = decoration_runs.last_mut() - && last_run.color == run.color - && last_run.underline == run.underline - && last_run.strikethrough == run.strikethrough - && last_run.background_color == run.background_color - { - last_run.len += run.len as u32; - continue; - } - decoration_runs.push(DecorationRun { - len: run.len as u32, - color: run.color, - background_color: run.background_color, - underline: run.underline, - strikethrough: run.strikethrough, - }); - } - - let mut used_force_width = force_width; - let layout = self.layout_line_by_hash( - text_hash, - text_len, - font_size, - runs, - used_force_width, - || { - let text = materialize_text(); - debug_assert!( - text.find('\n').is_none(), - "text argument should not contain newlines" - ); - text - }, - ); - - // We only materialize actual text on cache miss; on hit we avoid allocations. - // Since `ShapedLine` carries a `SharedString`, use an empty placeholder for hits. - // NOTE: Callers must not rely on `ShapedLine.text` for content when using this API. - let text: SharedString = SharedString::new_static(""); - - ShapedLine { - layout, - text, - decoration_runs, - } - } - - /// Shape a multi line string of text, at the given font_size, for painting to the screen. - /// Subsets of the text can be styled independently with the `runs` parameter. - /// If `wrap_width` is provided, the line breaks will be adjusted to fit within the given width. - pub fn shape_text( - &self, - text: SharedString, - font_size: Pixels, - runs: &[TextRun], - wrap_width: Option, - line_clamp: Option, - ) -> Result> { - let mut runs = runs.iter().filter(|run| run.len > 0).cloned().peekable(); - let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default(); - - let mut lines = SmallVec::new(); - let mut max_wrap_lines = line_clamp; - let mut wrapped_lines = 0; - - let mut process_line = |line_text: SharedString, line_start, line_end| { - font_runs.clear(); - - let mut decoration_runs = >::with_capacity(32); - let mut run_start = line_start; - while run_start < line_end { - let Some(run) = runs.peek_mut() else { - log::warn!("`TextRun`s do not cover the entire to be shaped text"); - break; - }; - - let run_len_within_line = cmp::min(line_end - run_start, run.len); - - let decoration_changed = if let Some(last_run) = decoration_runs.last_mut() - && last_run.color == run.color - && last_run.underline == run.underline - && last_run.strikethrough == run.strikethrough - && last_run.background_color == run.background_color - { - last_run.len += run_len_within_line as u32; - false - } else { - decoration_runs.push(DecorationRun { - len: run_len_within_line as u32, - color: run.color, - background_color: run.background_color, - underline: run.underline, - strikethrough: run.strikethrough, - }); - true - }; - - let font_id = self.resolve_font(&run.font); - if let Some(font_run) = font_runs.last_mut() - && font_id == font_run.font_id - && !decoration_changed - { - font_run.len += run_len_within_line; - } else { - font_runs.push(FontRun { - len: run_len_within_line, - font_id, - }); - } - - // Preserve the remainder of the run for the next line - run.len -= run_len_within_line; - if run.len == 0 { - runs.next(); - } - run_start += run_len_within_line; - } - - let layout = self.line_layout_cache.layout_wrapped_line( - &line_text, - font_size, - &font_runs, - wrap_width, - max_wrap_lines.map(|max| max.saturating_sub(wrapped_lines)), - ); - wrapped_lines += layout.wrap_boundaries.len(); - - lines.push(WrappedLine { - layout, - decoration_runs, - text: line_text, - }); - - // Skip `\n` character. - if let Some(run) = runs.peek_mut() { - run.len -= 1; - if run.len == 0 { - runs.next(); - } - } - }; - - let mut split_lines = text.split('\n'); - - // Special case single lines to prevent allocating a sharedstring - if let Some(first_line) = split_lines.next() - && let Some(second_line) = split_lines.next() - { - let mut line_start = 0; - process_line( - SharedString::new(first_line), - line_start, - line_start + first_line.len(), - ); - line_start += first_line.len() + '\n'.len_utf8(); - process_line( - SharedString::new(second_line), - line_start, - line_start + second_line.len(), - ); - for line_text in split_lines { - line_start += line_text.len() + '\n'.len_utf8(); - process_line( - SharedString::new(line_text), - line_start, - line_start + line_text.len(), - ); - } - } else { - let end = text.len(); - process_line(text, 0, end); - } - - self.font_runs_pool.lock().push(font_runs); - - Ok(lines) - } - - pub(crate) fn finish_frame(&self) { - self.line_layout_cache.finish_frame() - } - - /// Layout the given line of text, at the given font_size. - /// Subsets of the line can be styled independently with the `runs` parameter. - /// Generally, you should prefer to use [`Self::shape_line`] instead, which - /// can be painted directly. - pub fn layout_line( - &self, - text: &str, - font_size: Pixels, - runs: &[TextRun], - force_width: Option, - ) -> Arc { - let mut last_run = None::<&TextRun>; - let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default(); - font_runs.clear(); - - for run in runs.iter() { - let decoration_changed = if let Some(last_run) = last_run - && last_run.color == run.color - && last_run.underline == run.underline - && last_run.strikethrough == run.strikethrough - // we do not consider differing background color relevant, as it does not affect glyphs - // && last_run.background_color == run.background_color - { - false - } else { - last_run = Some(run); - true - }; - - let font_id = self.resolve_font(&run.font); - if let Some(font_run) = font_runs.last_mut() - && font_id == font_run.font_id - && !decoration_changed - { - font_run.len += run.len; - } else { - font_runs.push(FontRun { - len: run.len, - font_id, - }); - } - } - - let layout = self.line_layout_cache.layout_line( - &SharedString::new(text), - font_size, - &font_runs, - force_width, - ); - - self.font_runs_pool.lock().push(font_runs); - - layout - } - - /// Returns the shaped layout width of for the given character, in the given font and size. - pub fn layout_width(&self, font_id: FontId, font_size: Pixels, ch: char) -> Pixels { - let mut buffer = [0; 4]; - let buffer: &_ = ch.encode_utf8(&mut buffer); - self.line_layout_cache - .layout_line( - buffer, - font_size, - &[FontRun { - len: buffer.len(), - font_id, - }], - None, - ) - .width - } - - /// Returns the shaped layout width of an `em`. - pub fn em_layout_width(&self, font_id: FontId, font_size: Pixels) -> Pixels { - self.layout_width(font_id, font_size, 'm') - } - - /// Probe the line layout cache using a caller-provided content hash, without allocating. - /// - /// Returns `Some(layout)` if the layout is already cached in either the current frame - /// or the previous frame. Returns `None` if it is not cached. - /// - /// Contract (caller enforced): - /// - Same `text_hash` implies identical text content (collision risk accepted by caller). - /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions). - pub fn try_layout_line_by_hash( - &self, - text_hash: u64, - text_len: usize, - font_size: Pixels, - runs: &[TextRun], - force_width: Option, - ) -> Option> { - let mut last_run = None::<&TextRun>; - let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default(); - font_runs.clear(); - - for run in runs.iter() { - let decoration_changed = if let Some(last_run) = last_run - && last_run.color == run.color - && last_run.underline == run.underline - && last_run.strikethrough == run.strikethrough - // we do not consider differing background color relevant, as it does not affect glyphs - // && last_run.background_color == run.background_color - { - false - } else { - last_run = Some(run); - true - }; - - let font_id = self.resolve_font(&run.font); - if let Some(font_run) = font_runs.last_mut() - && font_id == font_run.font_id - && !decoration_changed - { - font_run.len += run.len; - } else { - font_runs.push(FontRun { - len: run.len, - font_id, - }); - } - } - - let layout = self.line_layout_cache.try_layout_line_by_hash( - text_hash, - text_len, - font_size, - &font_runs, - force_width, - ); - - self.font_runs_pool.lock().push(font_runs); - - layout - } - - /// Layout the given line of text using a caller-provided content hash as the cache key. - /// - /// This enables cache hits without materializing a contiguous `SharedString` for the text. - /// If the cache misses, `materialize_text` is invoked to produce the `SharedString` for shaping. - /// - /// Contract (caller enforced): - /// - Same `text_hash` implies identical text content (collision risk accepted by caller). - /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions). - pub fn layout_line_by_hash( - &self, - text_hash: u64, - text_len: usize, - font_size: Pixels, - runs: &[TextRun], - force_width: Option, - materialize_text: impl FnOnce() -> SharedString, - ) -> Arc { - let mut last_run = None::<&TextRun>; - let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default(); - font_runs.clear(); - - for run in runs.iter() { - let decoration_changed = if let Some(last_run) = last_run - && last_run.color == run.color - && last_run.underline == run.underline - && last_run.strikethrough == run.strikethrough - // we do not consider differing background color relevant, as it does not affect glyphs - // && last_run.background_color == run.background_color - { - false - } else { - last_run = Some(run); - true - }; - - let font_id = self.resolve_font(&run.font); - if let Some(font_run) = font_runs.last_mut() - && font_id == font_run.font_id - && !decoration_changed - { - font_run.len += run.len; - } else { - font_runs.push(FontRun { - len: run.len, - font_id, - }); - } - } - - let layout = self.line_layout_cache.layout_line_by_hash( - text_hash, - text_len, - font_size, - &font_runs, - force_width, - materialize_text, - ); - - self.font_runs_pool.lock().push(font_runs); - - layout - } -} - -#[derive(Hash, Eq, PartialEq)] -struct FontIdWithSize { - font_id: FontId, - font_size: Pixels, -} - -/// A handle into the text system, which can be used to compute the wrapped layout of text -pub struct LineWrapperHandle { - wrapper: Option, - text_system: Arc, -} - -impl Drop for LineWrapperHandle { - fn drop(&mut self) { - let mut state = self.text_system.wrapper_pool.lock(); - let wrapper = self.wrapper.take().unwrap(); - state - .get_mut(&FontIdWithSize { - font_id: wrapper.font_id, - font_size: wrapper.font_size, - }) - .unwrap() - .push(wrapper); - } -} - -impl Deref for LineWrapperHandle { - type Target = LineWrapper; - - fn deref(&self) -> &Self::Target { - self.wrapper.as_ref().unwrap() - } -} - -impl DerefMut for LineWrapperHandle { - fn deref_mut(&mut self) -> &mut Self::Target { - self.wrapper.as_mut().unwrap() - } -} - -/// The degree of blackness or stroke thickness of a font. This value ranges from 100.0 to 900.0, -/// with 400.0 as normal. -#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize, Deserialize, Add, Sub, FromStr)] -#[serde(transparent)] -pub struct FontWeight(pub f32); - -impl Display for FontWeight { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for FontWeight { - fn from(weight: f32) -> Self { - FontWeight(weight) - } -} - -impl Default for FontWeight { - #[inline] - fn default() -> FontWeight { - FontWeight::NORMAL - } -} - -impl Hash for FontWeight { - fn hash(&self, state: &mut H) { - state.write_u32(u32::from_be_bytes(self.0.to_be_bytes())); - } -} - -impl Eq for FontWeight {} - -impl FontWeight { - /// Thin weight (100), the thinnest value. - pub const THIN: FontWeight = FontWeight(100.0); - /// Extra light weight (200). - pub const EXTRA_LIGHT: FontWeight = FontWeight(200.0); - /// Light weight (300). - pub const LIGHT: FontWeight = FontWeight(300.0); - /// Normal (400). - pub const NORMAL: FontWeight = FontWeight(400.0); - /// Medium weight (500, higher than normal). - pub const MEDIUM: FontWeight = FontWeight(500.0); - /// Semibold weight (600). - pub const SEMIBOLD: FontWeight = FontWeight(600.0); - /// Bold weight (700). - pub const BOLD: FontWeight = FontWeight(700.0); - /// Extra-bold weight (800). - pub const EXTRA_BOLD: FontWeight = FontWeight(800.0); - /// Black weight (900), the thickest value. - pub const BLACK: FontWeight = FontWeight(900.0); - - /// All of the font weights, in order from thinnest to thickest. - pub const ALL: [FontWeight; 9] = [ - Self::THIN, - Self::EXTRA_LIGHT, - Self::LIGHT, - Self::NORMAL, - Self::MEDIUM, - Self::SEMIBOLD, - Self::BOLD, - Self::EXTRA_BOLD, - Self::BLACK, - ]; -} - -impl schemars::JsonSchema for FontWeight { - fn schema_name() -> std::borrow::Cow<'static, str> { - "FontWeight".into() - } - - fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { - use schemars::json_schema; - json_schema!({ - "type": "number", - "minimum": Self::THIN, - "maximum": Self::BLACK, - "default": Self::default(), - "description": "Font weight value between 100 (thin) and 900 (black)" - }) - } -} - -/// Allows italic or oblique faces to be selected. -#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize, JsonSchema)] -pub enum FontStyle { - /// A face that is neither italic not obliqued. - #[default] - Normal, - /// A form that is generally cursive in nature. - Italic, - /// A typically-sloped version of the regular face. - Oblique, -} - -impl Display for FontStyle { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - Debug::fmt(self, f) - } -} - -/// A styled run of text, for use in [`crate::TextLayout`]. -#[derive(Clone, Debug, PartialEq, Eq, Default)] -pub struct TextRun { - /// A number of utf8 bytes - pub len: usize, - /// The font to use for this run. - pub font: Font, - /// The color - pub color: Hsla, - /// The background color (if any) - pub background_color: Option, - /// The underline style (if any) - pub underline: Option, - /// The strikethrough style (if any) - pub strikethrough: Option, -} - -#[cfg(all(target_os = "macos", test))] -impl TextRun { - fn with_len(&self, len: usize) -> Self { - let mut this = self.clone(); - this.len = len; - this - } -} - -/// An identifier for a specific glyph, as returned by [`WindowTextSystem::layout_line`]. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] -#[repr(C)] -pub struct GlyphId(pub u32); - -/// Parameters for rendering a glyph, used as cache keys for raster bounds. -/// -/// This struct identifies a specific glyph rendering configuration including -/// font, size, subpixel positioning, and scale factor. It's used to look up -/// cached raster bounds and sprite atlas entries. -#[derive(Clone, Debug, PartialEq)] -#[expect(missing_docs)] -pub struct RenderGlyphParams { - pub font_id: FontId, - pub glyph_id: GlyphId, - pub font_size: Pixels, - pub subpixel_variant: Point, - pub scale_factor: f32, - pub is_emoji: bool, - pub subpixel_rendering: bool, - pub dilation: u8, -} - -impl Eq for RenderGlyphParams {} - -impl Hash for RenderGlyphParams { - fn hash(&self, state: &mut H) { - self.font_id.0.hash(state); - self.glyph_id.0.hash(state); - self.font_size.0.to_bits().hash(state); - self.subpixel_variant.hash(state); - self.scale_factor.to_bits().hash(state); - self.is_emoji.hash(state); - self.subpixel_rendering.hash(state); - self.dilation.hash(state); - } -} - -/// The configuration details for identifying a specific font. -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub struct Font { - /// The font family name. - /// - /// The special name ".SystemUIFont" is used to identify the system UI font, which varies based on platform. - pub family: SharedString, - - /// The font features to use. - pub features: FontFeatures, - - /// The fallbacks fonts to use. - pub fallbacks: Option, - - /// The font weight. - pub weight: FontWeight, - - /// The font style. - pub style: FontStyle, -} - -impl Default for Font { - fn default() -> Self { - font(".SystemUIFont") - } -} - -/// Get a [`Font`] for a given name. -pub fn font(family: impl Into) -> Font { - Font { - family: family.into(), - features: FontFeatures::default(), - weight: FontWeight::default(), - style: FontStyle::default(), - fallbacks: None, - } -} - -impl Font { - /// Set this Font to be bold - pub fn bold(mut self) -> Self { - self.weight = FontWeight::BOLD; - self - } - - /// Set this Font to be italic - pub fn italic(mut self) -> Self { - self.style = FontStyle::Italic; - self - } -} - -/// A struct for storing font metrics. -/// It is used to define the measurements of a typeface. -#[derive(Clone, Copy, Debug)] -pub struct FontMetrics { - /// The number of font units that make up the "em square", - /// a scalable grid for determining the size of a typeface. - pub units_per_em: u32, - - /// The vertical distance from the baseline of the font to the top of the glyph covers. - pub ascent: f32, - - /// The vertical distance from the baseline of the font to the bottom of the glyph covers. - pub descent: f32, - - /// The recommended additional space to add between lines of type. - pub line_gap: f32, - - /// The suggested position of the underline. - pub underline_position: f32, - - /// The suggested thickness of the underline. - pub underline_thickness: f32, - - /// The height of a capital letter measured from the baseline of the font. - pub cap_height: f32, - - /// The height of a lowercase x. - pub x_height: f32, - - /// The outer limits of the area that the font covers. - /// Corresponds to the xMin / xMax / yMin / yMax values in the OpenType `head` table - pub bounding_box: Bounds, -} - -impl FontMetrics { - /// Returns the vertical distance from the baseline of the font to the top of the glyph covers in pixels. - pub fn ascent(&self, font_size: Pixels) -> Pixels { - Pixels((self.ascent / self.units_per_em as f32) * font_size.0) - } - - /// Returns the vertical distance from the baseline of the font to the bottom of the glyph covers in pixels. - pub fn descent(&self, font_size: Pixels) -> Pixels { - Pixels((self.descent / self.units_per_em as f32) * font_size.0) - } - - /// Returns the recommended additional space to add between lines of type in pixels. - pub fn line_gap(&self, font_size: Pixels) -> Pixels { - Pixels((self.line_gap / self.units_per_em as f32) * font_size.0) - } - - /// Returns the suggested position of the underline in pixels. - pub fn underline_position(&self, font_size: Pixels) -> Pixels { - Pixels((self.underline_position / self.units_per_em as f32) * font_size.0) - } - - /// Returns the suggested thickness of the underline in pixels. - pub fn underline_thickness(&self, font_size: Pixels) -> Pixels { - Pixels((self.underline_thickness / self.units_per_em as f32) * font_size.0) - } - - /// Returns the height of a capital letter measured from the baseline of the font in pixels. - pub fn cap_height(&self, font_size: Pixels) -> Pixels { - Pixels((self.cap_height / self.units_per_em as f32) * font_size.0) - } - - /// Returns the height of a lowercase x in pixels. - pub fn x_height(&self, font_size: Pixels) -> Pixels { - Pixels((self.x_height / self.units_per_em as f32) * font_size.0) - } - - /// Returns the outer limits of the area that the font covers in pixels. - pub fn bounding_box(&self, font_size: Pixels) -> Bounds { - (self.bounding_box / self.units_per_em as f32 * font_size.0).map(px) - } -} - -/// Maps well-known virtual font names to their concrete equivalents. -#[allow(unused)] -pub fn font_name_with_fallbacks<'a>(name: &'a str, system: &'a str) -> &'a str { - // Note: the "Zed Plex" fonts were deprecated as we are not allowed to use "Plex" - // in a derived font name. They are essentially indistinguishable from IBM Plex/Lilex, - // and so retained here for backward compatibility. - match name { - ".SystemUIFont" => system, - ".ZedSans" | "Zed Plex Sans" => "IBM Plex Sans", - ".ZedMono" | "Zed Plex Mono" => "Lilex", - _ => name, - } -} - -/// Like [`font_name_with_fallbacks`] but accepts and returns [`SharedString`] references. -#[allow(unused)] -pub fn font_name_with_fallbacks_shared<'a>( - name: &'a SharedString, - system: &'a SharedString, -) -> &'a SharedString { - // Note: the "Zed Plex" fonts were deprecated as we are not allowed to use "Plex" - // in a derived font name. They are essentially indistinguishable from IBM Plex/Lilex, - // and so retained here for backward compatibility. - match name.as_str() { - ".SystemUIFont" => system, - ".ZedSans" | "Zed Plex Sans" => const { &SharedString::new_static("IBM Plex Sans") }, - ".ZedMono" | "Zed Plex Mono" => const { &SharedString::new_static("Lilex") }, - _ => name, - } -} diff --git a/crates/gpui_pre/src/text_system/font_fallbacks.rs b/crates/gpui_pre/src/text_system/font_fallbacks.rs deleted file mode 100644 index 63dc89b..0000000 --- a/crates/gpui_pre/src/text_system/font_fallbacks.rs +++ /dev/null @@ -1,21 +0,0 @@ -use std::sync::Arc; - -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -/// The fallback fonts that can be configured for a given font. -/// Fallback fonts family names are stored here. -#[derive(Default, Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize, JsonSchema)] -pub struct FontFallbacks(pub Arc>); - -impl FontFallbacks { - /// Get the fallback fonts family names - pub fn fallback_list(&self) -> &[String] { - self.0.as_slice() - } - - /// Create a font fallback from a list of strings - pub fn from_fonts(fonts: Vec) -> Self { - FontFallbacks(Arc::new(fonts)) - } -} diff --git a/crates/gpui_pre/src/text_system/font_features.rs b/crates/gpui_pre/src/text_system/font_features.rs deleted file mode 100644 index c1ab72b..0000000 --- a/crates/gpui_pre/src/text_system/font_features.rs +++ /dev/null @@ -1,154 +0,0 @@ -use std::borrow::Cow; -use std::sync::Arc; - -use schemars::{JsonSchema, json_schema}; - -/// The OpenType features that can be configured for a given font. -#[derive(Default, Clone, Eq, PartialEq, Hash)] -pub struct FontFeatures(pub Arc>); - -impl FontFeatures { - /// Disables `calt`. - pub fn disable_ligatures() -> Self { - Self(Arc::new(vec![("calt".into(), 0)])) - } - - /// Get the tag name list of the font OpenType features - /// only enabled or disabled features are returned - pub fn tag_value_list(&self) -> &[(String, u32)] { - self.0.as_slice() - } - - /// Returns whether the `calt` feature is enabled. - /// - /// Returns `None` if the feature is not present. - pub fn is_calt_enabled(&self) -> Option { - self.0 - .iter() - .find(|(feature, _)| feature == "calt") - .map(|(_, value)| *value == 1) - } -} - -impl std::fmt::Debug for FontFeatures { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut debug = f.debug_struct("FontFeatures"); - for (tag, value) in self.tag_value_list() { - debug.field(tag, value); - } - - debug.finish() - } -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -#[serde(untagged)] -enum FeatureValue { - Bool(bool), - Number(serde_json::Number), -} - -impl<'de> serde::Deserialize<'de> for FontFeatures { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - use serde::de::{MapAccess, Visitor}; - use std::fmt; - - struct FontFeaturesVisitor; - - impl<'de> Visitor<'de> for FontFeaturesVisitor { - type Value = FontFeatures; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a map of font features") - } - - fn visit_map(self, mut access: M) -> Result - where - M: MapAccess<'de>, - { - let mut feature_list = Vec::new(); - - while let Some((key, value)) = - access.next_entry::>()? - { - if !is_valid_feature_tag(&key) { - log::error!("Incorrect font feature tag: {}", key); - continue; - } - if let Some(value) = value { - match value { - FeatureValue::Bool(enable) => { - if enable { - feature_list.push((key, 1)); - } else { - feature_list.push((key, 0)); - } - } - FeatureValue::Number(value) => { - if value.is_u64() { - feature_list.push((key, value.as_u64().unwrap() as u32)); - } else { - log::error!( - "Incorrect font feature value {} for feature tag {}", - value, - key - ); - continue; - } - } - } - } - } - - Ok(FontFeatures(Arc::new(feature_list))) - } - } - - let features = deserializer.deserialize_map(FontFeaturesVisitor)?; - Ok(features) - } -} - -impl serde::Serialize for FontFeatures { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - use serde::ser::SerializeMap; - - let mut map = serializer.serialize_map(None)?; - - for (tag, value) in self.tag_value_list() { - map.serialize_entry(tag, value)?; - } - - map.end() - } -} - -impl JsonSchema for FontFeatures { - fn schema_name() -> Cow<'static, str> { - "FontFeatures".into() - } - - fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { - json_schema!({ - "type": "object", - "patternProperties": { - "[0-9a-zA-Z]{4}$": { - "type": ["boolean", "integer"], - "minimum": 0, - "multipleOf": 1 - } - }, - "additionalProperties": false - }) - } -} - -fn is_valid_feature_tag(tag: &str) -> bool { - tag.len() == 4 && tag.chars().all(|c| c.is_ascii_alphanumeric()) -} diff --git a/crates/gpui_pre/src/text_system/line.rs b/crates/gpui_pre/src/text_system/line.rs deleted file mode 100644 index 15101e6..0000000 --- a/crates/gpui_pre/src/text_system/line.rs +++ /dev/null @@ -1,1025 +0,0 @@ -use crate::{ - App, Bounds, DevicePixels, Half, Hsla, LineLayout, Pixels, Point, RenderGlyphParams, Result, - SharedString, StrikethroughStyle, TextAlign, UnderlineStyle, Window, WrapBoundary, - WrappedLineLayout, black, fill, point, px, size, -}; -use derive_more::{Deref, DerefMut}; -use smallvec::SmallVec; -use std::sync::Arc; - -/// Pre-computed glyph data for efficient painting without per-glyph cache lookups. -/// -/// This is produced by `ShapedLine::compute_glyph_raster_data` during prepaint -/// and consumed by `ShapedLine::paint_with_raster_data` during paint. -#[derive(Clone, Debug)] -pub struct GlyphRasterData { - /// The raster bounds for each glyph, in paint order. - pub bounds: Vec>, - /// The render params for each glyph (needed for sprite atlas lookup). - pub params: Vec, -} - -/// Set the text decoration for a run of text. -#[derive(Debug, Clone)] -pub struct DecorationRun { - /// The length of the run in utf-8 bytes. - pub len: u32, - - /// The color for this run - pub color: Hsla, - - /// The background color for this run - pub background_color: Option, - - /// The underline style for this run - pub underline: Option, - - /// The strikethrough style for this run - pub strikethrough: Option, -} - -/// A line of text that has been shaped and decorated. -#[derive(Clone, Default, Debug, Deref, DerefMut)] -pub struct ShapedLine { - #[deref] - #[deref_mut] - pub(crate) layout: Arc, - /// The text that was shaped for this line. - pub text: SharedString, - pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>, -} - -impl ShapedLine { - /// The length of the line in utf-8 bytes. - #[allow(clippy::len_without_is_empty)] - pub fn len(&self) -> usize { - self.layout.len - } - - /// The width of the shaped line in pixels. - /// - /// This is the glyph advance width computed by the text shaping system and is useful for - /// incrementally advancing a "pen" when painting multiple fragments on the same row. - pub fn width(&self) -> Pixels { - self.layout.width - } - - /// Override the len, useful if you're rendering text a - /// as text b (e.g. rendering invisibles). - pub fn with_len(mut self, len: usize) -> Self { - let layout = self.layout.as_ref(); - self.layout = Arc::new(LineLayout { - font_size: layout.font_size, - width: layout.width, - ascent: layout.ascent, - descent: layout.descent, - runs: layout.runs.clone(), - len, - }); - self - } - - /// Paint the line of text to the window. - pub fn paint( - &self, - origin: Point, - line_height: Pixels, - align: TextAlign, - align_width: Option, - window: &mut Window, - cx: &mut App, - ) -> Result<()> { - paint_line( - origin, - &self.layout, - line_height, - align, - align_width, - &self.decoration_runs, - &[], - window, - cx, - )?; - - Ok(()) - } - - /// Paint the background of the line to the window. - pub fn paint_background( - &self, - origin: Point, - line_height: Pixels, - align: TextAlign, - align_width: Option, - window: &mut Window, - cx: &mut App, - ) -> Result<()> { - paint_line_background( - origin, - &self.layout, - line_height, - align, - align_width, - &self.decoration_runs, - &[], - window, - cx, - )?; - - Ok(()) - } - - /// Split this shaped line at a byte index, returning `(prefix, suffix)`. - /// - /// - `prefix` contains glyphs for bytes `[0, byte_index)` with original positions. - /// Its width equals the x-advance up to the split point. - /// - `suffix` contains glyphs for bytes `[byte_index, len)` with positions - /// shifted left so the first glyph starts at x=0, and byte indices rebased to 0. - /// - Decoration runs are partitioned at the boundary; a run that straddles it is - /// split into two with adjusted lengths. - /// - `font_size`, `ascent`, and `descent` are copied to both halves. - pub fn split_at(&self, byte_index: usize) -> (ShapedLine, ShapedLine) { - let (left_layout, right_layout) = self.layout.split_at(byte_index); - - // Partition decoration runs. A run straddling the boundary is split into two. - let mut left_decorations = SmallVec::new(); - let mut right_decorations = SmallVec::new(); - let mut decoration_offset = 0u32; - let split_point = byte_index as u32; - - for decoration in &self.decoration_runs { - let run_end = decoration_offset + decoration.len; - - if run_end <= split_point { - left_decorations.push(decoration.clone()); - } else if decoration_offset >= split_point { - right_decorations.push(decoration.clone()); - } else { - let left_len = split_point - decoration_offset; - let right_len = run_end - split_point; - left_decorations.push(DecorationRun { - len: left_len, - color: decoration.color, - background_color: decoration.background_color, - underline: decoration.underline, - strikethrough: decoration.strikethrough, - }); - right_decorations.push(DecorationRun { - len: right_len, - color: decoration.color, - background_color: decoration.background_color, - underline: decoration.underline, - strikethrough: decoration.strikethrough, - }); - } - - decoration_offset = run_end; - } - - // Split text - let left_text = if byte_index == self.text.len() { - self.text.clone() - } else { - SharedString::new(&self.text[..byte_index]) - }; - let right_text = if byte_index == 0 { - self.text.clone() - } else { - SharedString::new(&self.text[byte_index..]) - }; - - let left = ShapedLine { - layout: Arc::new(left_layout), - text: left_text, - decoration_runs: left_decorations, - }; - - let right = ShapedLine { - layout: Arc::new(right_layout), - text: right_text, - decoration_runs: right_decorations, - }; - - (left, right) - } -} - -impl LineLayout { - /// Paint this layout to the window, using the given decoration runs to color - /// glyphs and draw underlines and strikethroughs. - /// - /// This is a lower-level alternative to [`ShapedLine::paint`] for callers that - /// hold a bare layout and track decorations themselves. - pub fn paint( - &self, - origin: Point, - line_height: Pixels, - align: TextAlign, - align_width: Option, - decoration_runs: &[DecorationRun], - window: &mut Window, - cx: &mut App, - ) -> Result<()> { - paint_line( - origin, - self, - line_height, - align, - align_width, - decoration_runs, - &[], - window, - cx, - ) - } - - /// Paint the background of this layout to the window, using the given - /// decoration runs to determine background colors. - /// - /// This is a lower-level alternative to [`ShapedLine::paint_background`] for - /// callers that hold a bare layout and track decorations themselves. - pub fn paint_background( - &self, - origin: Point, - line_height: Pixels, - align: TextAlign, - align_width: Option, - decoration_runs: &[DecorationRun], - window: &mut Window, - cx: &mut App, - ) -> Result<()> { - paint_line_background( - origin, - self, - line_height, - align, - align_width, - decoration_runs, - &[], - window, - cx, - ) - } -} - -/// A line of text that has been shaped, decorated, and wrapped by the text layout system. -#[derive(Default, Debug, Deref, DerefMut)] -pub struct WrappedLine { - #[deref] - #[deref_mut] - pub(crate) layout: Arc, - /// The text that was shaped for this line. - pub text: SharedString, - pub(crate) decoration_runs: Vec, -} - -impl WrappedLine { - /// The length of the underlying, unwrapped layout, in utf-8 bytes. - #[allow(clippy::len_without_is_empty)] - pub fn len(&self) -> usize { - self.layout.len() - } - - /// Paint this line of text to the window. - pub fn paint( - &self, - origin: Point, - line_height: Pixels, - align: TextAlign, - bounds: Option>, - window: &mut Window, - cx: &mut App, - ) -> Result<()> { - let align_width = match bounds { - Some(bounds) => Some(bounds.size.width), - None => self.layout.wrap_width, - }; - - paint_line( - origin, - &self.layout.unwrapped_layout, - line_height, - align, - align_width, - &self.decoration_runs, - &self.wrap_boundaries, - window, - cx, - )?; - - Ok(()) - } - - /// Paint the background of line of text to the window. - pub fn paint_background( - &self, - origin: Point, - line_height: Pixels, - align: TextAlign, - bounds: Option>, - window: &mut Window, - cx: &mut App, - ) -> Result<()> { - let align_width = match bounds { - Some(bounds) => Some(bounds.size.width), - None => self.layout.wrap_width, - }; - - paint_line_background( - origin, - &self.layout.unwrapped_layout, - line_height, - align, - align_width, - &self.decoration_runs, - &self.wrap_boundaries, - window, - cx, - )?; - - Ok(()) - } -} - -fn paint_line( - origin: Point, - layout: &LineLayout, - line_height: Pixels, - align: TextAlign, - align_width: Option, - decoration_runs: &[DecorationRun], - wrap_boundaries: &[WrapBoundary], - window: &mut Window, - cx: &mut App, -) -> Result<()> { - let line_bounds = Bounds::new( - origin, - size( - layout.width, - line_height * (wrap_boundaries.len() as f32 + 1.), - ), - ); - window.paint_layer(line_bounds, |window| { - let padding_top = (line_height - layout.ascent - layout.descent) / 2.; - let baseline_offset = point(px(0.), padding_top + layout.ascent); - let mut decoration_runs = decoration_runs.iter(); - let mut wraps = wrap_boundaries.iter().peekable(); - let mut run_end = 0; - let mut color = black(); - let mut current_underline: Option<(Point, UnderlineStyle)> = None; - let mut current_strikethrough: Option<(Point, StrikethroughStyle)> = None; - let text_system = cx.text_system().clone(); - let mut glyph_origin = point( - aligned_origin_x( - origin, - align_width.unwrap_or(layout.width), - px(0.0), - &align, - layout, - wraps.peek(), - ), - origin.y, - ); - let mut prev_glyph_position = Point::default(); - let mut max_glyph_size = size(px(0.), px(0.)); - let mut first_glyph_x = origin.x; - for (run_ix, run) in layout.runs.iter().enumerate() { - max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size; - - for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { - glyph_origin.x += glyph.position.x - prev_glyph_position.x; - if glyph_ix == 0 && run_ix == 0 { - first_glyph_x = glyph_origin.x; - } - - if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) { - wraps.next(); - if let Some((underline_origin, underline_style)) = current_underline.as_mut() { - if glyph_origin.x == underline_origin.x { - underline_origin.x -= max_glyph_size.width.half(); - }; - window.paint_underline( - *underline_origin, - glyph_origin.x - underline_origin.x, - underline_style, - ); - if glyph.index < run_end { - underline_origin.x = origin.x; - underline_origin.y += line_height; - } else { - current_underline = None; - } - } - if let Some((strikethrough_origin, strikethrough_style)) = - current_strikethrough.as_mut() - { - if glyph_origin.x == strikethrough_origin.x { - strikethrough_origin.x -= max_glyph_size.width.half(); - }; - window.paint_strikethrough( - *strikethrough_origin, - glyph_origin.x - strikethrough_origin.x, - strikethrough_style, - ); - if glyph.index < run_end { - strikethrough_origin.x = origin.x; - strikethrough_origin.y += line_height; - } else { - current_strikethrough = None; - } - } - - glyph_origin.x = aligned_origin_x( - origin, - align_width.unwrap_or(layout.width), - glyph.position.x, - &align, - layout, - wraps.peek(), - ); - glyph_origin.y += line_height; - } - prev_glyph_position = glyph.position; - - let mut finished_underline: Option<(Point, UnderlineStyle)> = None; - let mut finished_strikethrough: Option<(Point, StrikethroughStyle)> = None; - if glyph.index >= run_end { - let mut style_run = decoration_runs.next(); - - // ignore style runs that apply to a partial glyph - while let Some(run) = style_run { - if glyph.index < run_end + (run.len as usize) { - break; - } - run_end += run.len as usize; - style_run = decoration_runs.next(); - } - - if let Some(style_run) = style_run { - if let Some((_, underline_style)) = &mut current_underline - && style_run.underline.as_ref() != Some(underline_style) - { - finished_underline = current_underline.take(); - } - if let Some(run_underline) = style_run.underline.as_ref() { - current_underline.get_or_insert(( - point( - glyph_origin.x, - glyph_origin.y + baseline_offset.y + (layout.descent * 0.618), - ), - UnderlineStyle { - color: Some(run_underline.color.unwrap_or(style_run.color)), - thickness: run_underline.thickness, - wavy: run_underline.wavy, - }, - )); - } - if let Some((_, strikethrough_style)) = &mut current_strikethrough - && style_run.strikethrough.as_ref() != Some(strikethrough_style) - { - finished_strikethrough = current_strikethrough.take(); - } - if let Some(run_strikethrough) = style_run.strikethrough.as_ref() { - current_strikethrough.get_or_insert(( - point( - glyph_origin.x, - glyph_origin.y - + (((layout.ascent * 0.5) + baseline_offset.y) * 0.5), - ), - StrikethroughStyle { - color: Some(run_strikethrough.color.unwrap_or(style_run.color)), - thickness: run_strikethrough.thickness, - }, - )); - } - - run_end += style_run.len as usize; - color = style_run.color; - } else { - run_end = layout.len; - finished_underline = current_underline.take(); - finished_strikethrough = current_strikethrough.take(); - } - } - - if let Some((mut underline_origin, underline_style)) = finished_underline { - if underline_origin.x == glyph_origin.x { - underline_origin.x -= max_glyph_size.width.half(); - }; - window.paint_underline( - underline_origin, - glyph_origin.x - underline_origin.x, - &underline_style, - ); - } - - if let Some((mut strikethrough_origin, strikethrough_style)) = - finished_strikethrough - { - if strikethrough_origin.x == glyph_origin.x { - strikethrough_origin.x -= max_glyph_size.width.half(); - }; - window.paint_strikethrough( - strikethrough_origin, - glyph_origin.x - strikethrough_origin.x, - &strikethrough_style, - ); - } - - let max_glyph_bounds = Bounds { - origin: glyph_origin, - size: max_glyph_size, - }; - - let content_mask = window.content_mask(); - if max_glyph_bounds.intersects(&content_mask.bounds) { - let vertical_offset = point(px(0.0), glyph.position.y); - if glyph.is_emoji { - window.paint_emoji( - glyph_origin + baseline_offset + vertical_offset, - run.font_id, - glyph.id, - layout.font_size, - )?; - } else { - window.paint_glyph( - glyph_origin + baseline_offset + vertical_offset, - run.font_id, - glyph.id, - layout.font_size, - color, - )?; - } - } - } - } - - let mut last_line_end_x = first_glyph_x + layout.width; - if let Some(boundary) = wrap_boundaries.last() { - let run = &layout.runs[boundary.run_ix]; - let glyph = &run.glyphs[boundary.glyph_ix]; - last_line_end_x -= glyph.position.x; - } - - if let Some((mut underline_start, underline_style)) = current_underline.take() { - if last_line_end_x == underline_start.x { - underline_start.x -= max_glyph_size.width.half() - }; - window.paint_underline( - underline_start, - last_line_end_x - underline_start.x, - &underline_style, - ); - } - - if let Some((mut strikethrough_start, strikethrough_style)) = current_strikethrough.take() { - if last_line_end_x == strikethrough_start.x { - strikethrough_start.x -= max_glyph_size.width.half() - }; - window.paint_strikethrough( - strikethrough_start, - last_line_end_x - strikethrough_start.x, - &strikethrough_style, - ); - } - - Ok(()) - }) -} - -fn paint_line_background( - origin: Point, - layout: &LineLayout, - line_height: Pixels, - align: TextAlign, - align_width: Option, - decoration_runs: &[DecorationRun], - wrap_boundaries: &[WrapBoundary], - window: &mut Window, - cx: &mut App, -) -> Result<()> { - let line_bounds = Bounds::new( - origin, - size( - layout.width, - line_height * (wrap_boundaries.len() as f32 + 1.), - ), - ); - window.paint_layer(line_bounds, |window| { - let mut decoration_runs = decoration_runs.iter(); - let mut wraps = wrap_boundaries.iter().peekable(); - let mut run_end = 0; - let mut current_background: Option<(Point, Hsla)> = None; - let text_system = cx.text_system().clone(); - let mut glyph_origin = point( - aligned_origin_x( - origin, - align_width.unwrap_or(layout.width), - px(0.0), - &align, - layout, - wraps.peek(), - ), - origin.y, - ); - let mut prev_glyph_position = Point::default(); - let mut max_glyph_size = size(px(0.), px(0.)); - for (run_ix, run) in layout.runs.iter().enumerate() { - max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size; - - for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { - glyph_origin.x += glyph.position.x - prev_glyph_position.x; - - if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) { - wraps.next(); - if let Some((background_origin, background_color)) = current_background.as_mut() - { - if glyph_origin.x == background_origin.x { - background_origin.x -= max_glyph_size.width.half() - } - window.paint_quad(fill( - Bounds { - origin: *background_origin, - size: size(glyph_origin.x - background_origin.x, line_height), - }, - *background_color, - )); - if glyph.index < run_end { - background_origin.x = origin.x; - background_origin.y += line_height; - } else { - current_background = None; - } - } - - glyph_origin.x = aligned_origin_x( - origin, - align_width.unwrap_or(layout.width), - glyph.position.x, - &align, - layout, - wraps.peek(), - ); - glyph_origin.y += line_height; - } - prev_glyph_position = glyph.position; - - let mut finished_background: Option<(Point, Hsla)> = None; - if glyph.index >= run_end { - let mut style_run = decoration_runs.next(); - - // ignore style runs that apply to a partial glyph - while let Some(run) = style_run { - if glyph.index < run_end + (run.len as usize) { - break; - } - run_end += run.len as usize; - style_run = decoration_runs.next(); - } - - if let Some(style_run) = style_run { - if let Some((_, background_color)) = &mut current_background - && style_run.background_color.as_ref() != Some(background_color) - { - finished_background = current_background.take(); - } - if let Some(run_background) = style_run.background_color { - current_background.get_or_insert(( - point(glyph_origin.x, glyph_origin.y), - run_background, - )); - } - run_end += style_run.len as usize; - } else { - run_end = layout.len; - finished_background = current_background.take(); - } - } - - if let Some((mut background_origin, background_color)) = finished_background { - let mut width = glyph_origin.x - background_origin.x; - if background_origin.x == glyph_origin.x { - background_origin.x -= max_glyph_size.width.half(); - }; - window.paint_quad(fill( - Bounds { - origin: background_origin, - size: size(width, line_height), - }, - background_color, - )); - } - } - } - - let mut last_line_end_x = origin.x + layout.width; - if let Some(boundary) = wrap_boundaries.last() { - let run = &layout.runs[boundary.run_ix]; - let glyph = &run.glyphs[boundary.glyph_ix]; - last_line_end_x -= glyph.position.x; - } - - if let Some((mut background_origin, background_color)) = current_background.take() { - if last_line_end_x == background_origin.x { - background_origin.x -= max_glyph_size.width.half() - }; - window.paint_quad(fill( - Bounds { - origin: background_origin, - size: size(last_line_end_x - background_origin.x, line_height), - }, - background_color, - )); - } - - Ok(()) - }) -} - -fn aligned_origin_x( - origin: Point, - align_width: Pixels, - last_glyph_x: Pixels, - align: &TextAlign, - layout: &LineLayout, - wrap_boundary: Option<&&WrapBoundary>, -) -> Pixels { - let end_of_line = if let Some(WrapBoundary { run_ix, glyph_ix }) = wrap_boundary { - layout.runs[*run_ix].glyphs[*glyph_ix].position.x - } else { - layout.width - }; - - let line_width = end_of_line - last_glyph_x; - - match align { - TextAlign::Left => origin.x, - TextAlign::Center => (origin.x * 2.0 + align_width - line_width) / 2.0, - TextAlign::Right => origin.x + align_width - line_width, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{FontId, GlyphId, ShapedGlyph, ShapedRun}; - - /// Helper: build a ShapedLine from glyph descriptors without the platform text system. - /// Each glyph is described as (byte_index, x_position). - fn make_shaped_line( - text: &str, - glyphs: &[(usize, f32)], - width: f32, - decorations: &[DecorationRun], - ) -> ShapedLine { - let shaped_glyphs: Vec = glyphs - .iter() - .map(|&(index, x)| ShapedGlyph { - id: GlyphId(0), - position: point(px(x), px(0.0)), - index, - is_emoji: false, - }) - .collect(); - - ShapedLine { - layout: Arc::new(LineLayout { - font_size: px(16.0), - width: px(width), - ascent: px(12.0), - descent: px(4.0), - runs: vec![ShapedRun { - font_id: FontId(0), - glyphs: shaped_glyphs, - }], - len: text.len(), - }), - text: SharedString::new(text), - decoration_runs: SmallVec::from(decorations.to_vec()), - } - } - - #[test] - fn test_split_at_invariants() { - // Split "abcdef" at every possible byte index and verify structural invariants. - let line = make_shaped_line( - "abcdef", - &[ - (0, 0.0), - (1, 10.0), - (2, 20.0), - (3, 30.0), - (4, 40.0), - (5, 50.0), - ], - 60.0, - &[], - ); - - for i in 0..=6 { - let (left, right) = line.split_at(i); - - assert_eq!( - left.width() + right.width(), - line.width(), - "widths must sum at split={i}" - ); - assert_eq!( - left.len() + right.len(), - line.len(), - "lengths must sum at split={i}" - ); - assert_eq!( - format!("{}{}", left.text.as_ref(), right.text.as_ref()), - "abcdef", - "text must concatenate at split={i}" - ); - assert_eq!(left.font_size, line.font_size, "font_size at split={i}"); - assert_eq!(right.ascent, line.ascent, "ascent at split={i}"); - assert_eq!(right.descent, line.descent, "descent at split={i}"); - } - - // Edge: split at 0 produces no left runs, full content on right - let (left, right) = line.split_at(0); - assert_eq!(left.runs.len(), 0); - assert_eq!(right.runs[0].glyphs.len(), 6); - - // Edge: split at end produces full content on left, no right runs - let (left, right) = line.split_at(6); - assert_eq!(left.runs[0].glyphs.len(), 6); - assert_eq!(right.runs.len(), 0); - } - - #[test] - fn test_split_at_glyph_rebasing() { - // Two font runs (simulating a font fallback boundary at byte 3): - // run A (FontId 0): glyphs at bytes 0,1,2 positions 0,10,20 - // run B (FontId 1): glyphs at bytes 3,4,5 positions 30,40,50 - // Successive splits simulate the incremental splitting done during wrap. - let line = ShapedLine { - layout: Arc::new(LineLayout { - font_size: px(16.0), - width: px(60.0), - ascent: px(12.0), - descent: px(4.0), - runs: vec![ - ShapedRun { - font_id: FontId(0), - glyphs: vec![ - ShapedGlyph { - id: GlyphId(0), - position: point(px(0.0), px(0.0)), - index: 0, - is_emoji: false, - }, - ShapedGlyph { - id: GlyphId(0), - position: point(px(10.0), px(0.0)), - index: 1, - is_emoji: false, - }, - ShapedGlyph { - id: GlyphId(0), - position: point(px(20.0), px(0.0)), - index: 2, - is_emoji: false, - }, - ], - }, - ShapedRun { - font_id: FontId(1), - glyphs: vec![ - ShapedGlyph { - id: GlyphId(0), - position: point(px(30.0), px(0.0)), - index: 3, - is_emoji: false, - }, - ShapedGlyph { - id: GlyphId(0), - position: point(px(40.0), px(0.0)), - index: 4, - is_emoji: false, - }, - ShapedGlyph { - id: GlyphId(0), - position: point(px(50.0), px(0.0)), - index: 5, - is_emoji: false, - }, - ], - }, - ], - len: 6, - }), - text: "abcdef".into(), - decoration_runs: SmallVec::new(), - }; - - // First split at byte 2 — mid-run in run A - let (first, remainder) = line.split_at(2); - assert_eq!(first.text.as_ref(), "ab"); - assert_eq!(first.runs.len(), 1); - assert_eq!(first.runs[0].font_id, FontId(0)); - - // Remainder "cdef" should have two runs: tail of A (1 glyph) + all of B (3 glyphs) - assert_eq!(remainder.text.as_ref(), "cdef"); - assert_eq!(remainder.runs.len(), 2); - assert_eq!(remainder.runs[0].font_id, FontId(0)); - assert_eq!(remainder.runs[0].glyphs.len(), 1); - assert_eq!(remainder.runs[0].glyphs[0].index, 0); - assert_eq!(remainder.runs[0].glyphs[0].position.x, px(0.0)); - assert_eq!(remainder.runs[1].font_id, FontId(1)); - assert_eq!(remainder.runs[1].glyphs[0].index, 1); - assert_eq!(remainder.runs[1].glyphs[0].position.x, px(10.0)); - - // Second split at byte 2 within remainder — crosses the run boundary - let (second, final_part) = remainder.split_at(2); - assert_eq!(second.text.as_ref(), "cd"); - assert_eq!(final_part.text.as_ref(), "ef"); - assert_eq!(final_part.runs[0].glyphs[0].index, 0); - assert_eq!(final_part.runs[0].glyphs[0].position.x, px(0.0)); - - // Widths must sum across all three pieces - assert_eq!( - first.width() + second.width() + final_part.width(), - line.width() - ); - } - - #[test] - fn test_split_at_decorations() { - // Three decoration runs: red [0..2), green [2..5), blue [5..6). - // Split at byte 3 — red goes entirely left, green straddles, blue goes entirely right. - let red = Hsla { - h: 0.0, - s: 1.0, - l: 0.5, - a: 1.0, - }; - let green = Hsla { - h: 0.3, - s: 1.0, - l: 0.5, - a: 1.0, - }; - let blue = Hsla { - h: 0.6, - s: 1.0, - l: 0.5, - a: 1.0, - }; - - let line = make_shaped_line( - "abcdef", - &[ - (0, 0.0), - (1, 10.0), - (2, 20.0), - (3, 30.0), - (4, 40.0), - (5, 50.0), - ], - 60.0, - &[ - DecorationRun { - len: 2, - color: red, - background_color: None, - underline: None, - strikethrough: None, - }, - DecorationRun { - len: 3, - color: green, - background_color: None, - underline: None, - strikethrough: None, - }, - DecorationRun { - len: 1, - color: blue, - background_color: None, - underline: None, - strikethrough: None, - }, - ], - ); - - let (left, right) = line.split_at(3); - - // Left: red(2) + green(1) — green straddled, left portion has len 1 - assert_eq!(left.decoration_runs.len(), 2); - assert_eq!(left.decoration_runs[0].len, 2); - assert_eq!(left.decoration_runs[0].color, red); - assert_eq!(left.decoration_runs[1].len, 1); - assert_eq!(left.decoration_runs[1].color, green); - - // Right: green(2) + blue(1) — green straddled, right portion has len 2 - assert_eq!(right.decoration_runs.len(), 2); - assert_eq!(right.decoration_runs[0].len, 2); - assert_eq!(right.decoration_runs[0].color, green); - assert_eq!(right.decoration_runs[1].len, 1); - assert_eq!(right.decoration_runs[1].color, blue); - } -} diff --git a/crates/gpui_pre/src/text_system/line_layout.rs b/crates/gpui_pre/src/text_system/line_layout.rs deleted file mode 100644 index e7362f7..0000000 --- a/crates/gpui_pre/src/text_system/line_layout.rs +++ /dev/null @@ -1,1140 +0,0 @@ -use crate::{FontId, GlyphId, Pixels, PlatformTextSystem, Point, SharedString, Size, point, px}; -use collections::FxHashMap; -use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; -use smallvec::SmallVec; -use std::{ - borrow::Borrow, - hash::{Hash, Hasher}, - ops::Range, - sync::Arc, -}; - -use super::LineWrapper; - -/// A laid out and styled line of text -#[derive(Default, Debug)] -pub struct LineLayout { - /// The font size for this line - pub font_size: Pixels, - /// The width of the line - pub width: Pixels, - /// The ascent of the line - pub ascent: Pixels, - /// The descent of the line - pub descent: Pixels, - /// The shaped runs that make up this line - pub runs: Vec, - /// The length of the line in utf-8 bytes - pub len: usize, -} - -/// A run of text that has been shaped . -#[derive(Debug, Clone)] -pub struct ShapedRun { - /// The font id for this run - pub font_id: FontId, - /// The glyphs that make up this run - pub glyphs: Vec, -} - -/// A single glyph, ready to paint. -#[derive(Clone, Debug)] -pub struct ShapedGlyph { - /// The ID for this glyph, as determined by the text system. - pub id: GlyphId, - - /// The position of this glyph in its containing line. - pub position: Point, - - /// The index of this glyph in the original text. - pub index: usize, - - /// Whether this glyph is an emoji - pub is_emoji: bool, -} - -impl LineLayout { - /// The index for the character at the given x coordinate - pub fn index_for_x(&self, x: Pixels) -> Option { - if x >= self.width { - None - } else { - for run in self.runs.iter().rev() { - for glyph in run.glyphs.iter().rev() { - if glyph.position.x <= x { - return Some(glyph.index); - } - } - } - Some(0) - } - } - - /// closest_index_for_x returns the character boundary closest to the given x coordinate - /// (e.g. to handle aligning up/down arrow keys) - pub fn closest_index_for_x(&self, x: Pixels) -> usize { - let mut prev_index = 0; - let mut prev_x = px(0.); - - for run in self.runs.iter() { - for glyph in run.glyphs.iter() { - if glyph.position.x >= x { - if glyph.position.x - x < x - prev_x { - return glyph.index; - } else { - return prev_index; - } - } - prev_index = glyph.index; - prev_x = glyph.position.x; - } - } - - if self.len == 1 { - if x > self.width / 2. { - return 1; - } else { - return 0; - } - } - - self.len - } - - /// The x position of the character at the given index - pub fn x_for_index(&self, index: usize) -> Pixels { - for run in &self.runs { - for glyph in &run.glyphs { - if glyph.index >= index { - return glyph.position.x; - } - } - } - self.width - } - - /// The corresponding Font at the given index - pub fn font_id_for_index(&self, index: usize) -> Option { - for run in &self.runs { - for glyph in &run.glyphs { - if glyph.index >= index { - return Some(run.font_id); - } - } - } - None - } - - /// Split this layout at a byte index, returning `(prefix, suffix)`. - /// - /// - `prefix` contains glyphs for bytes `[0, byte_index)` with original positions. - /// Its width equals the x-advance up to the split point. - /// - `suffix` contains glyphs for bytes `[byte_index, len)` with positions - /// shifted left so the first glyph starts at x=0, and byte indices rebased to 0. - /// - `font_size`, `ascent`, and `descent` are copied to both halves. - pub fn split_at(&self, byte_index: usize) -> (LineLayout, LineLayout) { - let x_offset = self.x_for_index(byte_index); - - // Partition glyph runs. A single run may contribute glyphs to both halves. - let mut left_runs = Vec::new(); - let mut right_runs = Vec::new(); - - for run in &self.runs { - let split_pos = run.glyphs.partition_point(|g| g.index < byte_index); - - if split_pos > 0 { - left_runs.push(ShapedRun { - font_id: run.font_id, - glyphs: run.glyphs[..split_pos].to_vec(), - }); - } - - if split_pos < run.glyphs.len() { - let right_glyphs = run.glyphs[split_pos..] - .iter() - .map(|g| ShapedGlyph { - id: g.id, - position: point(g.position.x - x_offset, g.position.y), - index: g.index - byte_index, - is_emoji: g.is_emoji, - }) - .collect(); - right_runs.push(ShapedRun { - font_id: run.font_id, - glyphs: right_glyphs, - }); - } - } - - let left = LineLayout { - font_size: self.font_size, - width: x_offset, - ascent: self.ascent, - descent: self.descent, - runs: left_runs, - len: byte_index, - }; - - let right = LineLayout { - font_size: self.font_size, - width: self.width - x_offset, - ascent: self.ascent, - descent: self.descent, - runs: right_runs, - len: self.len - byte_index, - }; - - (left, right) - } - - fn compute_wrap_boundaries( - &self, - text: &str, - wrap_width: Pixels, - max_lines: Option, - ) -> SmallVec<[WrapBoundary; 1]> { - let mut boundaries = SmallVec::new(); - let mut first_non_whitespace_ix = None; - let mut last_candidate_ix = None; - let mut last_candidate_x = px(0.); - let mut last_boundary = WrapBoundary { - run_ix: 0, - glyph_ix: 0, - }; - let mut last_boundary_x = px(0.); - let mut prev_ch = '\0'; - let mut glyphs = self - .runs - .iter() - .enumerate() - .flat_map(move |(run_ix, run)| { - run.glyphs.iter().enumerate().map(move |(glyph_ix, glyph)| { - let character = text[glyph.index..].chars().next().unwrap(); - ( - WrapBoundary { run_ix, glyph_ix }, - character, - glyph.position.x, - ) - }) - }) - .peekable(); - - while let Some((boundary, ch, x)) = glyphs.next() { - if ch == '\n' { - continue; - } - - // Here is very similar to `LineWrapper::wrap_line` to determine text wrapping, - // but there are some differences, so we have to duplicate the code here. - if LineWrapper::is_word_char(ch) { - if prev_ch == ' ' && ch != ' ' && first_non_whitespace_ix.is_some() { - last_candidate_ix = Some(boundary); - last_candidate_x = x; - } - } else { - if ch != ' ' && first_non_whitespace_ix.is_some() { - last_candidate_ix = Some(boundary); - last_candidate_x = x; - } - } - - if ch != ' ' && first_non_whitespace_ix.is_none() { - first_non_whitespace_ix = Some(boundary); - } - - let next_x = glyphs.peek().map_or(self.width, |(_, _, x)| *x); - let width = next_x - last_boundary_x; - - if width > wrap_width && boundary > last_boundary { - // When used line_clamp, we should limit the number of lines. - if let Some(max_lines) = max_lines - && boundaries.len() >= max_lines.saturating_sub(1) - { - break; - } - - if let Some(last_candidate_ix) = last_candidate_ix.take() { - last_boundary = last_candidate_ix; - last_boundary_x = last_candidate_x; - } else { - last_boundary = boundary; - last_boundary_x = x; - } - boundaries.push(last_boundary); - } - prev_ch = ch; - } - - boundaries - } -} - -/// A line of text that has been wrapped to fit a given width -#[derive(Default, Debug)] -pub struct WrappedLineLayout { - /// The line layout, pre-wrapping. - pub unwrapped_layout: Arc, - - /// The boundaries at which the line was wrapped - pub wrap_boundaries: SmallVec<[WrapBoundary; 1]>, - - /// The width of the line, if it was wrapped - pub wrap_width: Option, -} - -/// A boundary at which a line was wrapped -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct WrapBoundary { - /// The index in the run just before the line was wrapped - pub run_ix: usize, - /// The index of the glyph just before the line was wrapped - pub glyph_ix: usize, -} - -impl WrappedLineLayout { - /// The length of the underlying text, in utf8 bytes. - #[allow(clippy::len_without_is_empty)] - pub fn len(&self) -> usize { - self.unwrapped_layout.len - } - - /// The width of this line, in pixels, whether or not it was wrapped. - pub fn width(&self) -> Pixels { - self.wrap_width - .unwrap_or(Pixels::MAX) - .min(self.unwrapped_layout.width) - } - - /// The size of the whole wrapped text, for the given line_height. - /// can span multiple lines if there are multiple wrap boundaries. - pub fn size(&self, line_height: Pixels) -> Size { - Size { - width: self.width(), - height: line_height * (self.wrap_boundaries.len() + 1), - } - } - - /// The ascent of a line in this layout - pub fn ascent(&self) -> Pixels { - self.unwrapped_layout.ascent - } - - /// The descent of a line in this layout - pub fn descent(&self) -> Pixels { - self.unwrapped_layout.descent - } - - /// The wrap boundaries in this layout - pub fn wrap_boundaries(&self) -> &[WrapBoundary] { - &self.wrap_boundaries - } - - /// The font size of this layout - pub fn font_size(&self) -> Pixels { - self.unwrapped_layout.font_size - } - - /// The runs in this layout, sans wrapping - pub fn runs(&self) -> &[ShapedRun] { - &self.unwrapped_layout.runs - } - - /// The index corresponding to a given position in this layout for the given line height. - /// - /// See also [`Self::closest_index_for_position`]. - pub fn index_for_position( - &self, - position: Point, - line_height: Pixels, - ) -> Result { - self._index_for_position(position, line_height, false) - } - - /// The closest index to a given position in this layout for the given line height. - /// - /// Closest means the character boundary closest to the given position. - /// - /// See also [`LineLayout::closest_index_for_x`]. - pub fn closest_index_for_position( - &self, - position: Point, - line_height: Pixels, - ) -> Result { - self._index_for_position(position, line_height, true) - } - - fn _index_for_position( - &self, - mut position: Point, - line_height: Pixels, - closest: bool, - ) -> Result { - let wrapped_line_ix = (position.y / line_height) as usize; - - let wrapped_line_start_index; - let wrapped_line_start_x; - if wrapped_line_ix > 0 { - let Some(line_start_boundary) = self.wrap_boundaries.get(wrapped_line_ix - 1) else { - return Err(0); - }; - let run = &self.unwrapped_layout.runs[line_start_boundary.run_ix]; - let glyph = &run.glyphs[line_start_boundary.glyph_ix]; - wrapped_line_start_index = glyph.index; - wrapped_line_start_x = glyph.position.x; - } else { - wrapped_line_start_index = 0; - wrapped_line_start_x = Pixels::ZERO; - }; - - let wrapped_line_end_index; - let wrapped_line_end_x; - if wrapped_line_ix < self.wrap_boundaries.len() { - let next_wrap_boundary_ix = wrapped_line_ix; - let next_wrap_boundary = self.wrap_boundaries[next_wrap_boundary_ix]; - let run = &self.unwrapped_layout.runs[next_wrap_boundary.run_ix]; - let glyph = &run.glyphs[next_wrap_boundary.glyph_ix]; - wrapped_line_end_index = glyph.index; - wrapped_line_end_x = glyph.position.x; - } else { - wrapped_line_end_index = self.unwrapped_layout.len; - wrapped_line_end_x = self.unwrapped_layout.width; - }; - - let mut position_in_unwrapped_line = position; - position_in_unwrapped_line.x += wrapped_line_start_x; - if position_in_unwrapped_line.x < wrapped_line_start_x { - Err(wrapped_line_start_index) - } else if position_in_unwrapped_line.x >= wrapped_line_end_x { - Err(wrapped_line_end_index) - } else { - if closest { - Ok(self - .unwrapped_layout - .closest_index_for_x(position_in_unwrapped_line.x)) - } else { - Ok(self - .unwrapped_layout - .index_for_x(position_in_unwrapped_line.x) - .unwrap()) - } - } - } - - /// Returns the pixel position for the given byte index. - pub fn position_for_index(&self, index: usize, line_height: Pixels) -> Option> { - let mut line_start_ix = 0; - let mut line_end_indices = self - .wrap_boundaries - .iter() - .map(|wrap_boundary| { - let run = &self.unwrapped_layout.runs[wrap_boundary.run_ix]; - let glyph = &run.glyphs[wrap_boundary.glyph_ix]; - glyph.index - }) - .chain([self.len()]) - .enumerate(); - for (ix, line_end_ix) in line_end_indices { - let line_y = ix as f32 * line_height; - if index < line_start_ix { - break; - } else if index > line_end_ix { - line_start_ix = line_end_ix; - continue; - } else { - let line_start_x = self.unwrapped_layout.x_for_index(line_start_ix); - let x = self.unwrapped_layout.x_for_index(index) - line_start_x; - return Some(point(x, line_y)); - } - } - - None - } -} - -pub(crate) struct LineLayoutCache { - previous_frame: Mutex, - current_frame: RwLock, - platform_text_system: Arc, -} - -#[derive(Default)] -struct FrameCache { - lines: FxHashMap, Arc>, - wrapped_lines: FxHashMap, Arc>, - used_lines: Vec>, - used_wrapped_lines: Vec>, - - // Content-addressable caches keyed by caller-provided text hash + layout params. - // These allow cache hits without materializing a contiguous `SharedString`. - // - // IMPORTANT: To support allocation-free lookups, we store these maps using a key type - // (`HashedCacheKeyRef`) that can be computed without building a contiguous `&str`/`SharedString`. - // On miss, we allocate once and store under an owned `HashedCacheKey`. - lines_by_hash: FxHashMap, Arc>, - wrapped_lines_by_hash: FxHashMap, Arc>, - used_lines_by_hash: Vec>, - used_wrapped_lines_by_hash: Vec>, -} - -#[derive(Clone, Default)] -pub(crate) struct LineLayoutIndex { - lines_index: usize, - wrapped_lines_index: usize, - lines_by_hash_index: usize, - wrapped_lines_by_hash_index: usize, -} - -impl LineLayoutCache { - pub fn new(platform_text_system: Arc) -> Self { - Self { - previous_frame: Mutex::default(), - current_frame: RwLock::default(), - platform_text_system, - } - } - - pub fn layout_index(&self) -> LineLayoutIndex { - let frame = self.current_frame.read(); - LineLayoutIndex { - lines_index: frame.used_lines.len(), - wrapped_lines_index: frame.used_wrapped_lines.len(), - lines_by_hash_index: frame.used_lines_by_hash.len(), - wrapped_lines_by_hash_index: frame.used_wrapped_lines_by_hash.len(), - } - } - - pub fn reuse_layouts(&self, range: Range) { - let mut previous_frame = &mut *self.previous_frame.lock(); - let mut current_frame = &mut *self.current_frame.write(); - - for key in &previous_frame.used_lines[range.start.lines_index..range.end.lines_index] { - if let Some((key, line)) = previous_frame.lines.remove_entry(key) { - current_frame.lines.insert(key, line); - } - current_frame.used_lines.push(key.clone()); - } - - for key in &previous_frame.used_wrapped_lines - [range.start.wrapped_lines_index..range.end.wrapped_lines_index] - { - if let Some((key, line)) = previous_frame.wrapped_lines.remove_entry(key) { - current_frame.wrapped_lines.insert(key, line); - } - current_frame.used_wrapped_lines.push(key.clone()); - } - - for key in &previous_frame.used_lines_by_hash - [range.start.lines_by_hash_index..range.end.lines_by_hash_index] - { - if let Some((key, line)) = previous_frame.lines_by_hash.remove_entry(key) { - current_frame.lines_by_hash.insert(key, line); - } - current_frame.used_lines_by_hash.push(key.clone()); - } - - for key in &previous_frame.used_wrapped_lines_by_hash - [range.start.wrapped_lines_by_hash_index..range.end.wrapped_lines_by_hash_index] - { - if let Some((key, line)) = previous_frame.wrapped_lines_by_hash.remove_entry(key) { - current_frame.wrapped_lines_by_hash.insert(key, line); - } - current_frame.used_wrapped_lines_by_hash.push(key.clone()); - } - } - - pub fn truncate_layouts(&self, index: LineLayoutIndex) { - let mut current_frame = &mut *self.current_frame.write(); - current_frame.used_lines.truncate(index.lines_index); - current_frame - .used_wrapped_lines - .truncate(index.wrapped_lines_index); - current_frame - .used_lines_by_hash - .truncate(index.lines_by_hash_index); - current_frame - .used_wrapped_lines_by_hash - .truncate(index.wrapped_lines_by_hash_index); - } - - pub fn finish_frame(&self) { - let mut prev_frame = self.previous_frame.lock(); - let mut curr_frame = self.current_frame.write(); - std::mem::swap(&mut *prev_frame, &mut *curr_frame); - curr_frame.lines.clear(); - curr_frame.wrapped_lines.clear(); - curr_frame.used_lines.clear(); - curr_frame.used_wrapped_lines.clear(); - - curr_frame.lines_by_hash.clear(); - curr_frame.wrapped_lines_by_hash.clear(); - curr_frame.used_lines_by_hash.clear(); - curr_frame.used_wrapped_lines_by_hash.clear(); - } - - pub fn layout_wrapped_line( - &self, - text: Text, - font_size: Pixels, - runs: &[FontRun], - wrap_width: Option, - max_lines: Option, - ) -> Arc - where - Text: AsRef, - SharedString: From, - { - let key = &CacheKeyRef { - text: text.as_ref(), - font_size, - runs, - wrap_width, - force_width: None, - } as &dyn AsCacheKeyRef; - - let current_frame = self.current_frame.upgradable_read(); - if let Some(layout) = current_frame.wrapped_lines.get(key) { - return layout.clone(); - } - - let previous_frame_entry = self.previous_frame.lock().wrapped_lines.remove_entry(key); - if let Some((key, layout)) = previous_frame_entry { - let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame); - current_frame - .wrapped_lines - .insert(key.clone(), layout.clone()); - current_frame.used_wrapped_lines.push(key); - layout - } else { - drop(current_frame); - let text = SharedString::from(text); - let unwrapped_layout = self.layout_line::<&SharedString>(&text, font_size, runs, None); - let wrap_boundaries = if let Some(wrap_width) = wrap_width { - unwrapped_layout.compute_wrap_boundaries(text.as_ref(), wrap_width, max_lines) - } else { - SmallVec::new() - }; - let layout = Arc::new(WrappedLineLayout { - unwrapped_layout, - wrap_boundaries, - wrap_width, - }); - let key = Arc::new(CacheKey { - text, - font_size, - runs: SmallVec::from(runs), - wrap_width, - force_width: None, - }); - - let mut current_frame = self.current_frame.write(); - current_frame - .wrapped_lines - .insert(key.clone(), layout.clone()); - current_frame.used_wrapped_lines.push(key); - - layout - } - } - - pub fn layout_line( - &self, - text: Text, - font_size: Pixels, - runs: &[FontRun], - force_width: Option, - ) -> Arc - where - Text: AsRef, - SharedString: From, - { - let key = &CacheKeyRef { - text: text.as_ref(), - font_size, - runs, - wrap_width: None, - force_width, - } as &dyn AsCacheKeyRef; - - let current_frame = self.current_frame.upgradable_read(); - if let Some(layout) = current_frame.lines.get(key) { - return layout.clone(); - } - - let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame); - if let Some((key, layout)) = self.previous_frame.lock().lines.remove_entry(key) { - current_frame.lines.insert(key.clone(), layout.clone()); - current_frame.used_lines.push(key); - layout - } else { - let text = SharedString::from(text); - let mut layout = self - .platform_text_system - .layout_line(&text, font_size, runs); - - if let Some(force_width) = force_width { - apply_force_width_to_layout(&mut layout, force_width); - } - - let key = Arc::new(CacheKey { - text, - font_size, - runs: SmallVec::from(runs), - wrap_width: None, - force_width, - }); - let layout = Arc::new(layout); - current_frame.lines.insert(key.clone(), layout.clone()); - current_frame.used_lines.push(key); - layout - } - } - - /// Try to retrieve a previously-shaped line layout using a caller-provided content hash. - /// - /// This is a *non-allocating* cache probe: it does not materialize any text. If the layout - /// is not already cached in either the current frame or previous frame, returns `None`. - /// - /// Contract (caller enforced): - /// - Same `text_hash` implies identical text content (collision risk accepted by caller). - /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions). - pub fn try_layout_line_by_hash( - &self, - text_hash: u64, - text_len: usize, - font_size: Pixels, - runs: &[FontRun], - force_width: Option, - ) -> Option> { - let key_ref = HashedCacheKeyRef { - text_hash, - text_len, - font_size, - runs, - wrap_width: None, - force_width, - }; - - let current_frame = self.current_frame.read(); - if let Some((_, layout)) = current_frame.lines_by_hash.iter().find(|(key, _)| { - HashedCacheKeyRef { - text_hash: key.text_hash, - text_len: key.text_len, - font_size: key.font_size, - runs: key.runs.as_slice(), - wrap_width: key.wrap_width, - force_width: key.force_width, - } == key_ref - }) { - return Some(layout.clone()); - } - - let previous_frame = self.previous_frame.lock(); - if let Some((_, layout)) = previous_frame.lines_by_hash.iter().find(|(key, _)| { - HashedCacheKeyRef { - text_hash: key.text_hash, - text_len: key.text_len, - font_size: key.font_size, - runs: key.runs.as_slice(), - wrap_width: key.wrap_width, - force_width: key.force_width, - } == key_ref - }) { - return Some(layout.clone()); - } - - None - } - - /// Layout a line of text using a caller-provided content hash as the cache key. - /// - /// This enables cache hits without materializing a contiguous `SharedString` for `text`. - /// If the cache misses, `materialize_text` is invoked to produce the `SharedString` for shaping. - /// - /// Contract (caller enforced): - /// - Same `text_hash` implies identical text content (collision risk accepted by caller). - /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions). - pub fn layout_line_by_hash( - &self, - text_hash: u64, - text_len: usize, - font_size: Pixels, - runs: &[FontRun], - force_width: Option, - materialize_text: impl FnOnce() -> SharedString, - ) -> Arc { - let key_ref = HashedCacheKeyRef { - text_hash, - text_len, - font_size, - runs, - wrap_width: None, - force_width, - }; - - // Fast path: already cached (no allocation). - let current_frame = self.current_frame.upgradable_read(); - if let Some((_, layout)) = current_frame.lines_by_hash.iter().find(|(key, _)| { - HashedCacheKeyRef { - text_hash: key.text_hash, - text_len: key.text_len, - font_size: key.font_size, - runs: key.runs.as_slice(), - wrap_width: key.wrap_width, - force_width: key.force_width, - } == key_ref - }) { - return layout.clone(); - } - - let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame); - - // Try to reuse from previous frame without allocating; do a linear scan to find a matching key. - // (We avoid `drain()` here because it would eagerly move all entries.) - let mut previous_frame = self.previous_frame.lock(); - if let Some(existing_key) = previous_frame - .used_lines_by_hash - .iter() - .find(|key| { - HashedCacheKeyRef { - text_hash: key.text_hash, - text_len: key.text_len, - font_size: key.font_size, - runs: key.runs.as_slice(), - wrap_width: key.wrap_width, - force_width: key.force_width, - } == key_ref - }) - .cloned() - { - if let Some((key, layout)) = previous_frame.lines_by_hash.remove_entry(&existing_key) { - current_frame - .lines_by_hash - .insert(key.clone(), layout.clone()); - current_frame.used_lines_by_hash.push(key); - return layout; - } - } - - let text = materialize_text(); - let mut layout = self - .platform_text_system - .layout_line(&text, font_size, runs); - - if let Some(force_width) = force_width { - apply_force_width_to_layout(&mut layout, force_width); - } - - let key = Arc::new(HashedCacheKey { - text_hash, - text_len, - font_size, - runs: SmallVec::from(runs), - wrap_width: None, - force_width, - }); - let layout = Arc::new(layout); - current_frame - .lines_by_hash - .insert(key.clone(), layout.clone()); - current_frame.used_lines_by_hash.push(key); - layout - } -} - -// Combining marks (e.g. Thai vowel signs, Arabic diacritics) are shaped by -// HarfBuzz at the same x position as their base character. The force-width -// loop must not advance the cell counter for these zero-advance glyphs, -// otherwise they get displaced into the next cell. We detect them by checking -// whether shaped x has advanced by at least half a cell beyond the last base. -fn apply_force_width_to_layout(layout: &mut LineLayout, force_width: Pixels) { - let mut glyph_pos: usize = 0; - // NEG_INFINITY ensures the first glyph is always classified as a base. - let mut last_base_shaped_x = px(f32::NEG_INFINITY); - let mut last_base_actual_x = px(0.); - - for run in layout.runs.iter_mut() { - for glyph in run.glyphs.iter_mut() { - let shaped_x = glyph.position.x; - - if shaped_x > last_base_shaped_x + force_width * 0.5 { - let forced_x = glyph_pos * force_width; - if (shaped_x - forced_x).abs() > px(1.) { - glyph.position.x = forced_x; - } - last_base_shaped_x = shaped_x; - last_base_actual_x = glyph.position.x; - glyph_pos += 1; - } else { - glyph.position.x = last_base_actual_x + (shaped_x - last_base_shaped_x); - } - } - } -} - -/// A run of text with a single font. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] -#[expect(missing_docs)] -pub struct FontRun { - pub len: usize, - pub font_id: FontId, -} - -trait AsCacheKeyRef { - fn as_cache_key_ref(&self) -> CacheKeyRef<'_>; -} - -#[derive(Clone, Debug, Eq)] -struct CacheKey { - text: SharedString, - font_size: Pixels, - runs: SmallVec<[FontRun; 1]>, - wrap_width: Option, - force_width: Option, -} - -#[derive(Copy, Clone, PartialEq, Eq, Hash)] -struct CacheKeyRef<'a> { - text: &'a str, - font_size: Pixels, - runs: &'a [FontRun], - wrap_width: Option, - force_width: Option, -} - -#[derive(Clone, Debug)] -struct HashedCacheKey { - text_hash: u64, - text_len: usize, - font_size: Pixels, - runs: SmallVec<[FontRun; 1]>, - wrap_width: Option, - force_width: Option, -} - -#[derive(Copy, Clone)] -struct HashedCacheKeyRef<'a> { - text_hash: u64, - text_len: usize, - font_size: Pixels, - runs: &'a [FontRun], - wrap_width: Option, - force_width: Option, -} - -impl PartialEq for dyn AsCacheKeyRef + '_ { - fn eq(&self, other: &dyn AsCacheKeyRef) -> bool { - self.as_cache_key_ref() == other.as_cache_key_ref() - } -} - -impl PartialEq for HashedCacheKey { - fn eq(&self, other: &Self) -> bool { - self.text_hash == other.text_hash - && self.text_len == other.text_len - && self.font_size == other.font_size - && self.runs.as_slice() == other.runs.as_slice() - && self.wrap_width == other.wrap_width - && self.force_width == other.force_width - } -} - -impl Eq for HashedCacheKey {} - -impl Hash for HashedCacheKey { - fn hash(&self, state: &mut H) { - self.text_hash.hash(state); - self.text_len.hash(state); - self.font_size.hash(state); - self.runs.as_slice().hash(state); - self.wrap_width.hash(state); - self.force_width.hash(state); - } -} - -impl PartialEq for HashedCacheKeyRef<'_> { - fn eq(&self, other: &Self) -> bool { - self.text_hash == other.text_hash - && self.text_len == other.text_len - && self.font_size == other.font_size - && self.runs == other.runs - && self.wrap_width == other.wrap_width - && self.force_width == other.force_width - } -} - -impl Eq for HashedCacheKeyRef<'_> {} - -impl Hash for HashedCacheKeyRef<'_> { - fn hash(&self, state: &mut H) { - self.text_hash.hash(state); - self.text_len.hash(state); - self.font_size.hash(state); - self.runs.hash(state); - self.wrap_width.hash(state); - self.force_width.hash(state); - } -} - -impl Eq for dyn AsCacheKeyRef + '_ {} - -impl Hash for dyn AsCacheKeyRef + '_ { - fn hash(&self, state: &mut H) { - self.as_cache_key_ref().hash(state) - } -} - -impl AsCacheKeyRef for CacheKey { - fn as_cache_key_ref(&self) -> CacheKeyRef<'_> { - CacheKeyRef { - text: &self.text, - font_size: self.font_size, - runs: self.runs.as_slice(), - wrap_width: self.wrap_width, - force_width: self.force_width, - } - } -} - -impl PartialEq for CacheKey { - fn eq(&self, other: &Self) -> bool { - self.as_cache_key_ref().eq(&other.as_cache_key_ref()) - } -} - -impl Hash for CacheKey { - fn hash(&self, state: &mut H) { - self.as_cache_key_ref().hash(state); - } -} - -impl<'a> Borrow for Arc { - fn borrow(&self) -> &(dyn AsCacheKeyRef + 'a) { - self.as_ref() as &dyn AsCacheKeyRef - } -} - -impl AsCacheKeyRef for CacheKeyRef<'_> { - fn as_cache_key_ref(&self) -> CacheKeyRef<'_> { - *self - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::GlyphId; - - fn glyph_at(x: f32, index: usize) -> ShapedGlyph { - ShapedGlyph { - id: GlyphId(0), - position: point(px(x), px(0.)), - index, - is_emoji: false, - } - } - - fn make_layout(glyphs: Vec) -> LineLayout { - LineLayout { - font_size: px(16.), - width: px(100.), - ascent: px(12.), - descent: px(4.), - runs: vec![ShapedRun { - font_id: FontId(0), - glyphs, - }], - len: 0, - } - } - - fn glyph_x_positions(layout: &LineLayout) -> Vec { - layout.runs[0] - .glyphs - .iter() - .map(|g| f32::from(g.position.x)) - .collect() - } - - #[test] - fn test_force_width_latin_unchanged() { - let cell_width = px(8.); - let mut layout = make_layout(vec![glyph_at(0., 0), glyph_at(8., 1), glyph_at(16., 2)]); - - apply_force_width_to_layout(&mut layout, cell_width); - - let positions = glyph_x_positions(&layout); - assert_eq!(positions, vec![0., 8., 16.]); - } - - #[test] - fn test_force_width_combining_marks_not_advanced() { - let cell_width = px(8.); - // Simulates Thai "กี" — base consonant at x=0, combining vowel also at x=0 - let mut layout = make_layout(vec![ - glyph_at(0., 0), // ก (base) - glyph_at(0., 3), // ี (combining mark, same x) - ]); - - apply_force_width_to_layout(&mut layout, cell_width); - - let positions = glyph_x_positions(&layout); - assert_eq!(positions, vec![0., 0.]); - } - - #[test] - fn test_force_width_base_after_combining_mark() { - let cell_width = px(8.); - let mut layout = make_layout(vec![glyph_at(0., 0), glyph_at(0., 3), glyph_at(8., 6)]); - - apply_force_width_to_layout(&mut layout, cell_width); - - let positions = glyph_x_positions(&layout); - assert_eq!(positions, vec![0., 0., 8.]); - } - - #[test] - fn test_force_width_multiple_combining_marks() { - let cell_width = px(8.); - // Simulates "ก้" — base + vowel + tone mark (two combining marks stacked) - let mut layout = make_layout(vec![ - glyph_at(0., 0), // ก (base) - glyph_at(0., 3), // vowel (combining) - glyph_at(0., 6), // tone mark (combining) - glyph_at(8., 9), // next base - ]); - - apply_force_width_to_layout(&mut layout, cell_width); - - let positions = glyph_x_positions(&layout); - assert_eq!(positions, vec![0., 0., 0., 8.]); - } - - #[test] - fn test_force_width_corrects_drifted_base_positions() { - let cell_width = px(8.); - // Font metrics don't perfectly match cell grid — glyphs drift >1px from cell boundary - let mut layout = make_layout(vec![ - glyph_at(0.5, 0), // within 1px tolerance, kept as-is - glyph_at(10.2, 1), // >1px off from 8.0, corrected - glyph_at(19.8, 2), // >1px off from 16.0, corrected - ]); - - apply_force_width_to_layout(&mut layout, cell_width); - - let positions = glyph_x_positions(&layout); - assert_eq!(positions, vec![0.5, 8., 16.]); - } - - #[test] - fn test_force_width_combining_mark_after_within_tolerance_base() { - let cell_width = px(8.); - // Base glyph is within 1px of grid so it keeps its shaped position. - // The combining mark must align to the base's actual position, not the grid slot. - let mut layout = make_layout(vec![glyph_at(0.5, 0), glyph_at(0.5, 3)]); - - apply_force_width_to_layout(&mut layout, cell_width); - - let positions = glyph_x_positions(&layout); - assert_eq!(positions, vec![0.5, 0.5]); - } -} diff --git a/crates/gpui_pre/src/text_system/line_wrapper.rs b/crates/gpui_pre/src/text_system/line_wrapper.rs deleted file mode 100644 index 678e9e0..0000000 --- a/crates/gpui_pre/src/text_system/line_wrapper.rs +++ /dev/null @@ -1,1587 +0,0 @@ -use crate::{FontId, Pixels, SharedString, TextRun, TextSystem, px}; -use collections::HashMap; -use std::{borrow::Cow, iter, sync::Arc}; - -/// Determines whether to truncate text from the start or end. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum TruncateFrom { - /// Truncate text from the start. - Start, - /// Truncate text from the end. - End, - /// Truncate text from the middle, preserving the start and end. - Middle, -} - -/// The GPUI line wrapper, used to wrap lines of text to a given width. -pub struct LineWrapper { - text_system: Arc, - pub(crate) font_id: FontId, - pub(crate) font_size: Pixels, - cached_ascii_char_widths: [Option; 128], - cached_other_char_widths: HashMap, -} - -impl LineWrapper { - /// The maximum indent that can be applied to a line. - pub const MAX_INDENT: u32 = 256; - - pub(crate) fn new(font_id: FontId, font_size: Pixels, text_system: Arc) -> Self { - Self { - text_system, - font_id, - font_size, - cached_ascii_char_widths: [None; 128], - cached_other_char_widths: HashMap::default(), - } - } - - /// Wrap a line of text to the given width with this wrapper's font and font size. - pub fn wrap_line<'a>( - &'a mut self, - fragments: &'a [LineFragment], - wrap_width: Pixels, - ) -> impl Iterator + 'a { - let mut width = px(0.); - let mut first_non_whitespace_ix = None; - let mut indent = None; - let mut last_candidate_ix = 0; - let mut last_candidate_width = px(0.); - let mut last_wrap_ix = 0; - let mut prev_c = '\0'; - let mut index = 0; - let mut candidates = fragments - .iter() - .flat_map(move |fragment| fragment.wrap_boundary_candidates()) - .peekable(); - iter::from_fn(move || { - for candidate in candidates.by_ref() { - let ix = index; - index += candidate.len_utf8(); - let mut new_prev_c = prev_c; - let item_width = match candidate { - WrapBoundaryCandidate::Char { character: c } => { - if c == '\n' { - continue; - } - - if Self::is_word_char(c) { - if prev_c == ' ' && c != ' ' && first_non_whitespace_ix.is_some() { - last_candidate_ix = ix; - last_candidate_width = width; - } - } else { - // CJK may not be space separated, e.g.: `Hello world你好世界` - if c != ' ' && first_non_whitespace_ix.is_some() { - last_candidate_ix = ix; - last_candidate_width = width; - } - } - - if c != ' ' && first_non_whitespace_ix.is_none() { - first_non_whitespace_ix = Some(ix); - } - - new_prev_c = c; - - self.width_for_char(c) - } - WrapBoundaryCandidate::Element { - width: element_width, - .. - } => { - if prev_c == ' ' && first_non_whitespace_ix.is_some() { - last_candidate_ix = ix; - last_candidate_width = width; - } - - if first_non_whitespace_ix.is_none() { - first_non_whitespace_ix = Some(ix); - } - - element_width - } - }; - - width += item_width; - if width > wrap_width && ix > last_wrap_ix { - if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix) - { - indent = Some( - Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32), - ); - } - - if last_candidate_ix > 0 { - last_wrap_ix = last_candidate_ix; - width -= last_candidate_width; - last_candidate_ix = 0; - } else { - last_wrap_ix = ix; - width = item_width; - } - - if let Some(indent) = indent { - width += self.width_for_char(' ') * indent as f32; - } - - return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0))); - } - - prev_c = new_prev_c; - } - - None - }) - } - - /// Determines if a line should be truncated based on its width. - /// - /// Returns the truncation index in `line`. - pub fn should_truncate_line( - &mut self, - line: &str, - truncate_width: Pixels, - truncation_affix: &str, - truncate_from: TruncateFrom, - ) -> Option { - let mut width = px(0.); - let suffix_width = truncation_affix - .chars() - .map(|c| self.width_for_char(c)) - .fold(px(0.0), |a, x| a + x); - let mut truncate_ix = 0; - - match truncate_from { - TruncateFrom::Start => { - for (ix, c) in line.char_indices().rev() { - if width + suffix_width < truncate_width { - truncate_ix = ix; - } - - let char_width = self.width_for_char(c); - width += char_width; - - if width.floor() > truncate_width { - return Some(truncate_ix); - } - } - } - TruncateFrom::End => { - for (ix, c) in line.char_indices() { - if width + suffix_width < truncate_width { - truncate_ix = ix; - } - - let char_width = self.width_for_char(c); - width += char_width; - - if width.floor() > truncate_width { - return Some(truncate_ix); - } - } - } - TruncateFrom::Middle => {} - } - - None - } - - fn should_truncate_line_middle( - &mut self, - line: &str, - truncate_width: Pixels, - truncation_affix: &str, - ) -> Option<(usize, usize)> { - let suffix_width = truncation_affix - .chars() - .map(|c| self.width_for_char(c)) - .fold(px(0.0), |a, x| a + x); - - let total_width: Pixels = line - .chars() - .map(|c| self.width_for_char(c)) - .fold(px(0.0), |a, x| a + x); - - if total_width <= truncate_width { - return None; - } - - let content_budget = truncate_width - suffix_width; - if content_budget <= px(0.) { - return Some((0, line.len())); - } - - let front_budget = content_budget * (2.0 / 3.0); - let back_budget = content_budget - front_budget; - - let mut front_width = px(0.); - let mut front_end_ix = 0usize; - for (ix, c) in line.char_indices() { - let char_width = self.width_for_char(c); - if front_width + char_width > front_budget { - break; - } - front_width += char_width; - front_end_ix = ix + c.len_utf8(); - } - - let mut back_width = px(0.); - let mut back_start_ix = line.len(); - for (ix, c) in line.char_indices().rev() { - let char_width = self.width_for_char(c); - if back_width + char_width > back_budget { - break; - } - back_width += char_width; - back_start_ix = ix; - } - - if front_end_ix >= back_start_ix { - return Some((0, line.len())); - } - - Some((front_end_ix, back_start_ix)) - } - - /// Truncate a line of text to the given width with this wrapper's font and font size. - pub fn truncate_line<'a>( - &mut self, - line: SharedString, - truncate_width: Pixels, - truncation_affix: &str, - runs: &'a [TextRun], - truncate_from: TruncateFrom, - ) -> (SharedString, Cow<'a, [TextRun]>) { - if truncate_from == TruncateFrom::Middle { - if let Some((front_end_ix, back_start_ix)) = - self.should_truncate_line_middle(&line, truncate_width, truncation_affix) - { - let result = SharedString::from(format!( - "{}{truncation_affix}{}", - &line[..front_end_ix], - &line[back_start_ix..] - )); - let mut runs = runs.to_vec(); - update_runs_after_middle_truncation( - truncation_affix, - &mut runs, - front_end_ix, - back_start_ix, - ); - return (result, Cow::Owned(runs)); - } else { - return (line, Cow::Borrowed(runs)); - } - } - - if let Some(truncate_ix) = - self.should_truncate_line(&line, truncate_width, truncation_affix, truncate_from) - { - let result = match truncate_from { - TruncateFrom::Start => SharedString::from(format!( - "{truncation_affix}{}", - &line[line.ceil_char_boundary(truncate_ix + 1)..] - )), - TruncateFrom::End => SharedString::from(format!( - "{}{truncation_affix}", - line[..truncate_ix] - .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation()) - )), - TruncateFrom::Middle => unreachable!("Middle truncation is handled above"), - }; - let mut runs = runs.to_vec(); - update_runs_after_truncation(&result, truncation_affix, &mut runs, truncate_from); - (result, Cow::Owned(runs)) - } else { - (line, Cow::Borrowed(runs)) - } - } - - /// Truncate text to fit within a given number of wrapped lines. - /// - /// Unlike `truncate_line` which treats the text as a flat width budget - /// (`width * max_lines`), this method accounts for word-boundary wrapping: - /// it walks through characters once, tracking wrap boundaries and the - /// truncation point simultaneously. When text overflows on the last - /// allowed line, it truncates there and appends the affix. - /// - /// For `max_lines == 1`, this delegates to `truncate_line`. - pub fn truncate_wrapped_line<'a>( - &mut self, - text: SharedString, - wrap_width: Pixels, - max_lines: usize, - truncation_affix: &str, - runs: &'a [TextRun], - truncate_from: TruncateFrom, - ) -> (SharedString, Cow<'a, [TextRun]>) { - if max_lines <= 1 || truncate_from == TruncateFrom::Start { - return self.truncate_line( - text, - wrap_width * max_lines, - truncation_affix, - runs, - truncate_from, - ); - } - if truncate_from == TruncateFrom::Middle { - return self.truncate_line(text, wrap_width, truncation_affix, runs, truncate_from); - } - - let affix_width: Pixels = truncation_affix - .chars() - .map(|c| self.width_for_char(c)) - .sum(); - - let mut width = px(0.); - let mut line = 0usize; - let mut first_non_whitespace_ix = None; - let mut last_candidate_ix = 0usize; - let mut last_candidate_width = px(0.); - let mut last_wrap_ix = 0usize; - let mut prev_c = '\0'; - let mut indent: Option = None; - let mut truncate_ix = 0usize; - - for (ix, c) in text.char_indices() { - if c == '\n' { - if line >= max_lines - 1 && !text[ix + 1..].trim().is_empty() { - // Newline on the last allowed line with real content - // below. Truncate here. - let truncated = text[..truncate_ix] - .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation()); - let result = SharedString::from(format!("{truncated}{truncation_affix}")); - let mut runs = runs.to_vec(); - update_runs_after_truncation( - &result, - truncation_affix, - &mut runs, - TruncateFrom::End, - ); - return (result, Cow::Owned(runs)); - } - - // Newline before the last line: it consumes a line. - line += 1; - width = px(0.); - first_non_whitespace_ix = None; - last_candidate_ix = 0; - last_candidate_width = px(0.); - last_wrap_ix = ix + 1; - prev_c = '\0'; - indent = None; - truncate_ix = ix + 1; - continue; - } - - let char_width = self.width_for_char(c); - - if Self::is_word_char(c) { - if prev_c == ' ' && first_non_whitespace_ix.is_some() { - last_candidate_ix = ix; - last_candidate_width = width; - } - } else if c != ' ' && first_non_whitespace_ix.is_some() { - last_candidate_ix = ix; - last_candidate_width = width; - } - - if c != ' ' && first_non_whitespace_ix.is_none() { - first_non_whitespace_ix = Some(ix); - } - - width += char_width; - - if line < max_lines - 1 { - // Before the last line: replicate wrap_line's boundary logic. - if width > wrap_width && ix > last_wrap_ix { - if let (None, Some(first_nw)) = (indent, first_non_whitespace_ix) { - indent = Some(Self::MAX_INDENT.min((first_nw - last_wrap_ix) as u32)); - } - - if last_candidate_ix > last_wrap_ix { - last_wrap_ix = last_candidate_ix; - width -= last_candidate_width; - last_candidate_ix = 0; - } else { - last_wrap_ix = ix; - width = char_width; - } - - if let Some(ind) = indent { - width += self.width_for_char(' ') * ind as f32; - } - - line += 1; - truncate_ix = last_wrap_ix; - } - } else { - // On the last line: track the furthest point where the affix - // still fits, and stop as soon as the line overflows. - if width + affix_width <= wrap_width { - truncate_ix = ix + c.len_utf8(); - } - - if width > wrap_width { - let truncated = text[..truncate_ix] - .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation()); - let result = SharedString::from(format!("{truncated}{truncation_affix}")); - let mut runs = runs.to_vec(); - update_runs_after_truncation( - &result, - truncation_affix, - &mut runs, - TruncateFrom::End, - ); - return (result, Cow::Owned(runs)); - } - } - - prev_c = c; - } - - // Text fits within max_lines without truncation. - (text, Cow::Borrowed(runs)) - } - - /// Any character in this list should be treated as a word character, - /// meaning it can be part of a word that should not be wrapped. - pub(crate) fn is_word_char(c: char) -> bool { - // ASCII alphanumeric characters, for English, numbers: `Hello123`, etc. - c.is_ascii_alphanumeric() || - // Latin script in Unicode for French, German, Spanish, etc. - // Latin-1 Supplement - // https://en.wikipedia.org/wiki/Latin-1_Supplement - matches!(c, '\u{00C0}'..='\u{00FF}') || - // Latin Extended-A - // https://en.wikipedia.org/wiki/Latin_Extended-A - matches!(c, '\u{0100}'..='\u{017F}') || - // Latin Extended-B - // https://en.wikipedia.org/wiki/Latin_Extended-B - matches!(c, '\u{0180}'..='\u{024F}') || - // Cyrillic for Russian, Ukrainian, etc. - // https://en.wikipedia.org/wiki/Cyrillic_script_in_Unicode - matches!(c, '\u{0400}'..='\u{04FF}') || - - // Vietnamese (https://vietunicode.sourceforge.net/charset/) - matches!(c, '\u{1E00}'..='\u{1EFF}') || // Latin Extended Additional - matches!(c, '\u{0300}'..='\u{036F}') || // Combining Diacritical Marks - - // Bengali (https://en.wikipedia.org/wiki/Bengali_(Unicode_block)) - matches!(c, '\u{0980}'..='\u{09FF}') || - - // Some other known special characters that should be treated as word characters, - // e.g. `a-b`, `var_name`, `I'm`/`won’t`, '@mention`, `#hashtag`, `100%`, `3.1415`, - // `2^3`, `a~b`, `a=1`, `Self::new`, etc. Trailing punctuation like `,`, `.`, `:`, `;` - // is included so it stays attached to the preceding word when wrapping. - matches!(c, '-' | '_' | '.' | '\'' | '’' | '‘' | '$' | '%' | '@' | '#' | '^' | '~' | ',' | '=' | ':' | ';') || - // Closing punctuation never starts a line (UAX #14 LB13: no break - // before `!`, `)`, `]`, `}`, closing quotes or an ellipsis) — `plz!`, - // `see)`, `quoted”` wrap as one word instead of orphaning the mark on - // the next line. `/` and `?` stay break opportunities so long paths - // and URLs (`a/b`, `foo?b=2`) can wrap. - matches!(c, '!' | ')' | ']' | '}' | '"' | '”' | '»' | '…') || - // `⋯` character is special used in Zed, to keep this at the end of the line. - matches!(c, '⋯') || - - // Non-breaking glue characters - matches!(c, '\u{202F}' | '\u{00A0}' | '\u{2011}') - } - - #[inline(always)] - fn width_for_char(&mut self, c: char) -> Pixels { - if (c as u32) < 128 { - if let Some(cached_width) = self.cached_ascii_char_widths[c as usize] { - cached_width - } else { - let width = self - .text_system - .layout_width(self.font_id, self.font_size, c); - self.cached_ascii_char_widths[c as usize] = Some(width); - width - } - } else if let Some(cached_width) = self.cached_other_char_widths.get(&c) { - *cached_width - } else { - let width = self - .text_system - .layout_width(self.font_id, self.font_size, c); - self.cached_other_char_widths.insert(c, width); - width - } - } -} - -fn update_runs_after_truncation( - result: &str, - ellipsis: &str, - runs: &mut Vec, - truncate_from: TruncateFrom, -) { - let mut truncate_at = result.len() - ellipsis.len(); - match truncate_from { - TruncateFrom::Start => { - for (run_index, run) in runs.iter_mut().enumerate().rev() { - if run.len <= truncate_at { - truncate_at -= run.len; - } else { - run.len = truncate_at + ellipsis.len(); - runs.splice(..run_index, std::iter::empty()); - break; - } - } - } - TruncateFrom::End => { - for (run_index, run) in runs.iter_mut().enumerate() { - if run.len <= truncate_at { - truncate_at -= run.len; - } else { - run.len = truncate_at + ellipsis.len(); - runs.truncate(run_index + 1); - break; - } - } - } - TruncateFrom::Middle => { - unreachable!("Middle truncation calls this function with TruncateFrom::End directly") - } - } -} - -fn update_runs_after_middle_truncation( - ellipsis: &str, - runs: &mut Vec, - front_end_ix: usize, - back_start_ix: usize, -) { - let original_runs = std::mem::take(runs); - let mut result_runs: Vec = Vec::with_capacity(original_runs.len()); - - // Front segment [0, front_end_ix) + ellipsis: walk forward until the run - // that straddles or ends at front_end_ix, then extend that run's length - // to include the ellipsis. - let mut front_remaining = front_end_ix; - let mut front_done = false; - for run in &original_runs { - if front_done { - break; - } - if run.len <= front_remaining { - result_runs.push(run.clone()); - front_remaining -= run.len; - } else { - let mut partial = run.clone(); - partial.len = front_remaining + ellipsis.len(); - result_runs.push(partial); - front_done = true; - } - } - if !front_done { - // front_end_ix landed exactly on a run boundary; append ellipsis to - // the last front run (or, if the front is empty, to the first back run). - if let Some(last) = result_runs.last_mut() { - last.len += ellipsis.len(); - } else if let Some(first) = original_runs.first() { - let mut affix_run = first.clone(); - affix_run.len = ellipsis.len(); - result_runs.push(affix_run); - } - } - - // Back segment [back_start_ix, original.len()): skip runs entirely in the - // removed middle, keep the rest. - let mut byte_pos = 0usize; - for run in &original_runs { - let run_end = byte_pos + run.len; - if run_end > back_start_ix { - if byte_pos < back_start_ix { - // Run straddles back_start_ix; keep only the tail. - let mut partial = run.clone(); - partial.len = run_end - back_start_ix; - result_runs.push(partial); - } else { - result_runs.push(run.clone()); - } - } - byte_pos = run_end; - } - - *runs = result_runs; -} - -/// A fragment of a line that can be wrapped. -pub enum LineFragment<'a> { - /// A text fragment consisting of characters. - Text { - /// The text content of the fragment. - text: &'a str, - }, - /// A non-text element with a fixed width. - Element { - /// The width of the element in pixels. - width: Pixels, - /// The UTF-8 encoded length of the element. - len_utf8: usize, - }, -} - -impl<'a> LineFragment<'a> { - /// Creates a new text fragment from the given text. - pub fn text(text: &'a str) -> Self { - LineFragment::Text { text } - } - - /// Creates a new non-text element with the given width and UTF-8 encoded length. - pub fn element(width: Pixels, len_utf8: usize) -> Self { - LineFragment::Element { width, len_utf8 } - } - - fn wrap_boundary_candidates(&self) -> impl Iterator { - let text = match self { - LineFragment::Text { text } => text, - LineFragment::Element { .. } => "\0", - }; - text.chars().map(move |character| { - if let LineFragment::Element { width, len_utf8 } = self { - WrapBoundaryCandidate::Element { - width: *width, - len_utf8: *len_utf8, - } - } else { - WrapBoundaryCandidate::Char { character } - } - }) - } -} - -enum WrapBoundaryCandidate { - Char { character: char }, - Element { width: Pixels, len_utf8: usize }, -} - -impl WrapBoundaryCandidate { - pub fn len_utf8(&self) -> usize { - match self { - WrapBoundaryCandidate::Char { character } => character.len_utf8(), - WrapBoundaryCandidate::Element { len_utf8: len, .. } => *len, - } - } -} - -/// A boundary between two lines of text. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct Boundary { - /// The index of the last character in a line - pub ix: usize, - /// The indent of the next line. - pub next_indent: u32, -} - -impl Boundary { - fn new(ix: usize, next_indent: u32) -> Self { - Self { ix, next_indent } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{Font, FontFeatures, FontStyle, FontWeight, TestAppContext, TestDispatcher, font}; - #[cfg(target_os = "macos")] - use crate::{TextRun, WindowTextSystem, WrapBoundary}; - - fn build_wrapper() -> LineWrapper { - let dispatcher = TestDispatcher::new(0); - let cx = TestAppContext::build(dispatcher, None); - let id = cx.text_system().resolve_font(&font(".ZedMono")); - LineWrapper::new(id, px(16.), cx.text_system().clone()) - } - - fn generate_test_runs(input_run_len: &[usize]) -> Vec { - input_run_len - .iter() - .map(|run_len| TextRun { - len: *run_len, - font: Font { - family: "Dummy".into(), - features: FontFeatures::default(), - fallbacks: None, - weight: FontWeight::default(), - style: FontStyle::Normal, - }, - ..Default::default() - }) - .collect() - } - - #[test] - fn test_wrap_line() { - let mut wrapper = build_wrapper(); - - assert_eq!( - wrapper - .wrap_line(&[LineFragment::text("aa bbb cccc ddddd eeee")], px(72.)) - .collect::>(), - &[ - Boundary::new(7, 0), - Boundary::new(12, 0), - Boundary::new(18, 0) - ], - ); - assert_eq!( - wrapper - .wrap_line(&[LineFragment::text("aaa aaaaaaaaaaaaaaaaaa")], px(72.0)) - .collect::>(), - &[ - Boundary::new(4, 0), - Boundary::new(11, 0), - Boundary::new(18, 0) - ], - ); - assert_eq!( - wrapper - .wrap_line(&[LineFragment::text(" aaaaaaa")], px(72.)) - .collect::>(), - &[ - Boundary::new(7, 5), - Boundary::new(9, 5), - Boundary::new(11, 5), - ] - ); - assert_eq!( - wrapper - .wrap_line( - &[LineFragment::text(" ")], - px(72.) - ) - .collect::>(), - &[ - Boundary::new(7, 0), - Boundary::new(14, 0), - Boundary::new(21, 0) - ] - ); - assert_eq!( - wrapper - .wrap_line(&[LineFragment::text(" aaaaaaaaaaaaaa")], px(72.)) - .collect::>(), - &[ - Boundary::new(7, 0), - Boundary::new(14, 3), - Boundary::new(18, 3), - Boundary::new(22, 3), - ] - ); - - // Test wrapping multiple text fragments - assert_eq!( - wrapper - .wrap_line( - &[ - LineFragment::text("aa bbb "), - LineFragment::text("cccc ddddd eeee") - ], - px(72.) - ) - .collect::>(), - &[ - Boundary::new(7, 0), - Boundary::new(12, 0), - Boundary::new(18, 0) - ], - ); - - // Test wrapping with a mix of text and element fragments - assert_eq!( - wrapper - .wrap_line( - &[ - LineFragment::text("aa "), - LineFragment::element(px(20.), 1), - LineFragment::text(" bbb "), - LineFragment::element(px(30.), 1), - LineFragment::text(" cccc") - ], - px(72.) - ) - .collect::>(), - &[ - Boundary::new(5, 0), - Boundary::new(9, 0), - Boundary::new(11, 0) - ], - ); - - // Test with element at the beginning and text afterward - assert_eq!( - wrapper - .wrap_line( - &[ - LineFragment::element(px(50.), 1), - LineFragment::text(" aaaa bbbb cccc dddd") - ], - px(72.) - ) - .collect::>(), - &[ - Boundary::new(2, 0), - Boundary::new(7, 0), - Boundary::new(12, 0), - Boundary::new(17, 0) - ], - ); - - // Test with a large element that forces wrapping by itself - assert_eq!( - wrapper - .wrap_line( - &[ - LineFragment::text("short text "), - LineFragment::element(px(100.), 1), - LineFragment::text(" more text") - ], - px(72.) - ) - .collect::>(), - &[ - Boundary::new(6, 0), - Boundary::new(11, 0), - Boundary::new(12, 0), - Boundary::new(18, 0) - ], - ); - - // Test with non-breaking glue characters - assert_eq!( - wrapper - .wrap_line( - &[LineFragment::text("a\u{202F}b\u{00A0}c\u{2011}d e")], - px(72.0) - ) - .collect::>(), - &[Boundary::new(12, 0),], // special chars above take up 3, 2 and 3 bytes, so boundary ends up at 12 - ); - } - - #[test] - fn test_truncate_line_end() { - let mut wrapper = build_wrapper(); - - fn perform_test( - wrapper: &mut LineWrapper, - text: &'static str, - expected: &'static str, - ellipsis: &str, - ) { - let dummy_run_lens = vec![text.len()]; - let dummy_runs = generate_test_runs(&dummy_run_lens); - let (result, dummy_runs) = wrapper.truncate_line( - text.into(), - px(220.), - ellipsis, - &dummy_runs, - TruncateFrom::End, - ); - assert_eq!(result, expected); - assert_eq!(dummy_runs.first().unwrap().len, result.len()); - } - - perform_test( - &mut wrapper, - "aa bbb cccc ddddd eeee ffff gggg", - "aa bbb cccc ddddd eeee", - "", - ); - perform_test( - &mut wrapper, - "aa bbb cccc ddddd eeee ffff gggg", - "aa bbb cccc ddddd eee…", - "…", - ); - perform_test( - &mut wrapper, - "aa bbb cccc ddddd eeee ffff gggg", - "aa bbb cccc dddd......", - "......", - ); - perform_test( - &mut wrapper, - "aa bbb cccc 🦀🦀🦀🦀🦀 eeee ffff gggg", - "aa bbb cccc 🦀🦀🦀🦀…", - "…", - ); - } - - #[test] - fn test_truncate_line_start() { - let mut wrapper = build_wrapper(); - - #[track_caller] - fn perform_test( - wrapper: &mut LineWrapper, - text: &'static str, - expected: &'static str, - ellipsis: &str, - ) { - let dummy_run_lens = vec![text.len()]; - let dummy_runs = generate_test_runs(&dummy_run_lens); - let (result, dummy_runs) = wrapper.truncate_line( - text.into(), - px(220.), - ellipsis, - &dummy_runs, - TruncateFrom::Start, - ); - assert_eq!(result, expected); - assert_eq!(dummy_runs.first().unwrap().len, result.len()); - } - - perform_test( - &mut wrapper, - "aaaa bbbb cccc ddddd eeee fff gg", - "cccc ddddd eeee fff gg", - "", - ); - perform_test( - &mut wrapper, - "aaaa bbbb cccc ddddd eeee fff gg", - "…ccc ddddd eeee fff gg", - "…", - ); - perform_test( - &mut wrapper, - "aaaa bbbb cccc ddddd eeee fff gg", - "......dddd eeee fff gg", - "......", - ); - perform_test( - &mut wrapper, - "aaaa bbbb cccc 🦀🦀🦀🦀🦀 eeee fff gg", - "…🦀🦀🦀🦀 eeee fff gg", - "…", - ); - } - - #[test] - fn test_truncate_multiple_runs_end() { - let mut wrapper = build_wrapper(); - - fn perform_test( - wrapper: &mut LineWrapper, - text: &'static str, - expected: &str, - run_lens: &[usize], - result_run_len: &[usize], - line_width: Pixels, - ) { - let dummy_runs = generate_test_runs(run_lens); - let (result, dummy_runs) = - wrapper.truncate_line(text.into(), line_width, "…", &dummy_runs, TruncateFrom::End); - assert_eq!(result, expected); - for (run, result_len) in dummy_runs.iter().zip(result_run_len) { - assert_eq!(run.len, *result_len); - } - } - // Case 0: Normal - // Text: abcdefghijkl - // Runs: Run0 { len: 12, ... } - // - // Truncate res: abcd… (truncate_at = 4) - // Run res: Run0 { string: abcd…, len: 7, ... } - perform_test(&mut wrapper, "abcdefghijkl", "abcd…", &[12], &[7], px(50.)); - // Case 1: Drop some runs - // Text: abcdefghijkl - // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... } - // - // Truncate res: abcdef… (truncate_at = 6) - // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len: - // 5, ... } - perform_test( - &mut wrapper, - "abcdefghijkl", - "abcdef…", - &[4, 4, 4], - &[4, 5], - px(70.), - ); - // Case 2: Truncate at start of some run - // Text: abcdefghijkl - // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... } - // - // Truncate res: abcdefgh… (truncate_at = 8) - // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len: - // 4, ... }, Run2 { string: …, len: 3, ... } - perform_test( - &mut wrapper, - "abcdefghijkl", - "abcdefgh…", - &[4, 4, 4], - &[4, 4, 3], - px(90.), - ); - } - - #[test] - fn test_truncate_multiple_runs_start() { - let mut wrapper = build_wrapper(); - - #[track_caller] - fn perform_test( - wrapper: &mut LineWrapper, - text: &'static str, - expected: &str, - run_lens: &[usize], - result_run_len: &[usize], - line_width: Pixels, - ) { - let dummy_runs = generate_test_runs(run_lens); - let (result, dummy_runs) = wrapper.truncate_line( - text.into(), - line_width, - "…", - &dummy_runs, - TruncateFrom::Start, - ); - assert_eq!(result, expected); - for (run, result_len) in dummy_runs.iter().zip(result_run_len) { - assert_eq!(run.len, *result_len); - } - } - // Case 0: Normal - // Text: abcdefghijkl - // Runs: Run0 { len: 12, ... } - // - // Truncate res: …ijkl (truncate_at = 9) - // Run res: Run0 { string: …ijkl, len: 7, ... } - perform_test(&mut wrapper, "abcdefghijkl", "…ijkl", &[12], &[7], px(50.)); - // Case 1: Drop some runs - // Text: abcdefghijkl - // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... } - // - // Truncate res: …ghijkl (truncate_at = 7) - // Runs res: Run0 { string: …gh, len: 5, ... }, Run1 { string: ijkl, len: - // 4, ... } - perform_test( - &mut wrapper, - "abcdefghijkl", - "…ghijkl", - &[4, 4, 4], - &[5, 4], - px(70.), - ); - // Case 2: Truncate at start of some run - // Text: abcdefghijkl - // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... } - // - // Truncate res: abcdefgh… (truncate_at = 3) - // Runs res: Run0 { string: …, len: 3, ... }, Run1 { string: efgh, len: - // 4, ... }, Run2 { string: ijkl, len: 4, ... } - perform_test( - &mut wrapper, - "abcdefghijkl", - "…efghijkl", - &[4, 4, 4], - &[3, 4, 4], - px(90.), - ); - } - - #[test] - fn test_update_run_after_truncation_end() { - fn perform_test(result: &str, run_lens: &[usize], result_run_lens: &[usize]) { - let mut dummy_runs = generate_test_runs(run_lens); - update_runs_after_truncation(result, "…", &mut dummy_runs, TruncateFrom::End); - for (run, result_len) in dummy_runs.iter().zip(result_run_lens) { - assert_eq!(run.len, *result_len); - } - } - // Case 0: Normal - // Text: abcdefghijkl - // Runs: Run0 { len: 12, ... } - // - // Truncate res: abcd… (truncate_at = 4) - // Run res: Run0 { string: abcd…, len: 7, ... } - perform_test("abcd…", &[12], &[7]); - // Case 1: Drop some runs - // Text: abcdefghijkl - // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... } - // - // Truncate res: abcdef… (truncate_at = 6) - // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len: - // 5, ... } - perform_test("abcdef…", &[4, 4, 4], &[4, 5]); - // Case 2: Truncate at start of some run - // Text: abcdefghijkl - // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... } - // - // Truncate res: abcdefgh… (truncate_at = 8) - // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len: - // 4, ... }, Run2 { string: …, len: 3, ... } - perform_test("abcdefgh…", &[4, 4, 4], &[4, 4, 3]); - } - - #[test] - fn test_is_word_char() { - #[track_caller] - fn assert_word(word: &str) { - for c in word.chars() { - assert!( - LineWrapper::is_word_char(c), - "assertion failed for '{}' (unicode 0x{:x})", - c, - c as u32 - ); - } - } - - #[track_caller] - fn assert_not_word(word: &str) { - let found = word.chars().any(|c| !LineWrapper::is_word_char(c)); - assert!(found, "assertion failed for '{}'", word); - } - - assert_word("Hello123"); - assert_word("non-English"); - assert_word("var_name"); - assert_word("123456"); - assert_word("3.1415"); - assert_word("10^2"); - assert_word("1~2"); - assert_word("100%"); - assert_word("@mention"); - assert_word("#hashtag"); - assert_word("$variable"); - assert_word("a=1"); - assert_word("Self::is_word_char"); - assert_word("on;"); - assert_word("more⋯"); - assert_word("won’t"); - assert_word("‘twas"); - assert_word("plz!"); - assert_word("see)"); - assert_word("quoted”"); - assert_word("well…"); - - // Space - assert_not_word("foo bar"); - - // URL case - assert_word("github.com"); - assert_not_word("zed-industries/zed"); - assert_not_word("zed-industries\\zed"); - assert_not_word("a=1&b=2"); - assert_not_word("foo?b=2"); - - // Latin-1 Supplement - assert_word("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏ"); - // Latin Extended-A - assert_word("ĀāĂ㥹ĆćĈĉĊċČčĎď"); - // Latin Extended-B - assert_word("ƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏ"); - // Cyrillic - assert_word("АБВГДЕЖЗИЙКЛМНОП"); - // Vietnamese (https://github.com/zed-industries/zed/issues/23245) - assert_word("ThậmchíđếnkhithuachạychúngcònnhẫntâmgiếtnốtsốđôngtùchínhtrịởYênBáivàCaoBằng"); - // Bengali - assert_word("গিয়েছিলেন"); - assert_word("ছেলে"); - assert_word("হচ্ছিল"); - - // non-word characters - assert_not_word("你好"); - assert_not_word("안녕하세요"); - assert_not_word("こんにちは"); - assert_not_word("😀😁😂"); - assert_not_word("()[]{}<>"); - - // Non-breaking ("Glue") characters, see https://www.unicode.org/reports/tr14/ - // (https://github.com/zed-industries/zed/issues/59664) - assert_word("\u{202F}"); // NNBSP " " - assert_word("\u{00A0}"); // NBSP " " - assert_word("\u{2011}"); // NBH "‑" - } - - // For compatibility with the test macro - #[cfg(target_os = "macos")] - use crate as gpui; - - // These seem to vary wildly based on the text system. - #[cfg(target_os = "macos")] - #[crate::test] - fn test_wrap_shaped_line(cx: &mut TestAppContext) { - cx.update(|cx| { - let text_system = WindowTextSystem::new(cx.text_system().clone()); - - let normal = TextRun { - len: 0, - font: font("Helvetica"), - color: Default::default(), - underline: Default::default(), - ..Default::default() - }; - let bold = TextRun { - len: 0, - font: font("Helvetica").bold(), - ..Default::default() - }; - - let text = "aa bbb cccc ddddd eeee".into(); - let lines = text_system - .shape_text( - text, - px(16.), - &[ - normal.with_len(4), - bold.with_len(5), - normal.with_len(6), - bold.with_len(1), - normal.with_len(7), - ], - Some(px(72.)), - None, - ) - .unwrap(); - - assert_eq!( - lines[0].layout.wrap_boundaries(), - &[ - WrapBoundary { - run_ix: 0, - glyph_ix: 7 - }, - WrapBoundary { - run_ix: 0, - glyph_ix: 12 - }, - WrapBoundary { - run_ix: 0, - glyph_ix: 18 - } - ], - ); - }); - } - - #[test] - fn test_multiline_truncation_fits_within_wrapped_lines() { - let mut wrapper = build_wrapper(); - - // With .ZedMono at 16px, each char is 9.6px wide. - // wrap_width = 72px fits ~7 chars per line. - // - // "aa bbbbbb cccccc dddddd eeee ffff" with wrap_width=72px wraps as: - // Line 1: "aa " (28.8px, wraps because "bbbbbb" won't fit) - // Line 2: "bbbbbb " (67.2px) - // Line 3: "cccccc " (67.2px) - // ... - // - // truncate_wrapped_line should wrap first to find line 2 starts at - // "bbbbbb...", then truncate only that line to fit with ellipsis. - let text: &str = "aa bbbbbb cccccc dddddd eeee ffff"; - let wrap_width = px(72.); - let max_lines: usize = 2; - - let runs = generate_test_runs(&[text.len()]); - let (truncated, _) = wrapper.truncate_wrapped_line( - text.into(), - wrap_width, - max_lines, - "\u{2026}", - &runs, - TruncateFrom::End, - ); - - // The truncated text, when wrapped, must fit within max_lines lines. - let wrap_count = wrapper - .wrap_line(&[LineFragment::text(&truncated)], wrap_width) - .count(); - - assert!( - wrap_count < max_lines, - "Truncated text '{}' wraps into {} visual lines, expected at most {}", - truncated, - wrap_count + 1, - max_lines - ); - - // The truncated text should end with the ellipsis. - assert!( - truncated.ends_with('\u{2026}'), - "Truncated text '{}' should end with ellipsis", - truncated - ); - } - - #[test] - fn test_multiline_truncation_no_truncation_needed() { - let mut wrapper = build_wrapper(); - - // Text that fits in 2 lines shouldn't be truncated. - // Line 1: "aa bbb " (67.2px), Line 2: "cccccc" (57.6px) - let text: &str = "aa bbb cccccc"; - let wrap_width = px(72.); - let max_lines: usize = 2; - - let runs = generate_test_runs(&[text.len()]); - let (result, _) = wrapper.truncate_wrapped_line( - text.into(), - wrap_width, - max_lines, - "\u{2026}", - &runs, - TruncateFrom::End, - ); - - assert_eq!( - result.as_ref(), - text, - "Text that fits should not be modified" - ); - } - - #[test] - fn test_multiline_truncation_three_lines() { - let mut wrapper = build_wrapper(); - - let text: &str = "aa bbb cccc ddddd eeee ffff gggg hhhh iiii jjjj"; - let wrap_width = px(72.); - let max_lines: usize = 3; - - let runs = generate_test_runs(&[text.len()]); - let (truncated, _) = wrapper.truncate_wrapped_line( - text.into(), - wrap_width, - max_lines, - "\u{2026}", - &runs, - TruncateFrom::End, - ); - - let wrap_count = wrapper - .wrap_line(&[LineFragment::text(&truncated)], wrap_width) - .count(); - - assert!( - wrap_count < max_lines, - "Truncated text '{}' wraps into {} visual lines, expected at most {}", - truncated, - wrap_count + 1, - max_lines - ); - - assert!( - truncated.ends_with('\u{2026}'), - "Truncated text '{}' should end with ellipsis", - truncated - ); - } - - #[test] - fn test_multiline_truncation_with_newlines() { - let mut wrapper = build_wrapper(); - - // "hello\nworld foo bar baz" with line_clamp(2): - // shape_text splits on \n, giving physical lines "hello" and - // "world foo bar baz". The newline consumes line 1, so the - // second physical line should be truncated on line 2. - let text: &str = "hello\nworld foo bar baz"; - let wrap_width = px(72.); - let max_lines: usize = 2; - - let runs = generate_test_runs(&[text.len()]); - let (truncated, _) = wrapper.truncate_wrapped_line( - text.into(), - wrap_width, - max_lines, - "\u{2026}", - &runs, - TruncateFrom::End, - ); - - // The newline should be preserved. - let parts: Vec<&str> = truncated.splitn(2, '\n').collect(); - assert_eq!( - parts.len(), - 2, - "Newline should be preserved: '{}'", - truncated - ); - assert_eq!(parts[0], "hello"); - - // The second line should fit within wrap_width and end with ellipsis. - let second_line_width: Pixels = parts[1].chars().map(|c| wrapper.width_for_char(c)).sum(); - assert!( - second_line_width <= wrap_width, - "Second line '{}' ({}px) exceeds wrap_width ({}px)", - parts[1], - second_line_width, - wrap_width - ); - assert!( - truncated.ends_with('\u{2026}'), - "Should end with ellipsis: '{}'", - truncated - ); - } - - #[test] - fn test_multiline_truncation_newline_on_last_line() { - let mut wrapper = build_wrapper(); - - // "hello\nworld\nmore" with line_clamp(2): - // Line 1: "hello", Line 2: "world" — but there's a third line, - // so line 2 should be truncated with ellipsis. - let text: &str = "hello\nworld\nmore"; - let wrap_width = px(72.); - let max_lines: usize = 2; - - let runs = generate_test_runs(&[text.len()]); - let (truncated, _) = wrapper.truncate_wrapped_line( - text.into(), - wrap_width, - max_lines, - "\u{2026}", - &runs, - TruncateFrom::End, - ); - - let parts: Vec<&str> = truncated.splitn(2, '\n').collect(); - assert_eq!(parts[0], "hello"); - assert!( - truncated.ends_with('\u{2026}'), - "Should end with ellipsis since there's more content: '{}'", - truncated - ); - } - - #[test] - fn test_truncate_line_middle() { - let mut wrapper = build_wrapper(); - - // No truncation when text fits within a very wide budget. - let short_text = "hello world"; - let runs = generate_test_runs(&[short_text.len()]); - let (result, result_runs) = wrapper.truncate_line( - short_text.into(), - px(10000.), - "…", - &runs, - TruncateFrom::Middle, - ); - assert_eq!(result.as_ref(), short_text); - assert_eq!(result_runs.len(), 1); - assert_eq!(result_runs[0].len, short_text.len()); - - // Basic middle truncation: long string with px(100.) budget. - let long_text = "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz"; - let runs = generate_test_runs(&[long_text.len()]); - let (result, _result_runs) = - wrapper.truncate_line(long_text.into(), px(100.), "…", &runs, TruncateFrom::Middle); - assert!( - result.contains('…'), - "Middle-truncated result should contain '…', got: '{}'", - result - ); - assert!( - result.chars().count() < long_text.chars().count(), - "Middle-truncated result should be shorter than original" - ); - assert_eq!( - result.chars().next(), - long_text.chars().next(), - "Result should start with the same first character as original" - ); - assert_eq!( - result.chars().last(), - long_text.chars().last(), - "Result should end with the same last character as original" - ); - - // Degenerate case: budget so narrow that middle truncation cannot find a valid split. - // Still show the truncation affix instead of returning the original overflowing text. - let text = "abcdef"; - let runs = generate_test_runs(&[text.len()]); - let (result, result_runs) = - wrapper.truncate_line(text.into(), px(1.), "…", &runs, TruncateFrom::Middle); - assert_eq!(result.as_ref(), "…"); - assert_eq!(result_runs.len(), 1); - assert_eq!(result_runs[0].len, "…".len()); - - // Run adjustment correctness: multiple runs across the string. - // Verify that the returned runs' lengths sum to result.len(). - let multi_run_text = "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz"; - let run_lens = [20, 20, multi_run_text.len() - 40]; - let runs = generate_test_runs(&run_lens); - let (result, result_runs) = wrapper.truncate_line( - multi_run_text.into(), - px(100.), - "…", - &runs, - TruncateFrom::Middle, - ); - let total_run_len: usize = result_runs.iter().map(|r| r.len).sum(); - assert_eq!( - total_run_len, - result.len(), - "Sum of run lengths ({}) should equal result byte length ({})", - total_run_len, - result.len() - ); - } - - #[test] - fn test_multiline_truncation_trailing_newline() { - let mut wrapper = build_wrapper(); - - // "hello\nworld\n" with line_clamp(2): - // The trailing newline has no content after it, so no ellipsis. - let text: &str = "hello\nworld\n"; - let wrap_width = px(72.); - let max_lines: usize = 2; - - let runs = generate_test_runs(&[text.len()]); - let (result, _) = wrapper.truncate_wrapped_line( - text.into(), - wrap_width, - max_lines, - "\u{2026}", - &runs, - TruncateFrom::End, - ); - - assert!( - !result.ends_with('\u{2026}'), - "Trailing newline with no content should not add ellipsis: '{}'", - result - ); - } - - #[test] - fn test_multiline_truncation_newline_fits_exactly() { - let mut wrapper = build_wrapper(); - - // "hello\nworld" with line_clamp(2): - // Exactly 2 lines, no truncation needed. - let text: &str = "hello\nworld"; - let wrap_width = px(72.); - let max_lines: usize = 2; - - let runs = generate_test_runs(&[text.len()]); - let (result, _) = wrapper.truncate_wrapped_line( - text.into(), - wrap_width, - max_lines, - "\u{2026}", - &runs, - TruncateFrom::End, - ); - - assert_eq!( - result.as_ref(), - text, - "Text that fits exactly should not be modified: '{}'", - result - ); - } -} diff --git a/crates/gpui_pre/src/util.rs b/crates/gpui_pre/src/util.rs deleted file mode 100644 index 13ee752..0000000 --- a/crates/gpui_pre/src/util.rs +++ /dev/null @@ -1,252 +0,0 @@ -use crate::{BackgroundExecutor, Task}; -use std::{ - future::Future, - pin::Pin, - sync::atomic::{AtomicUsize, Ordering::SeqCst}, - task, - time::Duration, -}; - -/// A helper trait for building complex objects with imperative conditionals in a fluent style. -pub trait FluentBuilder { - /// Imperatively modify self with the given closure. - fn map(self, f: impl FnOnce(Self) -> U) -> U - where - Self: Sized, - { - f(self) - } - - /// Conditionally modify self with the given closure. - fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self - where - Self: Sized, - { - self.map(|this| if condition { then(this) } else { this }) - } - - /// Conditionally modify self with the given closure. - fn when_else( - self, - condition: bool, - then: impl FnOnce(Self) -> Self, - else_fn: impl FnOnce(Self) -> Self, - ) -> Self - where - Self: Sized, - { - self.map(|this| if condition { then(this) } else { else_fn(this) }) - } - - /// Conditionally unwrap and modify self with the given closure, if the given option is Some. - fn when_some(self, option: Option, then: impl FnOnce(Self, T) -> Self) -> Self - where - Self: Sized, - { - self.map(|this| { - if let Some(value) = option { - then(this, value) - } else { - this - } - }) - } - /// Conditionally unwrap and modify self with the given closure, if the given option is None. - fn when_none(self, option: &Option, then: impl FnOnce(Self) -> Self) -> Self - where - Self: Sized, - { - self.map(|this| if option.is_some() { this } else { then(this) }) - } -} - -/// Extensions for Future types that provide additional combinators and utilities. -pub trait FutureExt { - /// Requires a Future to complete before the specified duration has elapsed. - /// Similar to tokio::timeout. - fn with_timeout(self, timeout: Duration, executor: &BackgroundExecutor) -> WithTimeout - where - Self: Sized; -} - -impl FutureExt for T { - fn with_timeout(self, timeout: Duration, executor: &BackgroundExecutor) -> WithTimeout - where - Self: Sized, - { - WithTimeout { - future: self, - timer: executor.timer(timeout), - } - } -} - -#[pin_project::pin_project] -pub struct WithTimeout { - #[pin] - future: T, - #[pin] - timer: Task<()>, -} - -#[derive(Debug, thiserror::Error)] -#[error("Timed out before future resolved")] -/// Error returned by with_timeout when the timeout duration elapsed before the future resolved -pub struct Timeout; - -impl Future for WithTimeout { - type Output = Result; - - fn poll(self: Pin<&mut Self>, cx: &mut task::Context) -> task::Poll { - let this = self.project(); - - if let task::Poll::Ready(output) = this.future.poll(cx) { - task::Poll::Ready(Ok(output)) - } else if this.timer.poll(cx).is_ready() { - task::Poll::Ready(Err(Timeout)) - } else { - task::Poll::Pending - } - } -} - -/// Increment the given atomic counter if it is not zero. -/// Return the new value of the counter. -pub(crate) fn atomic_incr_if_not_zero(counter: &AtomicUsize) -> usize { - let mut loaded = counter.load(SeqCst); - loop { - if loaded == 0 { - return 0; - } - match counter.compare_exchange_weak(loaded, loaded + 1, SeqCst, SeqCst) { - Ok(x) => return x + 1, - Err(actual) => loaded = actual, - } - } -} - -/// Rounds to the nearest integer with 0.5 ties toward zero. -#[inline] -pub(crate) fn round_half_toward_zero(value: f32) -> f32 { - (value.abs() - 0.5).ceil().copysign(value) -} - -#[inline] -pub(crate) fn round_half_toward_zero_f64(value: f64) -> f64 { - (value.abs() - 0.5).ceil().copysign(value) -} - -#[inline] -pub(crate) fn round_to_device_pixel(logical: f32, scale_factor: f32) -> f32 { - round_half_toward_zero(logical * scale_factor) -} - -#[inline] -pub(crate) fn round_stroke_to_device_pixel(logical: f32, scale_factor: f32) -> f32 { - if logical == 0.0 { - 0.0 - } else { - round_to_device_pixel(logical.max(0.0), scale_factor).max(1.0) - } -} - -#[inline] -pub(crate) fn floor_to_device_pixel(logical: f32, scale_factor: f32) -> f32 { - (logical * scale_factor).floor() -} - -#[inline] -pub(crate) fn ceil_to_device_pixel(logical: f32, scale_factor: f32) -> f32 { - (logical * scale_factor).ceil() -} - -#[cfg(test)] -mod tests { - use crate::TestAppContext; - - use super::*; - - #[test] - fn test_round_half_toward_zero() { - // Midpoint ties go toward zero - assert_eq!(round_half_toward_zero(0.5), 0.0); - assert_eq!(round_half_toward_zero(1.5), 1.0); - assert_eq!(round_half_toward_zero(2.5), 2.0); - assert_eq!(round_half_toward_zero(-0.5), 0.0); - assert_eq!(round_half_toward_zero(-1.5), -1.0); - assert_eq!(round_half_toward_zero(-2.5), -2.0); - - // Non-midpoint values round to nearest - assert_eq!(round_half_toward_zero(1.5001), 2.0); - assert_eq!(round_half_toward_zero(1.4999), 1.0); - assert_eq!(round_half_toward_zero(-1.5001), -2.0); - assert_eq!(round_half_toward_zero(-1.4999), -1.0); - - // Integers are unchanged - assert_eq!(round_half_toward_zero(0.0), 0.0); - assert_eq!(round_half_toward_zero(3.0), 3.0); - assert_eq!(round_half_toward_zero(-3.0), -3.0); - } - - #[test] - fn test_device_pixel_helpers() { - // Snap uses half-toward-zero: 1.0 * 1.5 = 1.5 ties toward 1.0. - assert_eq!(round_to_device_pixel(1.0, 1.5), 1.0); - // Below the tie rounds down, above rounds up. - assert_eq!(round_to_device_pixel(0.3, 2.0), 1.0); - assert_eq!(round_to_device_pixel(1.4, 1.0), 1.0); - assert_eq!(round_to_device_pixel(1.6, 1.0), 2.0); - - // Stroke uses snap, but clamps non-zero input up to at least 1dp. - assert_eq!(round_stroke_to_device_pixel(0.0, 1.0), 0.0); - assert_eq!(round_stroke_to_device_pixel(0.4, 1.0), 1.0); - assert_eq!(round_stroke_to_device_pixel(0.5, 1.0), 1.0); - assert_eq!(round_stroke_to_device_pixel(1.0, 1.5), 1.0); - assert_eq!(round_stroke_to_device_pixel(1.6, 1.0), 2.0); - - // Cover's near edge floors, far edge ceils. Together they form a strict superset. - assert_eq!(floor_to_device_pixel(0.3, 2.0), 0.0); - assert_eq!(ceil_to_device_pixel(0.3, 2.0), 1.0); - assert_eq!(floor_to_device_pixel(2.1, 1.0), 2.0); - assert_eq!(ceil_to_device_pixel(2.1, 1.0), 3.0); - - // Integer device-pixel inputs are stable under all three. - assert_eq!(round_to_device_pixel(2.0, 2.0), 4.0); - assert_eq!(floor_to_device_pixel(2.0, 2.0), 4.0); - assert_eq!(ceil_to_device_pixel(2.0, 2.0), 4.0); - } - - #[test] - fn test_round_half_toward_zero_f64() { - assert_eq!(round_half_toward_zero_f64(0.5), 0.0); - assert_eq!(round_half_toward_zero_f64(-0.5), 0.0); - assert_eq!(round_half_toward_zero_f64(1.5), 1.0); - assert_eq!(round_half_toward_zero_f64(-1.5), -1.0); - assert_eq!(round_half_toward_zero_f64(2.5001), 3.0); - } - - #[gpui::test] - async fn test_with_timeout(cx: &mut TestAppContext) { - Task::ready(()) - .with_timeout(Duration::from_secs(1), &cx.executor()) - .await - .expect("Timeout should be noop"); - - let long_duration = Duration::from_secs(6000); - let short_duration = Duration::from_secs(1); - cx.executor() - .timer(long_duration) - .with_timeout(short_duration, &cx.executor()) - .await - .expect_err("timeout should have triggered"); - - let fut = cx - .executor() - .timer(long_duration) - .with_timeout(short_duration, &cx.executor()); - cx.executor().advance_clock(short_duration * 2); - futures::FutureExt::now_or_never(fut) - .unwrap_or_else(|| panic!("timeout should have triggered")) - .expect_err("timeout"); - } -} diff --git a/crates/gpui_pre/src/view.rs b/crates/gpui_pre/src/view.rs deleted file mode 100644 index d2628d3..0000000 --- a/crates/gpui_pre/src/view.rs +++ /dev/null @@ -1,507 +0,0 @@ -use crate::{ - AnyElement, AnyEntity, AnyWeakEntity, App, Bounds, Context, Element, ElementId, Entity, - EntityId, GlobalElementId, InspectorElementId, IntoElement, LayoutId, PaintIndex, Pixels, - PrepaintStateIndex, Render, RenderOnce, Style, StyleRefinement, TextStyle, WeakEntity, -}; -use crate::{Empty, Window}; -use anyhow::Result; -use collections::FxHashSet; -use refineable::Refineable; -use std::mem; -use std::{any::TypeId, fmt, ops::Range}; - -/// A dynamically-typed view handle that can be downcast to a specific `Entity`. -/// -/// This is the type-erased counterpart to [`ViewElement`]: it holds an entity plus -/// a function pointer to its render, and is itself a [`View`], so embedding it as an -/// element goes through the same [`ViewElement`] machinery as any other view. -#[derive(Clone, Debug)] -pub struct AnyView { - entity: AnyEntity, - render: fn(&AnyView, &mut Window, &mut App) -> AnyElement, -} - -impl From> for AnyView { - fn from(value: Entity) -> Self { - AnyView { - entity: value.into_any(), - render: any_view::render::, - } - } -} - -impl AnyView { - /// Embed this view as a cached [`ViewElement`] laid out at `style`. - /// - /// The rendered subtree is recycled from the previous frame unless - /// [Context::notify] was called on the backing entity since it was rendered - /// (or [Window::refresh] is called, which ignores caching). - pub fn cached(self, style: StyleRefinement) -> ViewElement { - ViewElement::new(self).cached(style) - } - - /// Convert this to a weak handle. - pub fn downgrade(&self) -> AnyWeakView { - AnyWeakView { - entity: self.entity.downgrade(), - render: self.render, - } - } - - /// Convert this to a [Entity] of a specific type. - /// If this handle does not contain a view of the specified type, returns itself in an `Err` variant. - pub fn downcast(self) -> Result, Self> { - match self.entity.downcast() { - Ok(entity) => Ok(entity), - Err(entity) => Err(Self { - entity, - render: self.render, - }), - } - } - - /// Gets the [TypeId] of the underlying view. - pub fn entity_type(&self) -> TypeId { - self.entity.entity_type - } - - /// The [`EntityId`] of this view. - pub fn entity_id(&self) -> EntityId { - self.entity.entity_id() - } -} - -impl PartialEq for AnyView { - fn eq(&self, other: &Self) -> bool { - self.entity == other.entity - } -} - -impl Eq for AnyView {} - -/// `AnyView` is the type-erased [`View`]: its `render` is a function pointer rather -/// than a concrete type, but it participates in the reactive graph exactly like any -/// other view via [`ViewElement`]. -impl View for AnyView { - fn entity_id(&self) -> Option { - Some(self.entity.entity_id()) - } - - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - (self.render)(&self, window, cx) - } -} - -impl IntoElement for Entity { - type Element = ViewElement>; - - fn into_element(self) -> Self::Element { - ViewElement::new(self) - } -} - -impl IntoElement for AnyView { - type Element = ViewElement; - - fn into_element(self) -> Self::Element { - ViewElement::new(self) - } -} - -/// A weak, dynamically-typed view handle. -pub struct AnyWeakView { - entity: AnyWeakEntity, - render: fn(&AnyView, &mut Window, &mut App) -> AnyElement, -} - -impl AnyWeakView { - /// Upgrade to a strong `AnyView` handle, if the view is still alive. - pub fn upgrade(&self) -> Option { - let entity = self.entity.upgrade()?; - Some(AnyView { - entity, - render: self.render, - }) - } -} - -impl From> for AnyWeakView { - fn from(view: WeakEntity) -> Self { - AnyWeakView { - entity: view.into(), - render: any_view::render::, - } - } -} - -impl PartialEq for AnyWeakView { - fn eq(&self, other: &Self) -> bool { - self.entity == other.entity - } -} - -impl std::fmt::Debug for AnyWeakView { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("AnyWeakView") - .field("entity_id", &self.entity.entity_id) - .finish_non_exhaustive() - } -} - -mod any_view { - use crate::{AnyElement, AnyView, App, IntoElement, Render, Window}; - - pub(crate) fn render( - view: &AnyView, - window: &mut Window, - cx: &mut App, - ) -> AnyElement { - let view = view.clone().downcast::().unwrap(); - // Record the view's Render type name so the accessibility debug dump can - // attribute nodes to the view that produced them. - #[cfg(debug_assertions)] - window - .a11y - .view_type_names - .insert(view.entity_id(), std::any::type_name::()); - view.update(cx, |view, cx| view.render(window, cx).into_any_element()) - } -} - -/// A renderable that participates in GPUI's reactive graph — the unifying model -/// behind [`Render`] and [`RenderOnce`]. -/// -/// When `entity_id()` returns `Some`, that id becomes the view's identity: it gets -/// a unique element-id space (so internal `use_state` / `.id(..)` never collide -/// across siblings) and `cx.notify()` on that entity re-renders only this view's -/// subtree. `None` behaves like a stateless component. -/// -/// You rarely implement `View` directly. `Entity` and any `T: RenderOnce` -/// get a blanket impl below; implement it by hand only when a component needs both -/// parent-supplied props *and* a backing entity for identity. -pub trait View: 'static + Sized { - /// This view's identity, if it has one. A view typically holds the backing - /// entity as a field and returns its [`EntityId`] here. - /// - /// The id becomes this view's [`ElementId`], so two views keyed on the same - /// entity must not be rendered at the same position in the element tree - /// (e.g. as siblings under the same parent): their internal element state - /// (`use_state`, scroll offsets, etc.) would silently collide. Nesting is - /// fine — the id is scoped by the parent path. - fn entity_id(&self) -> Option; - - /// Render this view into an element tree, consuming `self`. - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement; -} - -/// A stateless component (`RenderOnce`) is a `View` with no identity. -impl View for T { - fn entity_id(&self) -> Option { - None - } - - #[inline] - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - RenderOnce::render(self, window, cx) - } -} - -/// An entity that renders itself (`Render`) is a `View` keyed on its own id. -impl View for Entity { - fn entity_id(&self) -> Option { - Some(Entity::entity_id(self)) - } - - #[inline] - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - self.update(cx, |this, cx| { - Render::render(this, window, cx).into_any_element() - }) - } -} - -impl Entity { - /// Embed this entity as a cached [`ViewElement`] laid out at `style`. - /// - /// The rendered subtree is reused until the entity is notified (or the - /// cached bounds / text style change). Caching requires a definite size: - /// a cached view is laid out from `style` and is *not* measured from its - /// contents. Use [`ViewElement::new`] (or `.child(entity)`) for the - /// uncached case. - #[track_caller] - pub fn cached(self, style: StyleRefinement) -> ViewElement> { - ViewElement::new(self).cached(style) - } -} - -/// The element type for [`View`] implementations. Wraps a `View` and hooks it -/// into layout, prepaint, and paint. Constructed via [`ViewElement::new`]. -#[doc(hidden)] -pub struct ViewElement { - view: Option, - entity_id: Option, - cached_style: Option, - #[cfg(debug_assertions)] - source: &'static core::panic::Location<'static>, -} - -impl ViewElement { - /// Wrap a [`View`] as an element. - #[track_caller] - pub fn new(view: V) -> Self { - let entity_id = view.entity_id(); - ViewElement { - entity_id, - cached_style: None, - view: Some(view), - #[cfg(debug_assertions)] - source: core::panic::Location::caller(), - } - } - - /// Enable caching of this view's rendered subtree, laid out at `style`. - /// The composer supplies the layout style because caching skips rendering - /// the contents to measure them. - /// - /// Crate-private on purpose: caching is only sound for entity-backed views, - /// where [`Context::notify`] is the contract that busts the cache. A stateless - /// view has no such contract, so a frozen subtree could never be invalidated. - /// Reach this through [`Entity::cached`] or [`AnyView::cached`], which are - /// entity-backed by construction. - pub(crate) fn cached(mut self, style: StyleRefinement) -> Self { - self.cached_style = Some(style); - self - } -} - -impl IntoElement for ViewElement { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -struct ViewElementState { - prepaint_range: Range, - paint_range: Range, - cache_key: ViewElementCacheKey, - accessed_entities: FxHashSet, -} - -struct ViewElementCacheKey { - bounds: Bounds, - content_mask: crate::ClipRegion, - text_style: TextStyle, -} - -impl Element for ViewElement { - type RequestLayoutState = Option; - type PrepaintState = Option; - - fn id(&self) -> Option { - self.entity_id.map(ElementId::View) - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - #[cfg(debug_assertions)] - return Some(self.source); - - #[cfg(not(debug_assertions))] - return None; - } - - fn request_layout( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - if let Some(entity_id) = self.entity_id { - // Stateful path: create a reactive boundary. - window.with_rendered_view(entity_id, |window| { - let caching_disabled = window.is_inspector_picking(cx); - match self.cached_style.as_ref() { - Some(style) if !caching_disabled => { - let mut root_style = Style::default(); - root_style.refine(style); - let layout_id = window.request_layout(root_style, None, cx); - (layout_id, None) - } - _ => { - let mut element = self - .view - .take() - .unwrap() - .render(window, cx) - .into_any_element(); - let layout_id = element.request_layout(window, cx); - (layout_id, Some(element)) - } - } - }) - } else { - // Stateless path: isolate subtree via type name (no entity identity). - window.with_id( - ElementId::Name(std::any::type_name::().into()), - |window| { - let mut element = self - .view - .take() - .unwrap() - .render(window, cx) - .into_any_element(); - let layout_id = element.request_layout(window, cx); - (layout_id, Some(element)) - }, - ) - } - } - - fn prepaint( - &mut self, - global_id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - element: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Option { - if let Some(entity_id) = self.entity_id { - // Stateful path. - window.set_view_id(entity_id); - window.with_rendered_view(entity_id, |window| { - if let Some(mut element) = element.take() { - element.prepaint(window, cx); - return Some(element); - } - - window.with_element_state::( - global_id.unwrap(), - |element_state, window| { - let content_mask = window.content_mask(); - let text_style = window.text_style(); - - if let Some(mut element_state) = element_state - && element_state.cache_key.bounds == bounds - && element_state.cache_key.content_mask == content_mask - && element_state.cache_key.text_style == text_style - && !window.dirty_views.contains(&entity_id) - && !window.refreshing - { - let prepaint_start = window.prepaint_index(); - window.reuse_prepaint(element_state.prepaint_range.clone()); - cx.entities - .extend_accessed(&element_state.accessed_entities); - let prepaint_end = window.prepaint_index(); - element_state.prepaint_range = prepaint_start..prepaint_end; - - return (None, element_state); - } - - let refreshing = mem::replace(&mut window.refreshing, true); - let prepaint_start = window.prepaint_index(); - let (mut element, accessed_entities) = cx.detect_accessed_entities(|cx| { - let mut element = self - .view - .take() - .unwrap() - .render(window, cx) - .into_any_element(); - element.layout_as_root(bounds.size.into(), window, cx); - element.prepaint_at(bounds.origin, window, cx); - element - }); - - let prepaint_end = window.prepaint_index(); - window.refreshing = refreshing; - - ( - Some(element), - ViewElementState { - accessed_entities, - prepaint_range: prepaint_start..prepaint_end, - paint_range: PaintIndex::default()..PaintIndex::default(), - cache_key: ViewElementCacheKey { - bounds, - content_mask, - text_style, - }, - }, - ) - }, - ) - }) - } else { - // Stateless path: just prepaint the element. - window.with_id( - ElementId::Name(std::any::type_name::().into()), - |window| { - element.as_mut().unwrap().prepaint(window, cx); - }, - ); - Some(element.take().unwrap()) - } - } - - fn paint( - &mut self, - global_id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - element: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - if let Some(entity_id) = self.entity_id { - // Stateful path. - window.with_rendered_view(entity_id, |window| { - let caching_disabled = window.is_inspector_picking(cx); - if self.cached_style.is_some() && !caching_disabled { - window.with_element_state::( - global_id.unwrap(), - |element_state, window| { - let mut element_state = element_state.unwrap(); - - let paint_start = window.paint_index(); - - if let Some(element) = element { - let refreshing = mem::replace(&mut window.refreshing, true); - element.paint(window, cx); - window.refreshing = refreshing; - } else { - window.reuse_paint(element_state.paint_range.clone()); - } - - let paint_end = window.paint_index(); - element_state.paint_range = paint_start..paint_end; - - ((), element_state) - }, - ) - } else { - element.as_mut().unwrap().paint(window, cx); - } - }); - } else { - // Stateless path: just paint the element. - window.with_id( - ElementId::Name(std::any::type_name::().into()), - |window| { - element.as_mut().unwrap().paint(window, cx); - }, - ); - } - } -} - -/// A view that renders nothing -pub struct EmptyView; - -impl Render for EmptyView { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - Empty - } -} diff --git a/crates/gpui_pre/src/window.rs b/crates/gpui_pre/src/window.rs deleted file mode 100644 index b4e23dd..0000000 --- a/crates/gpui_pre/src/window.rs +++ /dev/null @@ -1,8383 +0,0 @@ -#[cfg(feature = "profiler")] -use crate::profiler; -#[cfg(feature = "profiler")] -use crate::DebugFrameOverlayMode; -#[cfg(any(feature = "inspector", debug_assertions))] -use crate::Inspector; -use crate::{ - point, prelude::*, px, rems, size, transparent_black, Action, AnyDrag, AnyElement, - AnyImageCache, AnyTooltip, AnyView, App, AppContext, Arena, Asset, AsyncWindowContext, - AtlasTile, AvailableSpace, Background, BorderStyle, Bounds, BoxShadow, Capslock, Context, - Corners, CursorHideMode, CursorStyle, Decorations, DevicePixels, DispatchActionListener, - DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, - FileDropEvent, FontId, Global, GlobalElementId, GlyphId, GpuSpecs, Hsla, InputHandler, IsZero, - KeyBinding, KeyContext, KeyDownEvent, KeyEvent, Keystroke, KeystrokeEvent, LayoutId, - LineLayoutIndex, Modifiers, ModifiersChangedEvent, MonochromeSprite, MouseButton, MouseEvent, - MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, - PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, Priority, PromptButton, - PromptLevel, Quad, Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, - Replay, ResizeEdge, ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style, - SubpixelSprite, SubscriberSet, Subscription, SystemWindowTab, SystemWindowTabController, - TabStopMap, TaffyLayoutEngine, Task, TextInputConfiguration, TextInputStateChange, - TextRenderingMode, TextStyle, TextStyleRefinement, ThermalState, TransformationMatrix, - Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance, WindowBounds, - WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, - SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y, -}; - -use crate::gestures::{GestureTuning, RecognizedTouchGesture, TouchGestureRecognizer}; -use crate::interactive::TouchEvent; -use anyhow::{anyhow, Context as _, Result}; -use collections::{FxHashMap, FxHashSet}; -#[cfg(target_os = "macos")] -use core_video::pixel_buffer::CVPixelBuffer; -use derive_more::{Deref, DerefMut}; -use futures::channel::oneshot; -use futures::FutureExt; -use gpui_util::post_inc; -use gpui_util::{measure, ResultExt}; -use itertools::FoldWhile::{Continue, Done}; -use itertools::Itertools; -use parking_lot::RwLock; -use raw_window_handle::{HandleError, HasDisplayHandle, HasWindowHandle}; -use refineable::Refineable; -use scheduler::Instant; -use slotmap::SlotMap; -use smallvec::SmallVec; -use std::{ - any::{Any, TypeId}, - borrow::Cow, - cell::{Cell, RefCell}, - cmp, - fmt::{Debug, Display}, - hash::{Hash, Hasher}, - marker::PhantomData, - mem, - ops::{DerefMut, Range}, - rc::Rc, - sync::{ - atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, - Arc, Weak, - }, - time::Duration, -}; -use uuid::Uuid; - -pub(crate) mod a11y; -mod prompts; - -pub use a11y::A11ySubtreeBuilder; - -use self::a11y::A11y; -#[cfg(not(target_family = "wasm"))] -use self::a11y::ROOT_NODE_ID; -use crate::util::{ - atomic_incr_if_not_zero, ceil_to_device_pixel, floor_to_device_pixel, round_half_toward_zero, - round_half_toward_zero_f64, round_stroke_to_device_pixel, round_to_device_pixel, -}; -pub use prompts::*; - -/// Default window size used when no explicit size is provided. -pub const DEFAULT_WINDOW_SIZE: Size = size(px(1536.), px(1095.)); - -/// A 6:5 aspect ratio minimum window size to be used for functional, -/// additional-to-main-Zed windows, like the settings and rules library windows. -pub const DEFAULT_ADDITIONAL_WINDOW_SIZE: Size = Size { - width: Pixels(900.), - height: Pixels(750.), -}; - -/// Represents the two different phases when dispatching events. -#[derive(Default, Copy, Clone, Debug, Eq, PartialEq)] -pub enum DispatchPhase { - /// After the capture phase comes the bubble phase, in which mouse event listeners are - /// invoked front to back and keyboard event listeners are invoked from the focused element - /// to the root of the element tree. This is the phase you'll most commonly want to use when - /// registering event listeners. - #[default] - Bubble, - /// During the initial capture phase, mouse event listeners are invoked back to front, and keyboard - /// listeners are invoked from the root of the tree downward toward the focused element. This phase - /// is used for special purposes such as clearing the "pressed" state for click events. If - /// you stop event propagation during this phase, you need to know what you're doing. Handlers - /// outside of the immediate region may rely on detecting non-local events during this phase. - Capture, -} - -impl DispatchPhase { - /// Returns true if this represents the "bubble" phase. - #[inline] - pub fn bubble(self) -> bool { - self == DispatchPhase::Bubble - } - - /// Returns true if this represents the "capture" phase. - #[inline] - pub fn capture(self) -> bool { - self == DispatchPhase::Capture - } -} - -struct WindowInvalidatorInner { - #[cfg(feature = "profiler")] - pub window_id: WindowId, - pub dirty: bool, - pub draw_phase: DrawPhase, - pub dirty_views: FxHashSet, - pub update_count: usize, - #[cfg(feature = "profiler")] - pub frame_dirty: FrameDirtyAccumulator, - pub platform_waker: Option>, -} - -/// Per-frame invalidation bookkeeping, drained at draw time and emitted to the -/// frame profiler. Tracks when the current frame first became dirty and how -/// many invalidations were coalesced into it, whenever the profiler is -/// compiled in. Retention of the resulting per-frame records is what -/// `profiler::trace_enabled()` controls, not this measurement. -#[cfg(feature = "profiler")] -#[derive(Default)] -struct FrameDirtyAccumulator { - dirty_at: Option, - invalidations: u64, -} - -#[derive(Clone)] -pub(crate) struct WindowInvalidator { - inner: Rc>, -} - -impl WindowInvalidator { - pub fn new(#[allow(unused_variables)] window_id: WindowId) -> Self { - WindowInvalidator { - inner: Rc::new(RefCell::new(WindowInvalidatorInner { - #[cfg(feature = "profiler")] - window_id, - dirty: true, - draw_phase: DrawPhase::None, - dirty_views: FxHashSet::default(), - update_count: 0, - #[cfg(feature = "profiler")] - frame_dirty: FrameDirtyAccumulator::default(), - platform_waker: None, - })), - } - } - - pub fn invalidate_view(&self, entity: EntityId, cx: &mut App) -> bool { - let mut inner = self.inner.borrow_mut(); - inner.update_count += 1; - inner.dirty_views.insert(entity); - if inner.draw_phase == DrawPhase::None { - #[cfg(feature = "profiler")] - let dirty_at = Self::record_frame_dirty(&mut inner); - let became_dirty = !inner.dirty; - inner.dirty = true; - let waker = became_dirty.then(|| inner.platform_waker.clone()).flatten(); - #[cfg(feature = "profiler")] - let window_id = inner.window_id; - drop(inner); - #[cfg(feature = "profiler")] - if became_dirty { - profiler::journal::record_frame_pending(window_id, dirty_at); - } - cx.push_effect(Effect::Notify { emitter: entity }); - if let Some(waker) = waker { - waker(); - } - true - } else { - false - } - } - - pub fn is_dirty(&self) -> bool { - self.inner.borrow().dirty - } - - pub fn set_dirty(&self, dirty: bool) { - let mut inner = self.inner.borrow_mut(); - let became_dirty = dirty && !inner.dirty; - inner.dirty = dirty; - if dirty { - inner.update_count += 1; - } - #[cfg(feature = "profiler")] - let dirty_at = dirty.then(|| Self::record_frame_dirty(&mut inner)); - let waker = became_dirty.then(|| inner.platform_waker.clone()).flatten(); - #[cfg(feature = "profiler")] - let window_id = inner.window_id; - drop(inner); - #[cfg(feature = "profiler")] - if became_dirty && let Some(dirty_at) = dirty_at { - profiler::journal::record_frame_pending(window_id, dirty_at); - } - if let Some(waker) = waker { - waker(); - } - } - - pub fn set_platform_waker(&self, waker: Option>) { - let mut inner = self.inner.borrow_mut(); - inner.platform_waker = waker; - let waker = inner.dirty.then(|| inner.platform_waker.clone()).flatten(); - drop(inner); - if let Some(waker) = waker { - waker(); - } - } - - /// Wakes the platform's frame-request source so a frame request is - /// delivered even if the platform stops requesting frames for idle - /// windows. No-op on platforms without a frame waker. - pub fn wake_platform(&self) { - let waker = self.inner.borrow().platform_waker.clone(); - if let Some(waker) = waker { - waker(); - } - } - - pub fn set_phase(&self, phase: DrawPhase) { - self.inner.borrow_mut().draw_phase = phase - } - - pub fn update_count(&self) -> usize { - self.inner.borrow().update_count - } - - #[cfg(feature = "profiler")] - fn record_frame_dirty(inner: &mut WindowInvalidatorInner) -> Instant { - let dirty_at = *inner.frame_dirty.dirty_at.get_or_insert_with(Instant::now); - inner.frame_dirty.invalidations += 1; - dirty_at - } - - #[cfg(feature = "profiler")] - fn take_frame_dirty(&self) -> FrameDirtyAccumulator { - mem::take(&mut self.inner.borrow_mut().frame_dirty) - } - - pub fn take_views(&self) -> FxHashSet { - mem::take(&mut self.inner.borrow_mut().dirty_views) - } - - pub fn replace_views(&self, views: FxHashSet) { - self.inner.borrow_mut().dirty_views = views; - } - - pub fn not_drawing(&self) -> bool { - self.inner.borrow().draw_phase == DrawPhase::None - } - - #[track_caller] - pub fn debug_assert_paint(&self) { - debug_assert!( - matches!(self.inner.borrow().draw_phase, DrawPhase::Paint), - "this method can only be called during paint" - ); - } - - #[track_caller] - pub fn debug_assert_prepaint(&self) { - debug_assert!( - matches!(self.inner.borrow().draw_phase, DrawPhase::Prepaint), - "this method can only be called during request_layout, or prepaint" - ); - } - - #[track_caller] - pub fn debug_assert_paint_or_prepaint(&self) { - debug_assert!( - matches!( - self.inner.borrow().draw_phase, - DrawPhase::Paint | DrawPhase::Prepaint - ), - "this method can only be called during request_layout, prepaint, or paint" - ); - } -} - -type AnyObserver = Box bool + 'static>; - -pub(crate) type AnyWindowFocusListener = - Box bool + 'static>; - -pub(crate) struct WindowFocusEvent { - pub(crate) previous_focus_path: SmallVec<[FocusId; 8]>, - pub(crate) current_focus_path: SmallVec<[FocusId; 8]>, -} - -impl WindowFocusEvent { - pub fn is_focus_in(&self, focus_id: FocusId) -> bool { - !self.previous_focus_path.contains(&focus_id) && self.current_focus_path.contains(&focus_id) - } - - pub fn is_focus_out(&self, focus_id: FocusId) -> bool { - self.previous_focus_path.contains(&focus_id) && !self.current_focus_path.contains(&focus_id) - } -} - -/// This is provided when subscribing for `Context::on_focus_out` events. -pub struct FocusOutEvent { - /// A weak focus handle representing what was blurred. - pub blurred: WeakFocusHandle, -} - -slotmap::new_key_type! { - /// A globally unique identifier for a focusable element. - pub struct FocusId; -} - -thread_local! { - /// Fallback arena used when no app-specific arena is active. - /// In production, each window draw sets CURRENT_ELEMENT_ARENA to the app's arena. - pub(crate) static ELEMENT_ARENA: RefCell = RefCell::new(Arena::new(1024 * 1024)); - - /// Points to the current App's element arena during draw operations. - /// This allows multiple test Apps to have isolated arenas, preventing - /// cross-session corruption when the scheduler interleaves their tasks. - static CURRENT_ELEMENT_ARENA: Cell>> = const { Cell::new(None) }; -} - -/// Whether a window draw is currently in progress on this thread. -/// -/// This holds exactly while an `ElementArenaScope` is active: nested scopes -/// restore the previous (still set) arena pointer, so `CURRENT_ELEMENT_ARENA` -/// is `Some` from the outermost draw's start to its end. -/// -/// The `on_request_frame` callback uses this to defer draw requests that -/// arrive re-entrantly while a draw is already on the stack (e.g. via nested -/// message pumping in the Windows window procedure), instead of running a -/// nested draw or panicking on the already-borrowed App. -fn draw_in_progress() -> bool { - CURRENT_ELEMENT_ARENA.with(|current| current.get().is_some()) -} - -/// Allocates an element in the current arena. Uses the app-specific arena if one -/// is active (during draw), otherwise falls back to the thread-local ELEMENT_ARENA. -pub(crate) fn with_element_arena(f: impl FnOnce(&mut Arena) -> R) -> R { - CURRENT_ELEMENT_ARENA.with(|current| { - if let Some(arena_ptr) = current.get() { - // SAFETY: The pointer is valid for the duration of the draw operation - // that set it, and we're being called during that same draw. - let arena_cell = unsafe { &*arena_ptr }; - f(&mut arena_cell.borrow_mut()) - } else { - ELEMENT_ARENA.with_borrow_mut(f) - } - }) -} - -/// Scope guard that sets CURRENT_ELEMENT_ARENA for the duration of a draw -/// operation and tracks the arena's scope depth, so that a nested draw's -/// `ArenaClearNeeded::clear` is deferred rather than freeing memory the outer -/// draw still references (see `Arena::clear`). -/// -/// Call [`ElementArenaScope::exit`] with the same arena that was entered to -/// obtain the [`ArenaClearNeeded`] token the draw now owes; requiring `exit` -/// makes it impossible to request a clear before the scope has ended. The -/// scope's teardown — restoring the thread-local and balancing `begin_scope` -/// with `end_scope` — happens in `Drop`, so the arena's scope depth stays -/// balanced on every path, including when a panic unwinds a draw before `exit` -/// is reached. (If teardown lived only in `exit`, such a panic would leave the -/// scope depth permanently elevated and defer every future clear, leaking -/// memory unboundedly.) -pub(crate) struct ElementArenaScope { - /// The entered arena: compared against the argument in `exit`, and - /// dereferenced in `Drop` to end its scope (see the SAFETY note there). - entered: *const RefCell, - previous: Option<*const RefCell>, - exited: bool, -} - -impl ElementArenaScope { - /// Enter a scope where element allocations use the given arena. - pub(crate) fn enter(arena: &RefCell) -> Self { - arena.borrow_mut().begin_scope(); - let previous = CURRENT_ELEMENT_ARENA.with(|current| { - let prev = current.get(); - current.set(Some(arena as *const RefCell)); - prev - }); - Self { - entered: arena as *const RefCell, - previous, - exited: false, - } - } - - /// End the scope: restores the previously-current arena and ends the - /// arena's clear-deferral scope. Returns the token for the arena clear the - /// draw now owes; producing it here makes it impossible to request a clear - /// before the scope has ended (which would be silently deferred forever). - /// - /// Panics if passed a different arena than was entered: ending the scope - /// of the wrong arena would unbalance two arenas' scope depths, allowing - /// one of them to clear while a draw still references its memory. - pub(crate) fn exit(mut self, arena: &RefCell) -> ArenaClearNeeded { - assert!( - std::ptr::eq(self.entered, arena), - "ElementArenaScope::exit called with a different arena than was entered" - ); - self.exited = true; - // Teardown (restoring the thread-local and ending the arena's - // clear-deferral scope) runs in `Drop`, which fires both here — `self` - // is dropped as `exit` returns, before the token reaches the caller — - // and when a panic unwinds the draw before `exit` is reached. - ArenaClearNeeded::new(arena) - } -} - -impl Drop for ElementArenaScope { - fn drop(&mut self) { - // Teardown lives here (rather than in `exit`) so it runs exactly once on - // every path: `exit` consumes and drops the guard on the normal path, - // and unwinding drops it on the panic path. Balancing `begin_scope` here - // keeps the arena's scope depth correct even when a draw panics; if this - // only happened in `exit`, a panic between `enter` and `exit` would leave - // the depth elevated and defer every future clear. - CURRENT_ELEMENT_ARENA.with(|current| { - current.set(self.previous); - }); - // SAFETY: `entered` came from a `&RefCell` in `enter`, and the - // arena (owned by the `App` being drawn) outlives this guard on both the - // normal and unwinding paths, since the guard is a local of the draw. - unsafe { &*self.entered }.borrow_mut().end_scope(); - if !self.exited && !std::thread::panicking() { - debug_assert!(false, "ElementArenaScope dropped without calling exit()"); - log::error!( - "ElementArenaScope dropped without calling exit(); \ - the arena clear for this draw was never requested" - ); - } - } -} - -/// Returned when the element arena has been used and so must be cleared before the next draw. -#[must_use] -pub struct ArenaClearNeeded { - /// Identity of the arena that was drawn into. Only ever compared against - /// another pointer in `clear`; never dereferenced. - arena: *const RefCell, -} - -impl ArenaClearNeeded { - /// Create a new ArenaClearNeeded token for the App whose arena was drawn - /// into. Private: the only way to obtain one is [`ElementArenaScope::exit`]. - fn new(arena: &RefCell) -> Self { - Self { - arena: arena as *const RefCell, - } - } - - /// Clear the element arena of the App the draw ran against. If an enclosing - /// draw is still in progress (this draw was nested inside it), the clear is - /// deferred to the enclosing draw's own `ArenaClearNeeded` so that its live - /// allocations aren't freed. - /// - /// Panics if passed a different App than the draw ran against, since - /// clearing another App's arena could free memory its draws still - /// reference. - pub fn clear(self, cx: &mut App) { - assert!( - std::ptr::eq(self.arena, &cx.element_arena), - "ArenaClearNeeded::clear called with a different App than the draw ran against" - ); - cx.element_arena.borrow_mut().clear(); - } -} - -pub(crate) type FocusMap = RwLock>; -pub(crate) struct FocusRef { - pub(crate) ref_count: AtomicUsize, - pub(crate) tab_index: isize, - pub(crate) tab_stop: bool, -} - -impl FocusId { - /// Obtains whether the element associated with this handle is currently focused. - pub fn is_focused(&self, window: &Window) -> bool { - window.focus == Some(*self) - } - - /// Obtains whether the element associated with this handle contains the focused - /// element or is itself focused. - pub fn contains_focused(&self, window: &Window, cx: &App) -> bool { - window - .focused(cx) - .is_some_and(|focused| self.contains(focused.id, window)) - } - - /// Obtains whether the element associated with this handle is contained within the - /// focused element or is itself focused. - pub fn within_focused(&self, window: &Window, cx: &App) -> bool { - let focused = window.focused(cx); - focused.is_some_and(|focused| focused.id.contains(*self, window)) - } - - /// Obtains whether this handle contains the given handle in the most recently rendered frame. - pub(crate) fn contains(&self, other: Self, window: &Window) -> bool { - window - .rendered_frame - .dispatch_tree - .focus_contains(*self, other) - } -} - -/// A handle which can be used to track and manipulate the focused element in a window. -pub struct FocusHandle { - pub(crate) id: FocusId, - handles: Arc, - /// The index of this element in the tab order. - pub tab_index: isize, - /// Whether this element can be focused by tab navigation. - pub tab_stop: bool, -} - -impl std::fmt::Debug for FocusHandle { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("FocusHandle({:?})", self.id)) - } -} - -impl FocusHandle { - pub(crate) fn new(handles: &Arc) -> Self { - let id = handles.write().insert(FocusRef { - ref_count: AtomicUsize::new(1), - tab_index: 0, - tab_stop: false, - }); - - Self { - id, - tab_index: 0, - tab_stop: false, - handles: handles.clone(), - } - } - - pub(crate) fn for_id(id: FocusId, handles: &Arc) -> Option { - let lock = handles.read(); - let focus = lock.get(id)?; - if atomic_incr_if_not_zero(&focus.ref_count) == 0 { - return None; - } - Some(Self { - id, - tab_index: focus.tab_index, - tab_stop: focus.tab_stop, - handles: handles.clone(), - }) - } - - /// Sets the tab index of the element associated with this handle. - pub fn tab_index(mut self, index: isize) -> Self { - self.tab_index = index; - if let Some(focus) = self.handles.write().get_mut(self.id) { - focus.tab_index = index; - } - self - } - - /// Sets whether the element associated with this handle is a tab stop. - /// - /// When `false`, the element will not be included in the tab order. - pub fn tab_stop(mut self, tab_stop: bool) -> Self { - self.tab_stop = tab_stop; - if let Some(focus) = self.handles.write().get_mut(self.id) { - focus.tab_stop = tab_stop; - } - self - } - - /// Converts this focus handle into a weak variant, which does not prevent it from being released. - pub fn downgrade(&self) -> WeakFocusHandle { - WeakFocusHandle { - id: self.id, - handles: Arc::downgrade(&self.handles), - } - } - - /// Moves the focus to the element associated with this handle. - pub fn focus(&self, window: &mut Window, cx: &mut App) { - window.focus(self, cx) - } - - /// Obtains whether the element associated with this handle is currently focused. - pub fn is_focused(&self, window: &Window) -> bool { - self.id.is_focused(window) - } - - /// Obtains whether the element associated with this handle contains the focused - /// element or is itself focused. - pub fn contains_focused(&self, window: &Window, cx: &App) -> bool { - self.id.contains_focused(window, cx) - } - - /// Obtains whether the element associated with this handle is contained within the - /// focused element or is itself focused. - pub fn within_focused(&self, window: &Window, cx: &mut App) -> bool { - self.id.within_focused(window, cx) - } - - /// Obtains whether this handle contains the given handle in the most recently rendered frame. - pub fn contains(&self, other: &Self, window: &Window) -> bool { - self.id.contains(other.id, window) - } - - /// Dispatch an action on the element that rendered this focus handle - pub fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut App) { - if let Some(node_id) = window - .rendered_frame - .dispatch_tree - .focusable_node_id(self.id) - { - window.dispatch_action_on_node(node_id, action, cx) - } - } -} - -impl Clone for FocusHandle { - fn clone(&self) -> Self { - Self::for_id(self.id, &self.handles).unwrap() - } -} - -impl PartialEq for FocusHandle { - fn eq(&self, other: &Self) -> bool { - self.id == other.id - } -} - -impl Eq for FocusHandle {} - -impl Drop for FocusHandle { - fn drop(&mut self) { - self.handles - .read() - .get(self.id) - .unwrap() - .ref_count - .fetch_sub(1, SeqCst); - } -} - -/// A weak reference to a focus handle. -#[derive(Clone, Debug)] -pub struct WeakFocusHandle { - pub(crate) id: FocusId, - pub(crate) handles: Weak, -} - -impl WeakFocusHandle { - /// Attempts to upgrade the [WeakFocusHandle] to a [FocusHandle]. - pub fn upgrade(&self) -> Option { - let handles = self.handles.upgrade()?; - FocusHandle::for_id(self.id, &handles) - } -} - -impl PartialEq for WeakFocusHandle { - fn eq(&self, other: &WeakFocusHandle) -> bool { - self.id == other.id - } -} - -impl Eq for WeakFocusHandle {} - -impl PartialEq for WeakFocusHandle { - fn eq(&self, other: &FocusHandle) -> bool { - self.id == other.id - } -} - -impl PartialEq for FocusHandle { - fn eq(&self, other: &WeakFocusHandle) -> bool { - self.id == other.id - } -} - -/// Focusable allows users of your view to easily -/// focus it (using window.focus_view(cx, view)) -pub trait Focusable: 'static { - /// Returns the focus handle associated with this view. - fn focus_handle(&self, cx: &App) -> FocusHandle; -} - -impl Focusable for Entity { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.read(cx).focus_handle(cx) - } -} - -/// ManagedView is a view (like a Modal, Popover, Menu, etc.) -/// where the lifecycle of the view is handled by another view. -pub trait ManagedView: Focusable + EventEmitter + Render {} - -impl + Render> ManagedView for M {} - -/// Emitted by implementers of [`ManagedView`] to indicate the view should be dismissed, such as when a view is presented as a modal. -pub struct DismissEvent; - -type FrameCallback = Box; - -pub(crate) type AnyMouseListener = - Box; - -#[derive(Clone)] -pub(crate) struct CursorStyleRequest { - pub(crate) hitbox_id: Option, - pub(crate) style: CursorStyle, -} - -#[derive(Default, Eq, PartialEq)] -pub(crate) struct HitTest { - pub(crate) ids: SmallVec<[HitboxId; 8]>, - pub(crate) hover_hitbox_count: usize, -} - -/// A type of window control area that corresponds to the platform window. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum WindowControlArea { - /// An area that allows dragging of the platform window. - Drag, - /// An area that allows closing of the platform window. - Close, - /// An area that allows maximizing of the platform window. - Max, - /// An area that allows minimizing of the platform window. - Min, -} - -/// An identifier for a [Hitbox] which also includes [HitboxBehavior]. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] -pub struct HitboxId(u64); - -#[cfg(feature = "test-support")] -impl HitboxId { - /// A placeholder HitboxId exclusively for integration testing API's that - /// need a hitbox but where the value of the hitbox does not matter. The - /// alternative is to make the Hitbox optional but that complicates the - /// implementation. - pub const fn placeholder() -> Self { - Self(0) - } -} - -impl HitboxId { - /// Checks if the hitbox with this ID is currently hovered. Returns `false` during keyboard - /// input modality so that keyboard navigation suppresses hover highlights. Except when handling - /// `ScrollWheelEvent`, this is typically what you want when determining whether to handle mouse - /// events or paint hover styles. - /// - /// See [`Hitbox::is_hovered`] for details. - pub fn is_hovered(self, window: &Window) -> bool { - // If this hitbox has captured the pointer, it's always considered hovered - if window.captured_hitbox == Some(self) { - return true; - } - if window.last_input_was_keyboard() { - return false; - } - self.hit_test(window) - } - - /// Checks if the hitbox with this ID is currently hovered, regardless of the last - /// input modality used. - /// - /// See [`HitboxId::is_hovered`] for more details. - pub(crate) fn is_hovered_ignoring_last_input(self, window: &Window) -> bool { - // If this hitbox has captured the pointer, it's always considered hovered - if window.captured_hitbox == Some(self) { - return true; - } - self.hit_test(window) - } - - fn hit_test(self, window: &Window) -> bool { - let hit_test = &window.mouse_hit_test; - for id in hit_test.ids.iter().take(hit_test.hover_hitbox_count) { - if self == *id { - return true; - } - } - false - } - - /// Checks if the hitbox with this ID contains the mouse and should handle scroll events. - /// Typically this should only be used when handling `ScrollWheelEvent`, and otherwise - /// `is_hovered` should be used. See the documentation of `Hitbox::is_hovered` for details about - /// this distinction. - pub fn should_handle_scroll(self, window: &Window) -> bool { - window.mouse_hit_test.ids.contains(&self) - } - - fn next(mut self) -> HitboxId { - HitboxId(self.0.wrapping_add(1)) - } -} - -/// A rectangular region that potentially blocks hitboxes inserted prior. -/// See [Window::insert_hitbox] for more details. -#[derive(Clone, Debug, Deref)] -pub struct Hitbox { - /// A unique identifier for the hitbox. - pub id: HitboxId, - /// The bounds of the hitbox. - #[deref] - pub bounds: Bounds, - /// The content mask when the hitbox was inserted. - pub content_mask: crate::ClipRegion, - /// Flags that specify hitbox behavior. - pub behavior: HitboxBehavior, -} - -impl Hitbox { - /// Checks if the hitbox is currently hovered. Returns `false` during keyboard input modality - /// so that keyboard navigation suppresses hover highlights. Except when handling - /// `ScrollWheelEvent`, this is typically what you want when determining whether to handle mouse - /// events or paint hover styles. - /// - /// This can return `false` even when the hitbox contains the mouse, if a hitbox in front of - /// this sets `HitboxBehavior::BlockMouse` (`InteractiveElement::occlude`) or - /// `HitboxBehavior::BlockMouseExceptScroll` (`InteractiveElement::block_mouse_except_scroll`), - /// or if the current input modality is keyboard (see [`Window::last_input_was_keyboard`]). - /// - /// Handling of `ScrollWheelEvent` should typically use `should_handle_scroll` instead. - /// Concretely, this is due to use-cases like overlays that cause the elements under to be - /// non-interactive while still allowing scrolling. More abstractly, this is because - /// `is_hovered` is about element interactions directly under the mouse - mouse moves, clicks, - /// hover styling, etc. In contrast, scrolling is about finding the current outer scrollable - /// container. - pub fn is_hovered(&self, window: &Window) -> bool { - self.id.is_hovered(window) - } - - /// Checks whether this hitbox would be hovered at `position`, regardless of the current input - /// modality or mouse position. - pub fn is_hovered_at(&self, position: Point, window: &Window) -> bool { - let hit_test = window.rendered_frame.hit_test(position); - hit_test - .ids - .iter() - .take(hit_test.hover_hitbox_count) - .any(|id| self.id == *id) - } - - /// Checks if the hitbox contains the mouse and should handle scroll events. Typically this - /// should only be used when handling `ScrollWheelEvent`, and otherwise `is_hovered` should be - /// used. See the documentation of `Hitbox::is_hovered` for details about this distinction. - /// - /// This can return `false` even when the hitbox contains the mouse, if a hitbox in front of - /// this sets `HitboxBehavior::BlockMouse` (`InteractiveElement::occlude`). - pub fn should_handle_scroll(&self, window: &Window) -> bool { - self.id.should_handle_scroll(window) - } -} - -/// How the hitbox affects mouse behavior. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -pub enum HitboxBehavior { - /// Normal hitbox mouse behavior, doesn't affect mouse handling for other hitboxes. - #[default] - Normal, - - /// All hitboxes behind this hitbox will be ignored and so will have `hitbox.is_hovered() == - /// false` and `hitbox.should_handle_scroll() == false`. Typically for elements this causes - /// skipping of all mouse events, hover styles, and tooltips. This flag is set by - /// [`InteractiveElement::occlude`]. - /// - /// For mouse handlers that check those hitboxes, this behaves the same as registering a - /// bubble-phase handler for every mouse event type: - /// - /// ```ignore - /// window.on_mouse_event(move |_: &EveryMouseEventTypeHere, phase, window, cx| { - /// if phase == DispatchPhase::Capture && hitbox.is_hovered(window) { - /// cx.stop_propagation(); - /// } - /// }) - /// ``` - /// - /// This has effects beyond event handling - any use of hitbox checking, such as hover - /// styles and tooltips. These other behaviors are the main point of this mechanism. An - /// alternative might be to not affect mouse event handling - but this would allow - /// inconsistent UI where clicks and moves interact with elements that are not considered to - /// be hovered. - BlockMouse, - - /// All hitboxes behind this hitbox will have `hitbox.is_hovered() == false`, even when - /// `hitbox.should_handle_scroll() == true`. Typically for elements this causes all mouse - /// interaction except scroll events to be ignored - see the documentation of - /// [`Hitbox::is_hovered`] for details. This flag is set by - /// [`InteractiveElement::block_mouse_except_scroll`]. - /// - /// For mouse handlers that check those hitboxes, this behaves the same as registering a - /// bubble-phase handler for every mouse event type **except** `ScrollWheelEvent`: - /// - /// ```ignore - /// window.on_mouse_event(move |_: &EveryMouseEventTypeExceptScroll, phase, window, cx| { - /// if phase == DispatchPhase::Bubble && hitbox.should_handle_scroll(window) { - /// cx.stop_propagation(); - /// } - /// }) - /// ``` - /// - /// See the documentation of [`Hitbox::is_hovered`] for details of why `ScrollWheelEvent` is - /// handled differently than other mouse events. If also blocking these scroll events is - /// desired, then a `cx.stop_propagation()` handler like the one above can be used. - /// - /// This has effects beyond event handling - this affects any use of `is_hovered`, such as - /// hover styles and tooltips. These other behaviors are the main point of this mechanism. - /// An alternative might be to not affect mouse event handling - but this would allow - /// inconsistent UI where clicks and moves interact with elements that are not considered to - /// be hovered. - BlockMouseExceptScroll, -} - -/// An identifier for a tooltip. -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] -pub struct TooltipId(usize); - -impl TooltipId { - /// Checks if the tooltip is currently hovered. - pub fn is_hovered(&self, window: &Window) -> bool { - window - .tooltip_bounds - .as_ref() - .is_some_and(|tooltip_bounds| { - tooltip_bounds.id == *self - && tooltip_bounds.bounds.contains(&window.mouse_position()) - }) - } -} - -pub(crate) struct TooltipBounds { - id: TooltipId, - bounds: Bounds, -} - -#[derive(Clone)] -pub(crate) struct TooltipRequest { - id: TooltipId, - tooltip: AnyTooltip, -} - -pub(crate) struct DeferredDraw { - current_view: EntityId, - priority: usize, - parent_node: DispatchNodeId, - element_id_stack: SmallVec<[ElementId; 32]>, - text_style_stack: Vec, - content_mask: Option, - rem_size: Pixels, - element: Option, - absolute_offset: Point, - prepaint_range: Range, - paint_range: Range, -} - -pub(crate) struct Frame { - pub(crate) focus: Option, - pub(crate) window_active: bool, - pub(crate) element_states: FxHashMap<(GlobalElementId, TypeId), ElementStateBox>, - accessed_element_states: Vec<(GlobalElementId, TypeId)>, - pub(crate) mouse_listeners: Vec>, - pub(crate) dispatch_tree: DispatchTree, - pub(crate) scene: Scene, - pub(crate) hitboxes: Vec, - pub(crate) window_control_hitboxes: Vec<(WindowControlArea, Hitbox)>, - pub(crate) deferred_draws: Vec, - pub(crate) input_handlers: Vec>, - pub(crate) tooltip_requests: Vec>, - pub(crate) cursor_styles: Vec, - #[cfg(any(test, feature = "test-support"))] - pub(crate) debug_bounds: FxHashMap>, - #[cfg(any(feature = "inspector", debug_assertions))] - pub(crate) next_inspector_instance_ids: FxHashMap, usize>, - #[cfg(any(feature = "inspector", debug_assertions))] - pub(crate) inspector_hitboxes: FxHashMap, - pub(crate) tab_stops: TabStopMap, -} - -#[derive(Clone, Default)] -pub(crate) struct PrepaintStateIndex { - hitboxes_index: usize, - tooltips_index: usize, - deferred_draws_index: usize, - dispatch_tree_index: usize, - accessed_element_states_index: usize, - line_layout_index: LineLayoutIndex, -} - -#[derive(Clone, Default)] -pub(crate) struct PaintIndex { - scene_index: usize, - mouse_listeners_index: usize, - input_handlers_index: usize, - cursor_styles_index: usize, - accessed_element_states_index: usize, - tab_handle_index: usize, - line_layout_index: LineLayoutIndex, -} - -impl Frame { - pub(crate) fn new(dispatch_tree: DispatchTree) -> Self { - Frame { - focus: None, - window_active: false, - element_states: FxHashMap::default(), - accessed_element_states: Vec::new(), - mouse_listeners: Vec::new(), - dispatch_tree, - scene: Scene::default(), - hitboxes: Vec::new(), - window_control_hitboxes: Vec::new(), - deferred_draws: Vec::new(), - input_handlers: Vec::new(), - tooltip_requests: Vec::new(), - cursor_styles: Vec::new(), - - #[cfg(any(test, feature = "test-support"))] - debug_bounds: FxHashMap::default(), - - #[cfg(any(feature = "inspector", debug_assertions))] - next_inspector_instance_ids: FxHashMap::default(), - - #[cfg(any(feature = "inspector", debug_assertions))] - inspector_hitboxes: FxHashMap::default(), - tab_stops: TabStopMap::default(), - } - } - - pub(crate) fn clear(&mut self) { - self.element_states.clear(); - self.accessed_element_states.clear(); - self.mouse_listeners.clear(); - self.dispatch_tree.clear(); - self.scene.clear(); - self.input_handlers.clear(); - self.tooltip_requests.clear(); - self.cursor_styles.clear(); - self.hitboxes.clear(); - self.window_control_hitboxes.clear(); - self.deferred_draws.clear(); - self.tab_stops.clear(); - self.focus = None; - - #[cfg(any(test, feature = "test-support"))] - { - self.debug_bounds.clear(); - } - - #[cfg(any(feature = "inspector", debug_assertions))] - { - self.next_inspector_instance_ids.clear(); - self.inspector_hitboxes.clear(); - } - } - - pub(crate) fn cursor_style(&self, window: &Window) -> Option { - self.cursor_styles - .iter() - .rev() - .fold_while(None, |style, request| match request.hitbox_id { - None => Done(Some(request.style)), - Some(hitbox_id) => Continue(style.or_else(|| { - hitbox_id - .is_hovered_ignoring_last_input(window) - .then_some(request.style) - })), - }) - .into_inner() - } - - pub(crate) fn hit_test(&self, position: Point) -> HitTest { - let mut set_hover_hitbox_count = false; - let mut hit_test = HitTest::default(); - for hitbox in self.hitboxes.iter().rev() { - let bounds = hitbox.bounds.intersect(&hitbox.content_mask.bounds); - if bounds.contains(&position) { - hit_test.ids.push(hitbox.id); - if !set_hover_hitbox_count - && hitbox.behavior == HitboxBehavior::BlockMouseExceptScroll - { - hit_test.hover_hitbox_count = hit_test.ids.len(); - set_hover_hitbox_count = true; - } - if hitbox.behavior == HitboxBehavior::BlockMouse { - break; - } - } - } - if !set_hover_hitbox_count { - hit_test.hover_hitbox_count = hit_test.ids.len(); - } - hit_test - } - - pub(crate) fn focus_path(&self) -> SmallVec<[FocusId; 8]> { - self.focus - .map(|focus_id| self.dispatch_tree.focus_path(focus_id)) - .unwrap_or_default() - } - - pub(crate) fn finish(&mut self, prev_frame: &mut Self) { - for element_state_key in &self.accessed_element_states { - if let Some((element_state_key, element_state)) = - prev_frame.element_states.remove_entry(element_state_key) - { - self.element_states.insert(element_state_key, element_state); - } - } - - self.scene.finish(); - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] -enum InputModality { - Mouse, - Keyboard, - Touch, -} - -/// Holds the state for a specific window. -pub struct Window { - pub(crate) handle: AnyWindowHandle, - pub(crate) invalidator: WindowInvalidator, - pub(crate) removed: bool, - pub(crate) platform_window: Box, - display_id: Option, - is_resizable: bool, - is_minimizable: bool, - sprite_atlas: Arc, - text_system: Arc, - text_rendering_mode: Rc>, - rem_size: Pixels, - /// The stack of override values for the window's rem size. - /// - /// This is used by `with_rem_size` to allow rendering an element tree with - /// a given rem size. - rem_size_override_stack: SmallVec<[Pixels; 8]>, - pub(crate) viewport_size: Size, - layout_engine: Option, - pub(crate) root: Option, - pub(crate) element_id_stack: SmallVec<[ElementId; 32]>, - pub(crate) text_style_stack: Vec, - pub(crate) rendered_entity_stack: Vec, - pub(crate) element_offset_stack: Vec>, - pub(crate) element_opacity: f32, - pub(crate) content_mask_stack: Vec, - pub(crate) requested_autoscroll: Option>, - /// The [`TextInputConfiguration`] most recently forwarded to the platform - /// window, so that only actual changes are forwarded (reconfiguring a live - /// input session can restart the IME connection). - last_text_input_configuration: Option, - focused_text_input_active: bool, - pub(crate) image_cache_stack: Vec, - pub(crate) rendered_frame: Frame, - pub(crate) next_frame: Frame, - next_hitbox_id: HitboxId, - pub(crate) next_tooltip_id: TooltipId, - pub(crate) tooltip_bounds: Option, - pub(crate) next_frame_callbacks: Rc>>, - pub(crate) dirty_views: FxHashSet, - focus_listeners: SubscriberSet<(), AnyWindowFocusListener>, - pub(crate) focus_lost_listeners: SubscriberSet<(), AnyObserver>, - focus_lost_path: SmallVec<[FocusId; 8]>, - default_prevented: bool, - mouse_position: Point, - mouse_hit_test: HitTest, - modifiers: Modifiers, - capslock: Capslock, - scale_factor: f32, - pub(crate) bounds_observers: SubscriberSet<(), AnyObserver>, - appearance: WindowAppearance, - pub(crate) appearance_observers: SubscriberSet<(), AnyObserver>, - pub(crate) button_layout_observers: SubscriberSet<(), AnyObserver>, - active: Rc>, - hovered: Rc>, - pub(crate) needs_present: Rc>, - /// Tracks recent input event timestamps to determine if input is arriving at a high rate. - /// Used to selectively enable VRR optimization only when input rate exceeds 60fps. - pub(crate) input_rate_tracker: Rc>, - #[cfg(feature = "profiler")] - window_profiler: profiler::WindowProfiler, - last_input_modality: InputModality, - touch_gestures: TouchGestureRecognizer, - touch_prediction_enabled: bool, - long_press_timer: Option>, - long_press_capture: Option, - pub(crate) refreshing: bool, - pub(crate) activation_observers: SubscriberSet<(), AnyObserver>, - pub(crate) focus: Option, - focus_enabled: bool, - /// Incremented every time focus moves. Used to invalidate a - /// pending keyboard activation state when focus changes. - pub(crate) focus_generation: u64, - pending_input: Option, - pending_modifier: ModifierState, - pub(crate) pending_input_observers: SubscriberSet<(), AnyObserver>, - prompt: Option, - pub(crate) client_inset: Option, - /// The hitbox that has captured the pointer, if any. - /// While captured, mouse events route to this hitbox regardless of hit testing. - captured_hitbox: Option, - #[cfg(any(feature = "inspector", debug_assertions))] - inspector: Option>, - #[cfg(feature = "profiler")] - debug_frame_overlay: crate::debug_overlay::DebugFrameOverlay, - pub(crate) a11y: A11y, -} - -#[derive(Clone, Debug, Default)] -struct ModifierState { - modifiers: Modifiers, - saw_other_input: bool, -} - -/// Tracks input event timestamps to determine if input is arriving at a high rate. -/// Used for selective VRR (Variable Refresh Rate) optimization. -#[derive(Clone, Debug)] -pub(crate) struct InputRateTracker { - timestamps: Vec, - window: Duration, - inputs_per_second: u32, - sustain_until: Instant, - sustain_duration: Duration, -} - -impl Default for InputRateTracker { - fn default() -> Self { - Self { - timestamps: Vec::new(), - window: Duration::from_millis(100), - inputs_per_second: 60, - sustain_until: Instant::now(), - sustain_duration: Duration::from_secs(1), - } - } -} - -impl InputRateTracker { - pub fn record_input(&mut self) { - let now = Instant::now(); - self.timestamps.push(now); - self.prune_old_timestamps(now); - - let min_events = self.inputs_per_second as u128 * self.window.as_millis() / 1000; - if self.timestamps.len() as u128 >= min_events { - self.sustain_until = now + self.sustain_duration; - } - } - - pub fn is_high_rate(&self) -> bool { - Instant::now() < self.sustain_until - } - - fn prune_old_timestamps(&mut self, now: Instant) { - self.timestamps - .retain(|&t| now.duration_since(t) <= self.window); - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum DrawPhase { - None, - Prepaint, - Paint, - Focus, -} - -pub(crate) const PENDING_INPUT_TIMEOUT: Duration = Duration::from_secs(1); - -/// Pending input for a potential multi-stroke key binding. -pub struct PendingInputStatus<'a> { - keystrokes: &'a [Keystroke], - timeout: Option, -} - -impl<'a> PendingInputStatus<'a> { - /// Returns the keystrokes entered so far. - pub fn keystrokes(&self) -> &'a [Keystroke] { - self.keystrokes - } - - /// Returns the timeout state for flushing this input, if it needs a timeout. - pub fn timeout(&self) -> Option { - self.timeout - } -} - -/// The timeout state for pending input. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct PendingInputTimeoutStatus { - duration: Duration, - remaining: Duration, - started_at: Option, - paused: bool, -} - -impl PendingInputTimeoutStatus { - /// Returns the full timeout duration. - pub fn duration(&self) -> Duration { - self.duration - } - - /// Returns the duration remaining before pending input is flushed. - pub fn remaining(&self, cx: &App) -> Duration { - self.started_at - .map(|started_at| { - self.remaining - .saturating_sub(cx.background_executor().now() - started_at) - }) - .unwrap_or(self.remaining) - } - - /// Returns whether the timeout is paused. - pub fn is_paused(&self) -> bool { - self.paused - } -} - -#[derive(Debug)] -struct PendingInputTimeout { - duration: Duration, - remaining: Duration, - state: PendingInputTimeoutState, -} - -#[derive(Debug)] -enum PendingInputTimeoutState { - Running { started_at: Instant, task: Task<()> }, - Paused { pause: PendingInputTimeoutPause }, -} - -#[derive(Debug)] -struct PendingInputTimeoutPause { - owner_id: EntityId, - _release_subscription: Subscription, -} - -impl PendingInputTimeout { - fn is_paused(&self) -> bool { - matches!(&self.state, PendingInputTimeoutState::Paused { .. }) - } - - fn pause(&mut self, pause: PendingInputTimeoutPause, now: Instant) -> bool { - match std::mem::replace(&mut self.state, PendingInputTimeoutState::Paused { pause }) { - PendingInputTimeoutState::Running { started_at, task } => { - self.remaining = self.remaining.saturating_sub(now - started_at); - drop(task); - true - } - previous_state @ PendingInputTimeoutState::Paused { .. } => { - self.state = previous_state; - false - } - } - } - - fn pause_owner_id(&self) -> Option { - match &self.state { - PendingInputTimeoutState::Running { .. } => None, - PendingInputTimeoutState::Paused { pause } => Some(pause.owner_id), - } - } - - fn resume(&mut self, owner_id: EntityId, started_at: Instant, task: Task<()>) -> bool { - match std::mem::replace( - &mut self.state, - PendingInputTimeoutState::Running { started_at, task }, - ) { - PendingInputTimeoutState::Paused { pause } if pause.owner_id == owner_id => true, - previous_state => { - self.state = previous_state; - false - } - } - } - - fn reset_duration(&mut self, duration: Duration) { - self.duration = duration; - self.remaining = duration; - } - - fn status(&self) -> PendingInputTimeoutStatus { - let (started_at, paused) = match &self.state { - PendingInputTimeoutState::Running { started_at, .. } => (Some(*started_at), false), - PendingInputTimeoutState::Paused { .. } => (None, true), - }; - PendingInputTimeoutStatus { - duration: self.duration, - remaining: self.remaining, - started_at, - paused, - } - } -} - -#[derive(Default, Debug)] -struct PendingInput { - keystrokes: SmallVec<[Keystroke; 1]>, - focus: Option, - timeout: Option, -} - -pub(crate) struct ElementStateBox { - pub(crate) inner: Box, - #[cfg(debug_assertions)] - pub(crate) type_name: &'static str, -} - -fn default_bounds(display_id: Option, cx: &mut App) -> WindowBounds { - // TODO, BUG: if you open a window with the currently active window - // on the stack, this will erroneously fallback to `None` - // - // TODO these should be the initial window bounds not considering maximized/fullscreen - let active_window_bounds = cx - .active_window() - .and_then(|w| w.update(cx, |_, window, _| window.window_bounds()).ok()); - - const CASCADE_OFFSET: f32 = 25.0; - - let display = display_id - .map(|id| cx.find_display(id)) - .unwrap_or_else(|| cx.primary_display()); - - let default_placement = || Bounds::new(point(px(0.), px(0.)), DEFAULT_WINDOW_SIZE); - - // Use visible_bounds to exclude taskbar/dock areas - let display_bounds = display - .as_ref() - .map(|d| d.visible_bounds()) - .unwrap_or_else(default_placement); - - let ( - Bounds { - origin: base_origin, - size: base_size, - }, - window_bounds_ctor, - ): (_, fn(Bounds) -> WindowBounds) = match active_window_bounds { - Some(bounds) => match bounds { - WindowBounds::Windowed(bounds) => (bounds, WindowBounds::Windowed), - WindowBounds::Maximized(bounds) => (bounds, WindowBounds::Maximized), - WindowBounds::Fullscreen(bounds) => (bounds, WindowBounds::Fullscreen), - }, - None => ( - display - .as_ref() - .map(|d| d.default_bounds()) - .unwrap_or_else(default_placement), - WindowBounds::Windowed, - ), - }; - - let cascade_offset = point(px(CASCADE_OFFSET), px(CASCADE_OFFSET)); - let proposed_origin = base_origin + cascade_offset; - let proposed_bounds = Bounds::new(proposed_origin, base_size); - - let display_right = display_bounds.origin.x + display_bounds.size.width; - let display_bottom = display_bounds.origin.y + display_bounds.size.height; - let window_right = proposed_bounds.origin.x + proposed_bounds.size.width; - let window_bottom = proposed_bounds.origin.y + proposed_bounds.size.height; - - let fits_horizontally = window_right <= display_right; - let fits_vertically = window_bottom <= display_bottom; - - let final_origin = match (fits_horizontally, fits_vertically) { - (true, true) => proposed_origin, - (false, true) => point(display_bounds.origin.x, base_origin.y), - (true, false) => point(base_origin.x, display_bounds.origin.y), - (false, false) => display_bounds.origin, - }; - window_bounds_ctor(Bounds::new(final_origin, base_size)) -} - -impl Window { - pub(crate) fn new( - handle: AnyWindowHandle, - options: WindowOptions, - cx: &mut App, - ) -> Result { - let WindowOptions { - window_bounds, - titlebar, - focus, - show, - kind, - is_movable, - app_owns_titlebar_drag, - inactive_frame_interval, - is_resizable, - is_minimizable, - display_id, - window_background, - app_id, - window_min_size, - window_decorations, - #[cfg_attr( - not(any(target_os = "linux", target_os = "freebsd")), - allow(unused_variables) - )] - icon, - #[cfg_attr(not(target_os = "macos"), allow(unused_variables))] - tabbing_identifier, - } = options; - - let initial_window_title = titlebar - .as_ref() - .and_then(|titlebar| titlebar.title.clone()); - - let window_bounds = window_bounds.unwrap_or_else(|| default_bounds(display_id, cx)); - let mut platform_window = cx.platform.open_window( - handle, - WindowParams { - bounds: window_bounds.get_bounds(), - titlebar, - kind, - is_movable, - app_owns_titlebar_drag, - is_resizable, - is_minimizable, - focus, - show, - display_id, - window_min_size, - app_id: app_id.clone(), - icon, - #[cfg(target_os = "macos")] - tabbing_identifier, - }, - )?; - - let tab_bar_visible = platform_window.tab_bar_visible(); - SystemWindowTabController::init_visible(cx, tab_bar_visible); - if let Some(tabs) = platform_window.tabbed_windows() { - SystemWindowTabController::add_tab(cx, handle.window_id(), tabs); - } - - let display_id = platform_window.display().map(|display| display.id()); - let sprite_atlas = platform_window.sprite_atlas(); - let mouse_position = platform_window.mouse_position(); - let modifiers = platform_window.modifiers(); - let capslock = platform_window.capslock(); - let content_size = platform_window.content_size(); - let scale_factor = platform_window.scale_factor(); - let appearance = platform_window.appearance(); - let text_system = Arc::new(WindowTextSystem::new(cx.text_system().clone())); - let invalidator = WindowInvalidator::new(handle.window_id()); - let active = Rc::new(Cell::new(platform_window.is_active())); - let hovered = Rc::new(Cell::new(platform_window.is_hovered())); - let needs_present = Rc::new(Cell::new(false)); - let next_frame_callbacks: Rc>> = Default::default(); - let input_rate_tracker = Rc::new(RefCell::new(InputRateTracker::default())); - let last_frame_time = Rc::new(Cell::new(None)); - - platform_window - .request_decorations(window_decorations.unwrap_or(WindowDecorations::Server)); - platform_window.set_background_appearance(window_background); - - match window_bounds { - WindowBounds::Fullscreen(_) => platform_window.toggle_fullscreen(), - WindowBounds::Maximized(_) => platform_window.zoom(), - WindowBounds::Windowed(_) => {} - } - - let accessibility_force_disabled = cx.accessibility_force_disabled; - let a11y_active_flag = Arc::new(AtomicBool::new(false)); - - #[cfg(not(target_family = "wasm"))] - if !accessibility_force_disabled { - let mut initial_root_node = accesskit::Node::new(accesskit::Role::Window); - if let Some(title) = &initial_window_title { - initial_root_node.set_label(title.to_string()); - } - let initial_tree = accesskit::TreeUpdate { - nodes: vec![(ROOT_NODE_ID, initial_root_node)], - tree: Some(accesskit::Tree::new(ROOT_NODE_ID)), - tree_id: accesskit::TreeId::ROOT, - focus: ROOT_NODE_ID, - }; - let (activation_sender, activation_receiver) = async_channel::unbounded::<()>(); - let (deactivation_sender, deactivation_receiver) = async_channel::unbounded::<()>(); - let (action_sender, action_receiver) = - async_channel::unbounded::(); - - platform_window.a11y_init(crate::A11yCallbacks { - activation: { - let active_flag = a11y_active_flag.clone(); - Box::new(move || { - log::info!("Accessibility activated"); - active_flag.store(true, SeqCst); - activation_sender.send_blocking(()).log_err(); - Some(initial_tree.clone()) - }) - }, - action: Box::new(move |request| { - action_sender.send_blocking(request).log_err(); - }), - deactivation: { - let active_flag = a11y_active_flag.clone(); - Box::new(move || { - log::info!("Accessibility deactivated"); - active_flag.store(false, SeqCst); - deactivation_sender.send_blocking(()).log_err(); - }) - }, - }); - - // A11y can be activated at any time, and so we cannot compute a - // correct `TreeUpdate` on-demand. When this happens, we return a - // default empty `TreeUpdate`. - // - // So we force a new frame, which will then send a correct `TreeUpdate`. - let mut async_cx = cx.to_async(); - cx.foreground_executor() - .spawn(async move { - while activation_receiver.recv().await.is_ok() { - handle - .update(&mut async_cx, |_, window, _| window.refresh()) - .log_err(); - } - }) - .detach(); - - let mut async_cx = cx.to_async(); - cx.foreground_executor() - .spawn(async move { - while deactivation_receiver.recv().await.is_ok() { - handle - .update(&mut async_cx, |_, window, _| window.refresh()) - .log_err(); - } - }) - .detach(); - - let mut async_cx = cx.to_async(); - cx.foreground_executor() - .spawn(async move { - while let Ok(request) = action_receiver.recv().await { - handle - .update(&mut async_cx, |_, window, cx| { - window.handle_a11y_action(request, cx); - }) - .log_err(); - } - }) - .detach(); - } - - platform_window.on_close(Box::new({ - let window_id = handle.window_id(); - let mut cx = cx.to_async(); - move || { - let _ = handle.update(&mut cx, |_, window, _| window.remove_window()); - let _ = cx.update(|cx| { - SystemWindowTabController::remove_tab(cx, window_id); - }); - } - })); - platform_window.on_request_frame(Box::new({ - let mut cx = cx.to_async(); - let invalidator = invalidator.clone(); - let active = active.clone(); - let needs_present = needs_present.clone(); - let next_frame_callbacks = next_frame_callbacks.clone(); - let input_rate_tracker = input_rate_tracker.clone(); - let mut deferred_force_render = false; - move |request_frame_options| { - #[cfg(feature = "profiler")] - let _foreground_turn = profiler::journal::foreground_turn(); - // This must be checked before anything else: if this request - // arrived re-entrantly while a draw is on this thread's stack - // (e.g. via a nested message pump in the Windows window - // procedure), drawing would nest draws, and even touching the - // App would panic on its already-mutable borrow. Skip instead; - // the platform leaves the window invalidated (or re-invalidates - // it), so a fresh request arrives once the in-progress draw - // unwinds. Remember force_render so the deferred frame still - // bypasses the view cache. - // - // Returning here skips `complete_frame`, which on Wayland would - // stall the window's frame callbacks (no `surface.commit()`) — - // but calling it would hit the App borrow panic above, and this - // branch is unreachable there in practice: only Windows pumps - // platform events (and thus requests frames) mid-draw. - if draw_in_progress() { - log::debug!("deferring re-entrant window draw request"); - deferred_force_render |= request_frame_options.force_render; - return; - } - // Take the deferred flag first: `||` short-circuits, and leaving - // the flag set when this request already forces a render would - // force a second, redundant render on the next frame. - let force_render = - mem::take(&mut deferred_force_render) || request_frame_options.force_render; - - let thermal_state = handle - .update(&mut cx, |_, _, cx| cx.thermal_state()) - .log_err(); - - // Throttle frame rate based on conditions: - // - Thermal pressure (Serious/Critical): cap to ~60fps - // - Inactive window (not focused): cap to ~30fps to save energy - let min_frame_interval = if request_frame_options.require_presentation - || (!request_frame_options.force_render - && next_frame_callbacks.borrow().is_empty()) - { - None - } else if !active.get() && !input_rate_tracker.borrow_mut().is_high_rate() { - inactive_frame_interval - } else if let Some(ThermalState::Critical | ThermalState::Serious) = thermal_state { - Some(Duration::from_micros(16667)) - } else { - None - }; - - let now = Instant::now(); - if let Some(min_interval) = min_frame_interval { - if let Some(last_frame) = last_frame_time.get() - && now.duration_since(last_frame) < min_interval - { - // Don't lose a pending forced render to throttling. - deferred_force_render |= force_render; - // Deferred by throttling: ask demand-driven platforms to retry. - handle - .update(&mut cx, |_, window, _| { - window.platform_window.schedule_frame(); - }) - .log_err(); - // The demand that entered this branch (a deferred forced - // render or pending next-frame callbacks) is still - // unserved; platforms that stop requesting frames for - // idle windows need a wakeup to deliver the retry. - invalidator.wake_platform(); - return; - } - } - last_frame_time.set(Some(now)); - - let pending_next_frame_callbacks = next_frame_callbacks.take(); - if !pending_next_frame_callbacks.is_empty() { - handle - .update(&mut cx, |_, window, cx| { - for callback in pending_next_frame_callbacks { - callback(window, cx); - } - }) - .log_err(); - } - - // Keep presenting if input was recently arriving at a high rate (>= 60fps). - // Once high-rate input is detected, we sustain presentation for 1 second - // to prevent display underclocking during active input. - let needs_present = request_frame_options.require_presentation - || needs_present.get() - || input_rate_tracker.borrow_mut().is_high_rate(); - - if invalidator.is_dirty() || force_render { - measure("frame duration", || { - handle - .update(&mut cx, |_, window, cx| { - if force_render { - // Bypass cached view reuse so we don't replay stale - // atlas tile references after a GPU device recovery. - window.refresh(); - } - let arena_clear_needed = window.draw(cx); - window.present(); - arena_clear_needed.clear(cx); - }) - .log_err(); - }) - } else if needs_present { - handle - .update(&mut cx, |_, window, _| window.present()) - .log_err(); - } - - handle - .update(&mut cx, |_, window, _| { - if window.invalidator.is_dirty() - || !window.next_frame_callbacks.borrow().is_empty() - { - window.platform_window.schedule_frame(); - } - }) - .log_err(); - - // Platforms that stop requesting frames for idle windows only - // deliver another request after a wakeup. If demand remains - // after this frame (the window was re-invalidated mid-draw, or - // animations scheduled next-frame callbacks), re-arm the frame - // source explicitly. - if invalidator.is_dirty() || !next_frame_callbacks.borrow().is_empty() { - invalidator.wake_platform(); - } - } - })); - invalidator.set_platform_waker(platform_window.frame_waker()); - platform_window.on_resize(Box::new({ - let mut cx = cx.to_async(); - move |_, _| { - handle - .update(&mut cx, |_, window, cx| window.bounds_changed(cx)) - .log_err(); - } - })); - platform_window.on_moved(Box::new({ - let mut cx = cx.to_async(); - move || { - handle - .update(&mut cx, |_, window, cx| window.bounds_changed(cx)) - .log_err(); - } - })); - platform_window.on_appearance_changed(Box::new({ - let cx = cx.to_async(); - let foreground_executor = cx.foreground_executor().clone(); - move || { - let mut cx = cx.clone(); - // Defer the update because changing the AppKit appearance may - // synchronously invoke this callback while App is already borrowed. - foreground_executor - .spawn(async move { - handle - .update(&mut cx, |_, window, cx| window.appearance_changed(cx)) - .log_err(); - }) - .detach(); - } - })); - platform_window.on_button_layout_changed(Box::new({ - let mut cx = cx.to_async(); - move || { - handle - .update(&mut cx, |_, window, cx| window.button_layout_changed(cx)) - .log_err(); - } - })); - platform_window.on_active_status_change(Box::new({ - let mut cx = cx.to_async(); - move |active| { - handle - .update(&mut cx, |_, window, cx| { - window.active.set(active); - window.modifiers = window.platform_window.modifiers(); - window.capslock = window.platform_window.capslock(); - window - .activation_observers - .clone() - .retain(&(), |callback| callback(window, cx)); - - window.bounds_changed(cx); - window.refresh(); - - SystemWindowTabController::update_last_active(cx, window.handle.id); - }) - .log_err(); - } - })); - platform_window.on_hover_status_change(Box::new({ - let mut cx = cx.to_async(); - move |active| { - handle - .update(&mut cx, |_, window, _| { - window.hovered.set(active); - window.refresh(); - }) - .log_err(); - } - })); - platform_window.on_input({ - let mut cx = cx.to_async(); - Box::new(move |event| { - handle - .update(&mut cx, |_, window, cx| window.dispatch_event(event, cx)) - .log_err() - .unwrap_or(DispatchEventResult::default()) - }) - }); - platform_window.on_hit_test_window_control({ - let mut cx = cx.to_async(); - Box::new(move || { - handle - .update(&mut cx, |_, window, _cx| { - for (area, hitbox) in &window.rendered_frame.window_control_hitboxes { - if window.mouse_hit_test.ids.contains(&hitbox.id) { - return Some(*area); - } - } - None - }) - .log_err() - .unwrap_or(None) - }) - }); - platform_window.on_move_tab_to_new_window({ - let mut cx = cx.to_async(); - Box::new(move || { - handle - .update(&mut cx, |_, _window, cx| { - SystemWindowTabController::move_tab_to_new_window(cx, handle.window_id()); - }) - .log_err(); - }) - }); - platform_window.on_merge_all_windows({ - let mut cx = cx.to_async(); - Box::new(move || { - handle - .update(&mut cx, |_, _window, cx| { - SystemWindowTabController::merge_all_windows(cx, handle.window_id()); - }) - .log_err(); - }) - }); - platform_window.on_select_next_tab({ - let mut cx = cx.to_async(); - Box::new(move || { - handle - .update(&mut cx, |_, _window, cx| { - SystemWindowTabController::select_next_tab(cx, handle.window_id()); - }) - .log_err(); - }) - }); - platform_window.on_select_previous_tab({ - let mut cx = cx.to_async(); - Box::new(move || { - handle - .update(&mut cx, |_, _window, cx| { - SystemWindowTabController::select_previous_tab(cx, handle.window_id()) - }) - .log_err(); - }) - }); - platform_window.on_toggle_tab_bar({ - let mut cx = cx.to_async(); - Box::new(move || { - handle - .update(&mut cx, |_, window, cx| { - let tab_bar_visible = window.platform_window.tab_bar_visible(); - SystemWindowTabController::set_visible(cx, tab_bar_visible); - }) - .log_err(); - }) - }); - - if let Some(app_id) = app_id { - platform_window.set_app_id(&app_id); - } - - platform_window.map_window().unwrap(); - - Ok(Window { - handle, - invalidator, - removed: false, - platform_window, - display_id, - is_resizable, - is_minimizable, - sprite_atlas, - text_system, - text_rendering_mode: cx.text_rendering_mode.clone(), - rem_size: px(16.), - rem_size_override_stack: SmallVec::new(), - viewport_size: content_size, - layout_engine: Some(TaffyLayoutEngine::new()), - root: None, - element_id_stack: SmallVec::default(), - text_style_stack: Vec::new(), - rendered_entity_stack: Vec::new(), - element_offset_stack: Vec::new(), - content_mask_stack: Vec::new(), - element_opacity: 1.0, - requested_autoscroll: None, - last_text_input_configuration: None, - focused_text_input_active: false, - rendered_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), - next_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), - next_frame_callbacks, - next_hitbox_id: HitboxId(0), - next_tooltip_id: TooltipId::default(), - tooltip_bounds: None, - dirty_views: FxHashSet::default(), - focus_listeners: SubscriberSet::new(), - focus_lost_listeners: SubscriberSet::new(), - focus_lost_path: SmallVec::new(), - default_prevented: true, - mouse_position, - mouse_hit_test: HitTest::default(), - modifiers, - capslock, - scale_factor, - bounds_observers: SubscriberSet::new(), - appearance, - appearance_observers: SubscriberSet::new(), - button_layout_observers: SubscriberSet::new(), - active, - hovered, - needs_present, - input_rate_tracker, - #[cfg(feature = "profiler")] - window_profiler: profiler::WindowProfiler::new(handle.window_id())?, - last_input_modality: InputModality::Mouse, - touch_gestures: TouchGestureRecognizer::new( - cx.platform - .gestures() - .map_or_else(GestureTuning::default, |gestures| gestures.tuning()), - ), - touch_prediction_enabled: true, - long_press_timer: None, - long_press_capture: None, - refreshing: false, - activation_observers: SubscriberSet::new(), - focus: None, - focus_enabled: true, - focus_generation: 0, - pending_input: None, - pending_modifier: ModifierState::default(), - pending_input_observers: SubscriberSet::new(), - prompt: None, - client_inset: None, - image_cache_stack: Vec::new(), - captured_hitbox: None, - #[cfg(any(feature = "inspector", debug_assertions))] - inspector: None, - #[cfg(feature = "profiler")] - debug_frame_overlay: crate::debug_overlay::DebugFrameOverlay::new(), - a11y: A11y::new( - a11y_active_flag, - accessibility_force_disabled, - initial_window_title, - ), - }) - } - - pub(crate) fn new_focus_listener( - &self, - value: AnyWindowFocusListener, - ) -> (Subscription, impl FnOnce() + use<>) { - self.focus_listeners.insert((), value) - } -} - -#[derive(Clone, Debug, Default, PartialEq, Eq)] -#[expect(missing_docs)] -pub struct DispatchEventResult { - pub propagate: bool, - pub default_prevented: bool, -} - -/// Indicates which region of the window is visible. Content falling outside of -/// this mask will not be rendered. A mask carries both its rectangular cull -/// bounds and optional rounded corners; the bounds keep scene culling cheap, -/// while the renderer applies the corner shape to every primitive. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -#[repr(C)] -pub struct ContentMask { - /// The bounds - pub bounds: Bounds

, - /// The corner radii of the visible region. - pub corner_radii: Corners

, - /// Horizontal corner radii. - pub radii_x: Corners

, - /// Vertical corner radii. - pub radii_y: Corners

, - /// One-based index of the next clip in the scene; zero ends the chain. - pub parent: u32, - /// Explicit GPU record padding. - pub padding: u32, -} - -impl RoundedClip { - /// Convert logical clip geometry to device coordinates. - pub fn scale(&self, scale: f32) -> RoundedClip { - RoundedClip { - bounds: self.bounds.scale(scale), - radii_x: self.radii_x.scale(scale), - radii_y: self.radii_y.scale(scale), - parent: 0, - padding: 0, - } - } - - /// Exact point membership in the rounded rectangle. - pub fn contains(&self, point: Point) -> bool { - if self.bounds.is_empty() || !self.bounds.contains(&point) { - return false; - } - let x = f32::from(point.x - self.bounds.left()); - let y = f32::from(point.y - self.bounds.top()); - let right = f32::from(self.bounds.right() - point.x); - let bottom = f32::from(self.bounds.bottom() - point.y); - [ - (x, y, self.radii_x.top_left, self.radii_y.top_left), - (right, y, self.radii_x.top_right, self.radii_y.top_right), - ( - right, - bottom, - self.radii_x.bottom_right, - self.radii_y.bottom_right, - ), - ( - x, - bottom, - self.radii_x.bottom_left, - self.radii_y.bottom_left, - ), - ] - .into_iter() - .all(|(x, y, rx, ry)| { - let (rx, ry) = (f32::from(rx), f32::from(ry)); - if rx <= 0.0 || ry <= 0.0 || x >= rx || y >= ry { - return true; - } - let dx = (x - rx) / rx; - let dy = (y - ry) / ry; - dx * dx + dy * dy <= 1.0 - }) - } -} - -/// The exact intersection of rectangular and rounded ancestor clips. -/// -/// `bounds` is only a culling rectangle. Rounded curves keep the bounds and -/// radii of their owning element, even when a narrow descendant intersects them. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct ClipRegion { - /// Rectangular intersection used for culling and layout visibility. - pub bounds: Bounds, - /// Rounded constraints still relevant within `bounds`. - pub rounded_clips: SmallVec<[RoundedClip; 1]>, -} - -impl From> for ClipRegion { - fn from(mask: ContentMask) -> Self { - let radii = mask - .corner_radii - .clamp_radii_for_quad_size(mask.bounds.size); - Self::rounded(mask.bounds, radii, radii) - } -} - -impl ClipRegion { - /// Construct a clip with elliptical corners. The caller normalizes radii - /// against the original element before deriving any border inset. - pub fn rounded( - bounds: Bounds, - radii_x: Corners, - radii_y: Corners, - ) -> Self { - let mut region = Self { - bounds, - rounded_clips: SmallVec::new(), - }; - if radii_x.max() > px(0.) && radii_y.max() > px(0.) && !bounds.is_empty() { - region.rounded_clips.push(RoundedClip { - bounds, - radii_x, - radii_y, - parent: 0, - padding: 0, - }); - } - region - } - - /// Intersect without approximating or relocating either region's curves. - pub fn intersect(&self, other: &Self) -> Self { - let bounds = self.bounds.intersect(&other.bounds); - let mut result = Self { - bounds, - rounded_clips: SmallVec::new(), - }; - if bounds.is_empty() { - return result; - } - for clip in self.rounded_clips.iter().chain(&other.rounded_clips) { - // Logical containment cannot prune a curve: the GPU's conservative - // culling rectangle can expand across its antialiased edge. - if result.rounded_clips.contains(clip) { - continue; - } - result.rounded_clips.push(*clip); - } - result - } - - /// Exact logical point membership, independent of rasterization scale. - pub fn contains(&self, point: Point) -> bool { - !self.bounds.is_empty() - && self.bounds.contains(&point) - && self.rounded_clips.iter().all(|clip| clip.contains(point)) - } -} - -/// Concrete clip record exported to the Metal shader bindings. -#[allow(non_camel_case_types)] -pub type RoundedClip_ScaledPixels = RoundedClip; - -#[cfg(test)] -mod tests { - use super::*; - use crate::point; - - #[test] - fn nested_clips_preserve_both_original_shapes() { - let outer: ClipRegion = ContentMask { - bounds: Bounds::from_corners(point(px(0.), px(0.)), point(px(100.), px(100.))), - corner_radii: Corners::all(px(16.)), - ..Default::default() - } - .into(); - for (left, top, right, bottom) in - [(4., 4., 96., 96.), (0., 0., 10., 100.), (7., 1., 94., 80.)] - { - let inner: ClipRegion = ContentMask { - bounds: Bounds::from_corners( - point(px(left), px(top)), - point(px(right), px(bottom)), - ), - ..Default::default() - } - .into(); - let intersection = outer.intersect(&inner); - for x in 0..100 { - for y in 0..100 { - let point = point(px(x as f32 + 0.5), px(y as f32 + 0.5)); - assert_eq!( - intersection.contains(point), - outer.contains(point) && inner.contains(point) - ); - } - } - } - } -} diff --git a/crates/gpui_pre_apple/vendor/gpui/src/color.rs b/crates/gpui_pre_apple/vendor/gpui/src/color.rs deleted file mode 100644 index 3bd893f..0000000 --- a/crates/gpui_pre_apple/vendor/gpui/src/color.rs +++ /dev/null @@ -1,1070 +0,0 @@ -use anyhow::{Context as _, bail}; -use schemars::{JsonSchema, json_schema}; -use serde::{ - Deserialize, Deserializer, Serialize, Serializer, - de::{self, Visitor}, -}; -use std::borrow::Cow; -use std::{ - fmt::{self, Display, Formatter}, - hash::{Hash, Hasher}, -}; - -/// Convert an RGB hex color code number to a color type -pub fn rgb(hex: u32) -> Rgba { - let [_, r, g, b] = hex.to_be_bytes().map(|b| (b as f32) / 255.0); - Rgba { r, g, b, a: 1.0 } -} - -/// Convert an RGBA hex color code number to [`Rgba`] -pub fn rgba(hex: u32) -> Rgba { - let [r, g, b, a] = hex.to_be_bytes().map(|b| (b as f32) / 255.0); - Rgba { r, g, b, a } -} - -/// Swap from RGBA with premultiplied alpha to BGRA -pub fn swap_rgba_pa_to_bgra(color: &mut [u8]) { - color.swap(0, 2); - if color[3] > 0 { - let a = color[3] as f32 / 255.; - color[0] = (color[0] as f32 / a) as u8; - color[1] = (color[1] as f32 / a) as u8; - color[2] = (color[2] as f32 / a) as u8; - } -} - -/// An RGBA color -#[derive(PartialEq, Clone, Copy, Default)] -#[repr(C)] -pub struct Rgba { - /// The red component of the color, in the range 0.0 to 1.0 - pub r: f32, - /// The green component of the color, in the range 0.0 to 1.0 - pub g: f32, - /// The blue component of the color, in the range 0.0 to 1.0 - pub b: f32, - /// The alpha component of the color, in the range 0.0 to 1.0 - pub a: f32, -} - -impl fmt::Debug for Rgba { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "rgba({:#010x})", u32::from(*self)) - } -} - -impl Rgba { - /// Create a new [`Rgba`] color by blending this and another color together - pub fn blend(&self, other: Rgba) -> Self { - if other.a >= 1.0 { - other - } else if other.a <= 0.0 { - *self - } else { - Rgba { - r: (self.r * (1.0 - other.a)) + (other.r * other.a), - g: (self.g * (1.0 - other.a)) + (other.g * other.a), - b: (self.b * (1.0 - other.a)) + (other.b * other.a), - a: self.a, - } - } - } - - /// Returns a new RGBA color with the same red, green and blue channels, but - /// with a new alpha value. - /// - /// Example: - /// ``` - /// use gpui::rgba; - /// let color = rgba(0xFF0000FF); - /// let faded = color.alpha(0.25); - /// assert_eq!(faded.a, 0.25); - /// ``` - /// - /// This will return a red color with 25% opacity. - /// - /// Example: - /// ``` - /// use gpui::rgba; - /// let color = rgba(0x3399FFCC); - /// let transparent = color.alpha(0.0); - /// assert_eq!(transparent.a, 0.0); - /// ``` - /// - /// This will return the same blue color, fully transparent. - pub fn alpha(&self, a: f32) -> Self { - Rgba { - r: self.r, - g: self.g, - b: self.b, - a: a.clamp(0., 1.), - } - } - - /// Returns a new RGBA color with the same red, green, and blue channels, - /// but with the alpha channel multiplied by the given factor. - /// - /// Example: - /// ``` - /// use gpui::rgba; - /// let color = rgba(0xFF0000FF); // Fully opaque red - /// let faded = color.opacity(0.5); - /// assert_eq!(faded.a, 0.5); - /// ``` - /// - /// This will return a red color with 50% opacity. - /// - /// Example: - /// ``` - /// use gpui::rgba; - /// let color = rgba(0x3399FFCC); // A light blue with 80% opacity - /// let faded = color.opacity(0.5); - /// assert!((faded.a - 0.4).abs() < 1e-6); - /// ``` - /// - /// This will return the same blue color scaled down to 40% opacity. - pub fn opacity(&self, factor: f32) -> Self { - Rgba { - r: self.r, - g: self.g, - b: self.b, - a: self.a * factor.clamp(0., 1.), - } - } -} - -impl From for u32 { - fn from(rgba: Rgba) -> Self { - let r = (rgba.r * 255.0) as u32; - let g = (rgba.g * 255.0) as u32; - let b = (rgba.b * 255.0) as u32; - let a = (rgba.a * 255.0) as u32; - (r << 24) | (g << 16) | (b << 8) | a - } -} - -struct RgbaVisitor; - -impl Visitor<'_> for RgbaVisitor { - type Value = Rgba; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a string in the format #rrggbb or #rrggbbaa") - } - - fn visit_str(self, value: &str) -> Result { - Rgba::try_from(value).map_err(E::custom) - } -} - -impl JsonSchema for Rgba { - fn schema_name() -> Cow<'static, str> { - "Rgba".into() - } - - fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema { - json_schema!({ - "type": "string", - "pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$" - }) - } -} - -impl<'de> Deserialize<'de> for Rgba { - fn deserialize>(deserializer: D) -> Result { - deserializer.deserialize_str(RgbaVisitor) - } -} - -impl Serialize for Rgba { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let r = (self.r * 255.0).round() as u8; - let g = (self.g * 255.0).round() as u8; - let b = (self.b * 255.0).round() as u8; - let a = (self.a * 255.0).round() as u8; - - let s = format!("#{r:02x}{g:02x}{b:02x}{a:02x}"); - serializer.serialize_str(&s) - } -} - -impl From for Rgba { - fn from(color: Hsla) -> Self { - let h = color.h; - let s = color.s; - let l = color.l; - - let c = (1.0 - (2.0 * l - 1.0).abs()) * s; - let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs()); - let m = l - c / 2.0; - let cm = c + m; - let xm = x + m; - - let (r, g, b) = match (h * 6.0).floor() as i32 { - 0 | 6 => (cm, xm, m), - 1 => (xm, cm, m), - 2 => (m, cm, xm), - 3 => (m, xm, cm), - 4 => (xm, m, cm), - _ => (cm, m, xm), - }; - - Rgba { - r: r.clamp(0., 1.), - g: g.clamp(0., 1.), - b: b.clamp(0., 1.), - a: color.a, - } - } -} - -impl TryFrom<&'_ str> for Rgba { - type Error = anyhow::Error; - - fn try_from(value: &'_ str) -> Result { - const RGB: usize = "rgb".len(); - const RGBA: usize = "rgba".len(); - const RRGGBB: usize = "rrggbb".len(); - const RRGGBBAA: usize = "rrggbbaa".len(); - - const EXPECTED_FORMATS: &str = "Expected #rgb, #rgba, #rrggbb, or #rrggbbaa"; - const INVALID_UNICODE: &str = "invalid unicode characters in color"; - - let Some(("", hex)) = value.trim().split_once('#') else { - bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}"); - }; - - let (r, g, b, a) = match hex.len() { - RGB | RGBA => { - let r = u8::from_str_radix( - hex.get(0..1).with_context(|| { - format!("{INVALID_UNICODE}: r component of #rgb/#rgba for value: '{value}'") - })?, - 16, - )?; - let g = u8::from_str_radix( - hex.get(1..2).with_context(|| { - format!("{INVALID_UNICODE}: g component of #rgb/#rgba for value: '{value}'") - })?, - 16, - )?; - let b = u8::from_str_radix( - hex.get(2..3).with_context(|| { - format!("{INVALID_UNICODE}: b component of #rgb/#rgba for value: '{value}'") - })?, - 16, - )?; - let a = if hex.len() == RGBA { - u8::from_str_radix( - hex.get(3..4).with_context(|| { - format!("{INVALID_UNICODE}: a component of #rgba for value: '{value}'") - })?, - 16, - )? - } else { - 0xf - }; - - /// Duplicates a given hex digit. - /// E.g., `0xf` -> `0xff`. - const fn duplicate(value: u8) -> u8 { - (value << 4) | value - } - - (duplicate(r), duplicate(g), duplicate(b), duplicate(a)) - } - RRGGBB | RRGGBBAA => { - let r = u8::from_str_radix( - hex.get(0..2).with_context(|| { - format!( - "{}: r component of #rrggbb/#rrggbbaa for value: '{}'", - INVALID_UNICODE, value - ) - })?, - 16, - )?; - let g = u8::from_str_radix( - hex.get(2..4).with_context(|| { - format!( - "{INVALID_UNICODE}: g component of #rrggbb/#rrggbbaa for value: '{value}'" - ) - })?, - 16, - )?; - let b = u8::from_str_radix( - hex.get(4..6).with_context(|| { - format!( - "{INVALID_UNICODE}: b component of #rrggbb/#rrggbbaa for value: '{value}'" - ) - })?, - 16, - )?; - let a = if hex.len() == RRGGBBAA { - u8::from_str_radix( - hex.get(6..8).with_context(|| { - format!( - "{INVALID_UNICODE}: a component of #rrggbbaa for value: '{value}'" - ) - })?, - 16, - )? - } else { - 0xff - }; - (r, g, b, a) - } - _ => bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}"), - }; - - Ok(Rgba { - r: r as f32 / 255., - g: g as f32 / 255., - b: b as f32 / 255., - a: a as f32 / 255., - }) - } -} - -/// An HSLA color -#[derive(Default, Copy, Clone, Debug)] -#[repr(C)] -pub struct Hsla { - /// Hue, in a range from 0 to 1 - pub h: f32, - - /// Saturation, in a range from 0 to 1 - pub s: f32, - - /// Lightness, in a range from 0 to 1 - pub l: f32, - - /// Alpha, in a range from 0 to 1 - pub a: f32, -} - -#[cfg(feature = "proptest")] -mod property { - use super::Hsla; - use proptest::prelude::*; - - impl Hsla { - /// Proptest [`Strategy`] that produces opaque colors (i.e. alpha = 1). - /// - /// For truly arbitrary colors, use the [`Arbitrary`] implementation. - pub fn opaque_strategy() -> impl Strategy { - (0.0f32..=1.0, 0.0f32..=1.0, 0.0f32..=1.0).prop_map(|(h, s, l)| Hsla { h, s, l, a: 1. }) - } - } - - impl Arbitrary for Hsla { - type Strategy = BoxedStrategy; - type Parameters = (); - - fn arbitrary_with((): Self::Parameters) -> Self::Strategy { - (0.0f32..=1.0, 0.0f32..=1.0, 0.0f32..=1.0, 0.0f32..=1.0) - .prop_map(|(h, s, l, a)| Hsla { h, s, l, a }) - .boxed() - } - } -} - -impl PartialEq for Hsla { - fn eq(&self, other: &Self) -> bool { - self.h - .total_cmp(&other.h) - .then(self.s.total_cmp(&other.s)) - .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a))) - .is_eq() - } -} - -impl PartialOrd for Hsla { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Hsla { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.h - .total_cmp(&other.h) - .then(self.s.total_cmp(&other.s)) - .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a))) - } -} - -impl Eq for Hsla {} - -impl Hash for Hsla { - fn hash(&self, state: &mut H) { - state.write_u32(u32::from_be_bytes(self.h.to_be_bytes())); - state.write_u32(u32::from_be_bytes(self.s.to_be_bytes())); - state.write_u32(u32::from_be_bytes(self.l.to_be_bytes())); - state.write_u32(u32::from_be_bytes(self.a.to_be_bytes())); - } -} - -impl Display for Hsla { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "hsla({:.2}, {:.2}%, {:.2}%, {:.2})", - self.h * 360., - self.s * 100., - self.l * 100., - self.a - ) - } -} - -/// Construct an [`Hsla`] object from plain values -pub const fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla { - Hsla { - h: h.clamp(0., 1.), - s: s.clamp(0., 1.), - l: l.clamp(0., 1.), - a: a.clamp(0., 1.), - } -} - -/// Pure black in [`Hsla`] -pub const fn black() -> Hsla { - Hsla { - h: 0., - s: 0., - l: 0., - a: 1., - } -} - -/// Transparent black in [`Hsla`] -pub const fn transparent_black() -> Hsla { - Hsla { - h: 0., - s: 0., - l: 0., - a: 0., - } -} - -/// Transparent white in [`Hsla`] -pub const fn transparent_white() -> Hsla { - Hsla { - h: 0., - s: 0., - l: 1., - a: 0., - } -} - -/// Opaque grey in [`Hsla`], values will be clamped to the range [0, 1] -pub const fn opaque_grey(lightness: f32, opacity: f32) -> Hsla { - Hsla { - h: 0., - s: 0., - l: lightness.clamp(0., 1.), - a: opacity.clamp(0., 1.), - } -} - -/// Pure white in [`Hsla`] -pub const fn white() -> Hsla { - Hsla { - h: 0., - s: 0., - l: 1., - a: 1., - } -} - -/// The color red in [`Hsla`] -pub const fn red() -> Hsla { - Hsla { - h: 0., - s: 1., - l: 0.5, - a: 1., - } -} - -/// The color blue in [`Hsla`] -pub const fn blue() -> Hsla { - Hsla { - h: 0.6666666667, - s: 1., - l: 0.5, - a: 1., - } -} - -/// The color green in [`Hsla`] -pub const fn green() -> Hsla { - Hsla { - h: 0.3333333333, - s: 1., - l: 0.25, - a: 1., - } -} - -/// The color yellow in [`Hsla`] -pub const fn yellow() -> Hsla { - Hsla { - h: 0.1666666667, - s: 1., - l: 0.5, - a: 1., - } -} - -impl Hsla { - /// Converts this HSLA color to an RGBA color. - pub fn to_rgb(self) -> Rgba { - self.into() - } - - /// The color red - pub const fn red() -> Self { - red() - } - - /// The color green - pub const fn green() -> Self { - green() - } - - /// The color blue - pub const fn blue() -> Self { - blue() - } - - /// The color black - pub const fn black() -> Self { - black() - } - - /// The color white - pub const fn white() -> Self { - white() - } - - /// The color transparent black - pub const fn transparent_black() -> Self { - transparent_black() - } - - /// Returns true if the HSLA color is fully transparent, false otherwise. - pub fn is_transparent(&self) -> bool { - self.a == 0.0 - } - - /// Returns true if the HSLA color is fully opaque, false otherwise. - pub fn is_opaque(&self) -> bool { - self.a == 1.0 - } - - /// Blends `other` on top of `self` based on `other`'s alpha value. The resulting color is a combination of `self`'s and `other`'s colors. - /// - /// If `other`'s alpha value is 1.0 or greater, `other` color is fully opaque, thus `other` is returned as the output color. - /// If `other`'s alpha value is 0.0 or less, `other` color is fully transparent, thus `self` is returned as the output color. - /// Else, the output color is calculated as a blend of `self` and `other` based on their weighted alpha values. - /// - /// Assumptions: - /// - Alpha values are contained in the range [0, 1], with 1 as fully opaque and 0 as fully transparent. - /// - The relative contributions of `self` and `other` is based on `self`'s alpha value (`self.a`) and `other`'s alpha value (`other.a`), `self` contributing `self.a * (1.0 - other.a)` and `other` contributing its own alpha value. - /// - RGB color components are contained in the range [0, 1]. - /// - If `self` and `other` colors are out of the valid range, the blend operation's output and behavior is undefined. - pub fn blend(self, other: Hsla) -> Hsla { - let alpha = other.a; - - if alpha >= 1.0 { - other - } else if alpha <= 0.0 { - self - } else { - let converted_self = Rgba::from(self); - let converted_other = Rgba::from(other); - let blended_rgb = converted_self.blend(converted_other); - Hsla::from(blended_rgb) - } - } - - /// Returns a new HSLA color with the same hue, and lightness, but with no saturation. - pub fn grayscale(&self) -> Self { - Hsla { - h: self.h, - s: 0., - l: self.l, - a: self.a, - } - } - - /// Fade out the color by a given factor. This factor should be between 0.0 and 1.0. - /// Where 0.0 will leave the color unchanged, and 1.0 will completely fade out the color. - pub fn fade_out(&mut self, factor: f32) { - self.a *= 1.0 - factor.clamp(0., 1.); - } - - /// Multiplies the alpha value of the color by a given factor - /// and returns a new HSLA color. - /// - /// Useful for transforming colors with dynamic opacity, - /// like a color from an external source. - /// - /// Example: - /// ``` - /// let color = gpui::red(); - /// let faded_color = color.opacity(0.5); - /// assert_eq!(faded_color.a, 0.5); - /// ``` - /// - /// This will return a red color with half the opacity. - /// - /// Example: - /// ``` - /// use gpui::hsla; - /// let color = hsla(0.7, 1.0, 0.5, 0.7); // A saturated blue - /// let faded_color = color.opacity(0.16); - /// assert!((faded_color.a - 0.112).abs() < 1e-6); - /// ``` - /// - /// This will return a blue color with around ~10% opacity, - /// suitable for an element's hover or selected state. - /// - pub fn opacity(&self, factor: f32) -> Self { - Hsla { - h: self.h, - s: self.s, - l: self.l, - a: self.a * factor.clamp(0., 1.), - } - } - - /// Returns a new HSLA color with the same hue, saturation, - /// and lightness, but with a new alpha value. - /// - /// Example: - /// ``` - /// let color = gpui::red(); - /// let red_color = color.alpha(0.25); - /// assert_eq!(red_color.a, 0.25); - /// ``` - /// - /// This will return a red color with 25% opacity. - /// - /// Example: - /// ``` - /// use gpui::hsla; - /// let color = hsla(0.7, 1.0, 0.5, 0.7); // A saturated blue - /// let faded_color = color.alpha(0.25); - /// assert_eq!(faded_color.a, 0.25); - /// ``` - /// - /// This will return a blue color with 25% opacity. - pub fn alpha(&self, a: f32) -> Self { - Hsla { - h: self.h, - s: self.s, - l: self.l, - a: a.clamp(0., 1.), - } - } -} - -impl From for Hsla { - fn from(color: Rgba) -> Self { - let r = color.r; - let g = color.g; - let b = color.b; - - let max = r.max(g.max(b)); - let min = r.min(g.min(b)); - let delta = max - min; - - let l = (max + min) / 2.0; - let s = if l == 0.0 || l == 1.0 { - 0.0 - } else if l < 0.5 { - delta / (2.0 * l) - } else { - delta / (2.0 - 2.0 * l) - }; - - let h = if delta == 0.0 { - 0.0 - } else if max == r { - ((g - b) / delta).rem_euclid(6.0) / 6.0 - } else if max == g { - ((b - r) / delta + 2.0) / 6.0 - } else { - ((r - g) / delta + 4.0) / 6.0 - }; - - Hsla { - h, - s, - l, - a: color.a, - } - } -} - -impl JsonSchema for Hsla { - fn schema_name() -> Cow<'static, str> { - Rgba::schema_name() - } - - fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { - Rgba::json_schema(generator) - } -} - -impl Serialize for Hsla { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - Rgba::from(*self).serialize(serializer) - } -} - -impl<'de> Deserialize<'de> for Hsla { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - Ok(Rgba::deserialize(deserializer)?.into()) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)] -#[repr(C)] -pub(crate) enum BackgroundTag { - Solid = 0, - LinearGradient = 1, - PatternSlash = 2, - Checkerboard = 3, -} - -/// A color space for color interpolation. -/// -/// References: -/// - -/// - -#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize, JsonSchema)] -#[repr(C)] -pub enum ColorSpace { - #[default] - /// The sRGB color space. - Srgb = 0, - /// The Oklab color space. - Oklab = 1, -} - -impl Display for ColorSpace { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self { - ColorSpace::Srgb => write!(f, "sRGB"), - ColorSpace::Oklab => write!(f, "Oklab"), - } - } -} - -/// A background color, which can be either a solid color or a linear gradient. -#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)] -#[repr(C)] -pub struct Background { - pub(crate) tag: BackgroundTag, - pub(crate) color_space: ColorSpace, - pub(crate) solid: Hsla, - pub(crate) gradient_angle_or_pattern_height: f32, - pub(crate) colors: [LinearColorStop; 2], - /// Padding for alignment for repr(C) layout. - pad: u32, -} - -impl std::fmt::Debug for Background { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self.tag { - BackgroundTag::Solid => write!(f, "Solid({:?})", self.solid), - BackgroundTag::LinearGradient => write!( - f, - "LinearGradient({}, {:?}, {:?})", - self.gradient_angle_or_pattern_height, self.colors[0], self.colors[1] - ), - BackgroundTag::PatternSlash => write!( - f, - "PatternSlash({:?}, {})", - self.solid, self.gradient_angle_or_pattern_height - ), - BackgroundTag::Checkerboard => write!( - f, - "Checkerboard({:?}, {})", - self.solid, self.gradient_angle_or_pattern_height - ), - } - } -} - -impl Eq for Background {} -impl Default for Background { - fn default() -> Self { - Self { - tag: BackgroundTag::Solid, - solid: Hsla::default(), - color_space: ColorSpace::default(), - gradient_angle_or_pattern_height: 0.0, - colors: [LinearColorStop::default(), LinearColorStop::default()], - pad: 0, - } - } -} - -/// Creates a hash pattern background -pub fn pattern_slash(color: impl Into, width: f32, interval: f32) -> Background { - let width_scaled = (width * 255.0) as u32; - let interval_scaled = (interval * 255.0) as u32; - let height = ((width_scaled * 0xFFFF) + interval_scaled) as f32; - - Background { - tag: BackgroundTag::PatternSlash, - solid: color.into(), - gradient_angle_or_pattern_height: height, - ..Default::default() - } -} - -/// Creates a checkerboard pattern background -pub fn checkerboard(color: impl Into, size: f32) -> Background { - Background { - tag: BackgroundTag::Checkerboard, - solid: color.into(), - gradient_angle_or_pattern_height: size, - ..Default::default() - } -} - -/// Creates a solid background color. -pub fn solid_background(color: impl Into) -> Background { - Background { - solid: color.into(), - ..Default::default() - } -} - -/// Creates a LinearGradient background color. -/// -/// The gradient line's angle of direction. A value of `0.` is equivalent to top; increasing values rotate clockwise from there. -/// -/// The `angle` is in degrees value in the range 0.0 to 360.0. -/// -/// -pub fn linear_gradient( - angle: f32, - from: impl Into, - to: impl Into, -) -> Background { - Background { - tag: BackgroundTag::LinearGradient, - gradient_angle_or_pattern_height: angle, - colors: [from.into(), to.into()], - ..Default::default() - } -} - -/// A color stop in a linear gradient. -/// -/// -#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, JsonSchema)] -#[repr(C)] -pub struct LinearColorStop { - /// The color of the color stop. - pub color: Hsla, - /// The percentage of the gradient, in the range 0.0 to 1.0. - pub percentage: f32, -} - -/// Creates a new linear color stop. -/// -/// The percentage of the gradient, in the range 0.0 to 1.0. -pub fn linear_color_stop(color: impl Into, percentage: f32) -> LinearColorStop { - LinearColorStop { - color: color.into(), - percentage, - } -} - -impl LinearColorStop { - /// Returns a new color stop with the same color, but with a modified alpha value. - pub fn opacity(&self, factor: f32) -> Self { - Self { - percentage: self.percentage, - color: self.color.opacity(factor), - } - } -} - -impl Background { - /// Returns the solid color if this is a solid background, None otherwise. - pub fn as_solid(&self) -> Option { - if self.tag == BackgroundTag::Solid { - Some(self.solid) - } else { - None - } - } - - /// Use specified color space for color interpolation. - /// - /// - pub fn color_space(mut self, color_space: ColorSpace) -> Self { - self.color_space = color_space; - self - } - - /// Returns a new background color with the same hue, saturation, and lightness, but with a modified alpha value. - pub fn opacity(&self, factor: f32) -> Self { - let mut background = *self; - background.solid = background.solid.opacity(factor); - background.colors = [ - self.colors[0].opacity(factor), - self.colors[1].opacity(factor), - ]; - background - } - - /// Returns whether the background color is transparent. - pub fn is_transparent(&self) -> bool { - match self.tag { - BackgroundTag::Solid => self.solid.is_transparent(), - BackgroundTag::LinearGradient => self.colors.iter().all(|c| c.color.is_transparent()), - BackgroundTag::PatternSlash => self.solid.is_transparent(), - BackgroundTag::Checkerboard => self.solid.is_transparent(), - } - } -} - -impl From for Background { - fn from(value: Hsla) -> Self { - Background { - tag: BackgroundTag::Solid, - solid: value, - ..Default::default() - } - } -} - -impl From for Background { - fn from(value: Rgba) -> Self { - Background { - tag: BackgroundTag::Solid, - solid: Hsla::from(value), - ..Default::default() - } - } -} - -#[cfg(test)] -mod tests { - use serde_json::json; - - use super::*; - - #[test] - fn test_deserialize_three_value_hex_to_rgba() { - let actual: Rgba = serde_json::from_value(json!("#f09")).unwrap(); - - assert_eq!(actual, rgba(0xff0099ff)) - } - - #[test] - fn test_deserialize_four_value_hex_to_rgba() { - let actual: Rgba = serde_json::from_value(json!("#f09f")).unwrap(); - - assert_eq!(actual, rgba(0xff0099ff)) - } - - #[test] - fn test_deserialize_six_value_hex_to_rgba() { - let actual: Rgba = serde_json::from_value(json!("#ff0099")).unwrap(); - - assert_eq!(actual, rgba(0xff0099ff)) - } - - #[test] - fn test_deserialize_eight_value_hex_to_rgba() { - let actual: Rgba = serde_json::from_value(json!("#ff0099ff")).unwrap(); - - assert_eq!(actual, rgba(0xff0099ff)) - } - - #[test] - fn test_deserialize_eight_value_hex_with_padding_to_rgba() { - let actual: Rgba = serde_json::from_value(json!(" #f5f5f5ff ")).unwrap(); - - assert_eq!(actual, rgba(0xf5f5f5ff)) - } - - #[test] - fn test_deserialize_eight_value_hex_with_mixed_case_to_rgba() { - let actual: Rgba = serde_json::from_value(json!("#DeAdbEeF")).unwrap(); - - assert_eq!(actual, rgba(0xdeadbeef)) - } - - #[test] - fn test_background_solid() { - let color = Hsla::from(rgba(0xff0099ff)); - let mut background = Background::from(color); - assert_eq!(background.tag, BackgroundTag::Solid); - assert_eq!(background.solid, color); - - assert_eq!(background.opacity(0.5).solid, color.opacity(0.5)); - assert!(!background.is_transparent()); - background.solid = hsla(0.0, 0.0, 0.0, 0.0); - assert!(background.is_transparent()); - } - - #[test] - fn test_background_linear_gradient() { - let from = linear_color_stop(rgba(0xff0099ff), 0.0); - let to = linear_color_stop(rgba(0x00ff99ff), 1.0); - let background = linear_gradient(90.0, from, to); - assert_eq!(background.tag, BackgroundTag::LinearGradient); - assert_eq!(background.colors[0], from); - assert_eq!(background.colors[1], to); - - assert_eq!(background.opacity(0.5).colors[0], from.opacity(0.5)); - assert_eq!(background.opacity(0.5).colors[1], to.opacity(0.5)); - assert!(!background.is_transparent()); - assert!(background.opacity(0.0).is_transparent()); - } - - #[test] - fn test_rgba_alpha() { - let color = Rgba { - r: 0.2, - g: 0.6, - b: 1.0, - a: 0.8, - }; - - assert_eq!(color.alpha(0.25).a, 0.25); - assert_eq!(color.alpha(1.5).a, 1.0); - } - - #[test] - fn test_rgba_opacity() { - let color = Rgba { - r: 0.2, - g: 0.6, - b: 1.0, - a: 0.8, - }; - assert!((color.opacity(0.5).a - 0.4).abs() < 1e-6); - assert_eq!(color.opacity(2.0).a, 0.8); - } -} diff --git a/crates/gpui_pre_apple/vendor/gpui/src/geometry.rs b/crates/gpui_pre_apple/vendor/gpui/src/geometry.rs deleted file mode 100644 index 05dafbf..0000000 --- a/crates/gpui_pre_apple/vendor/gpui/src/geometry.rs +++ /dev/null @@ -1,4008 +0,0 @@ -//! The GPUI geometry module is a collection of types and traits that -//! can be used to describe common units, concepts, and the relationships -//! between them. - -use anyhow::{Context as _, anyhow}; -use core::fmt::Debug; -use derive_more::{Add, AddAssign, Div, DivAssign, Mul, Neg, Sub, SubAssign}; -use refineable::Refineable; -use schemars::{JsonSchema, json_schema}; -use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; -use std::borrow::Cow; -use std::ops::{AddAssign, Range}; -use std::{ - cmp::{self, PartialOrd}, - fmt::{self, Display}, - hash::Hash, - ops::{Add, Div, Mul, MulAssign, Neg, Sub}, -}; -use taffy::prelude::{TaffyGridLine, TaffyGridSpan}; - -use crate::{App, DisplayId}; - -/// Axis in a 2D cartesian space. -#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] -pub enum Axis { - /// The y axis, or up and down - Vertical, - /// The x axis, or left and right - Horizontal, -} - -impl Axis { - /// Swap this axis to the opposite axis. - pub fn invert(self) -> Self { - match self { - Axis::Vertical => Axis::Horizontal, - Axis::Horizontal => Axis::Vertical, - } - } -} - -/// A trait for accessing the given unit along a certain axis. -pub trait Along { - /// The unit associated with this type - type Unit; - - /// Returns the unit along the given axis. - fn along(&self, axis: Axis) -> Self::Unit; - - /// Applies the given function to the unit along the given axis and returns a new value. - fn apply_along(&self, axis: Axis, f: impl FnOnce(Self::Unit) -> Self::Unit) -> Self; -} - -/// Describes a location in a 2D cartesian space. -/// -/// It holds two public fields, `x` and `y`, which represent the coordinates in the space. -/// The type `T` for the coordinates can be any type that implements `Default`, `Clone`, and `Debug`. -/// -/// # Examples -/// -/// ``` -/// # use gpui::Point; -/// let point = Point { x: 10, y: 20 }; -/// println!("{:?}", point); // Outputs: Point { x: 10, y: 20 } -/// ``` -#[derive( - Refineable, - Default, - Add, - AddAssign, - Sub, - SubAssign, - Copy, - Debug, - PartialEq, - Eq, - Serialize, - Deserialize, - JsonSchema, - Hash, - Neg, -)] -#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -#[repr(C)] -pub struct Point { - /// The x coordinate of the point. - pub x: T, - /// The y coordinate of the point. - pub y: T, -} - -/// Constructs a new `Point` with the given x and y coordinates. -/// -/// # Arguments -/// -/// * `x` - The x coordinate of the point. -/// * `y` - The y coordinate of the point. -/// -/// # Returns -/// -/// Returns a `Point` with the specified coordinates. -/// -/// # Examples -/// -/// ``` -/// use gpui::point; -/// let p = point(10, 20); -/// assert_eq!(p.x, 10); -/// assert_eq!(p.y, 20); -/// ``` -pub const fn point(x: T, y: T) -> Point { - Point { x, y } -} - -impl Point { - /// Creates a new `Point` with the specified `x` and `y` coordinates. - /// - /// # Arguments - /// - /// * `x` - The horizontal coordinate of the point. - /// * `y` - The vertical coordinate of the point. - /// - /// # Examples - /// - /// ``` - /// use gpui::Point; - /// let p = Point::new(10, 20); - /// assert_eq!(p.x, 10); - /// assert_eq!(p.y, 20); - /// ``` - pub const fn new(x: T, y: T) -> Self { - Self { x, y } - } - - /// Transforms the point to a `Point` by applying the given function to both coordinates. - /// - /// This method allows for converting a `Point` to a `Point` by specifying a closure - /// that defines how to convert between the two types. The closure is applied to both the `x` - /// and `y` coordinates, resulting in a new point of the desired type. - /// - /// # Arguments - /// - /// * `f` - A closure that takes a value of type `T` and returns a value of type `U`. - /// - /// # Examples - /// - /// ``` - /// # use gpui::Point; - /// let p = Point { x: 3, y: 4 }; - /// let p_float = p.map(|coord| coord as f32); - /// assert_eq!(p_float, Point { x: 3.0, y: 4.0 }); - /// ``` - #[must_use] - pub fn map(&self, f: impl Fn(T) -> U) -> Point { - Point { - x: f(self.x.clone()), - y: f(self.y.clone()), - } - } -} - -impl Along for Point { - type Unit = T; - - fn along(&self, axis: Axis) -> T { - match axis { - Axis::Horizontal => self.x.clone(), - Axis::Vertical => self.y.clone(), - } - } - - fn apply_along(&self, axis: Axis, f: impl FnOnce(T) -> T) -> Point { - match axis { - Axis::Horizontal => Point { - x: f(self.x.clone()), - y: self.y.clone(), - }, - Axis::Vertical => Point { - x: self.x.clone(), - y: f(self.y.clone()), - }, - } - } -} - -impl Point { - /// Scales the point by a given factor, which is typically derived from the resolution - /// of a target display to ensure proper sizing of UI elements. - /// - /// # Arguments - /// - /// * `factor` - The scaling factor to apply to both the x and y coordinates. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Point, Pixels, ScaledPixels}; - /// let p = Point { x: Pixels::from(10.0), y: Pixels::from(20.0) }; - /// let scaled_p = p.scale(1.5); - /// assert_eq!(scaled_p, Point { x: ScaledPixels::from(15.0), y: ScaledPixels::from(30.0) }); - /// ``` - pub fn scale(&self, factor: f32) -> Point { - Point { - x: self.x.scale(factor), - y: self.y.scale(factor), - } - } - - /// Calculates the Euclidean distance from the origin (0, 0) to this point. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Pixels, Point}; - /// let p = Point { x: Pixels::from(3.0), y: Pixels::from(4.0) }; - /// assert_eq!(p.magnitude(), 5.0); - /// ``` - pub fn magnitude(&self) -> f64 { - ((self.x.0.powi(2) + self.y.0.powi(2)) as f64).sqrt() - } -} - -impl Point -where - T: Sub + Clone + Debug + Default + PartialEq, -{ - /// Get the position of this point, relative to the given origin - pub fn relative_to(&self, origin: &Point) -> Point { - point( - self.x.clone() - origin.x.clone(), - self.y.clone() - origin.y.clone(), - ) - } -} - -impl Mul for Point -where - T: Mul + Clone + Debug + Default + PartialEq, - Rhs: Clone + Debug, -{ - type Output = Point; - - fn mul(self, rhs: Rhs) -> Self::Output { - Point { - x: self.x * rhs.clone(), - y: self.y * rhs, - } - } -} - -impl MulAssign for Point -where - T: Mul + Clone + Debug + Default + PartialEq, - S: Clone, -{ - fn mul_assign(&mut self, rhs: S) { - self.x = self.x.clone() * rhs.clone(); - self.y = self.y.clone() * rhs; - } -} - -impl Div for Point -where - T: Div + Clone + Debug + Default + PartialEq, - S: Clone, -{ - type Output = Self; - - fn div(self, rhs: S) -> Self::Output { - Self { - x: self.x / rhs.clone(), - y: self.y / rhs, - } - } -} - -impl Point -where - T: PartialOrd + Clone + Debug + Default + PartialEq, -{ - /// Returns a new point with the maximum values of each dimension from `self` and `other`. - /// - /// # Arguments - /// - /// * `other` - A reference to another `Point` to compare with `self`. - /// - /// # Examples - /// - /// ``` - /// # use gpui::Point; - /// let p1 = Point { x: 3, y: 7 }; - /// let p2 = Point { x: 5, y: 2 }; - /// let max_point = p1.max(&p2); - /// assert_eq!(max_point, Point { x: 5, y: 7 }); - /// ``` - pub fn max(&self, other: &Self) -> Self { - Point { - x: if self.x > other.x { - self.x.clone() - } else { - other.x.clone() - }, - y: if self.y > other.y { - self.y.clone() - } else { - other.y.clone() - }, - } - } - - /// Returns a new point with the minimum values of each dimension from `self` and `other`. - /// - /// # Arguments - /// - /// * `other` - A reference to another `Point` to compare with `self`. - /// - /// # Examples - /// - /// ``` - /// # use gpui::Point; - /// let p1 = Point { x: 3, y: 7 }; - /// let p2 = Point { x: 5, y: 2 }; - /// let min_point = p1.min(&p2); - /// assert_eq!(min_point, Point { x: 3, y: 2 }); - /// ``` - pub fn min(&self, other: &Self) -> Self { - Point { - x: if self.x <= other.x { - self.x.clone() - } else { - other.x.clone() - }, - y: if self.y <= other.y { - self.y.clone() - } else { - other.y.clone() - }, - } - } - - /// Clamps the point to a specified range. - /// - /// Given a minimum point and a maximum point, this method constrains the current point - /// such that its coordinates do not exceed the range defined by the minimum and maximum points. - /// If the current point's coordinates are less than the minimum, they are set to the minimum. - /// If they are greater than the maximum, they are set to the maximum. - /// - /// # Arguments - /// - /// * `min` - A reference to a `Point` representing the minimum allowable coordinates. - /// * `max` - A reference to a `Point` representing the maximum allowable coordinates. - /// - /// # Examples - /// - /// ``` - /// # use gpui::Point; - /// let p = Point { x: 10, y: 20 }; - /// let min = Point { x: 0, y: 5 }; - /// let max = Point { x: 15, y: 25 }; - /// let clamped_p = p.clamp(&min, &max); - /// assert_eq!(clamped_p, Point { x: 10, y: 20 }); - /// - /// let p_out_of_bounds = Point { x: -5, y: 30 }; - /// let clamped_p_out_of_bounds = p_out_of_bounds.clamp(&min, &max); - /// assert_eq!(clamped_p_out_of_bounds, Point { x: 0, y: 25 }); - /// ``` - pub fn clamp(&self, min: &Self, max: &Self) -> Self { - self.max(min).min(max) - } -} - -impl Clone for Point { - fn clone(&self) -> Self { - Self { - x: self.x.clone(), - y: self.y.clone(), - } - } -} - -impl Display for Point { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "({}, {})", self.x, self.y) - } -} - -/// A structure representing a two-dimensional size with width and height in a given unit. -/// -/// This struct is generic over the type `T`, which can be any type that implements `Clone`, `Default`, and `Debug`. -/// It is commonly used to specify dimensions for elements in a UI, such as a window or element. -#[derive( - Add, Clone, Copy, Default, Deserialize, Div, Hash, Neg, PartialEq, Refineable, Serialize, Sub, -)] -#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -#[repr(C)] -pub struct Size { - /// The width component of the size. - pub width: T, - /// The height component of the size. - pub height: T, -} - -impl Size { - /// Create a new Size, a synonym for [`size`] - pub fn new(width: T, height: T) -> Self { - size(width, height) - } -} - -/// Constructs a new `Size` with the provided width and height. -/// -/// # Arguments -/// -/// * `width` - The width component of the `Size`. -/// * `height` - The height component of the `Size`. -/// -/// # Examples -/// -/// ``` -/// use gpui::size; -/// let my_size = size(10, 20); -/// assert_eq!(my_size.width, 10); -/// assert_eq!(my_size.height, 20); -/// ``` -pub const fn size(width: T, height: T) -> Size -where - T: Clone + Debug + Default + PartialEq, -{ - Size { width, height } -} - -impl Size -where - T: Clone + Debug + Default + PartialEq, -{ - /// Applies a function to the width and height of the size, producing a new `Size`. - /// - /// This method allows for converting a `Size` to a `Size` by specifying a closure - /// that defines how to convert between the two types. The closure is applied to both the `width` - /// and `height`, resulting in a new size of the desired type. - /// - /// # Arguments - /// - /// * `f` - A closure that takes a value of type `T` and returns a value of type `U`. - /// - /// # Examples - /// - /// ``` - /// # use gpui::Size; - /// let my_size = Size { width: 10, height: 20 }; - /// let my_new_size = my_size.map(|dimension| dimension as f32 * 1.5); - /// assert_eq!(my_new_size, Size { width: 15.0, height: 30.0 }); - /// ``` - pub fn map(&self, f: impl Fn(T) -> U) -> Size - where - U: Clone + Debug + Default + PartialEq, - { - Size { - width: f(self.width.clone()), - height: f(self.height.clone()), - } - } -} - -impl Size -where - T: Clone + Debug + Default + PartialEq + Half, -{ - /// Compute the center point of the size.g - pub fn center(&self) -> Point { - Point { - x: self.width.half(), - y: self.height.half(), - } - } -} - -impl Size { - /// Scales the size by a given factor. - /// - /// This method multiplies both the width and height by the provided scaling factor, - /// resulting in a new `Size` that is proportionally larger or smaller - /// depending on the factor. - /// - /// # Arguments - /// - /// * `factor` - The scaling factor to apply to the width and height. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Size, Pixels, ScaledPixels}; - /// let size = Size { width: Pixels::from(100.0), height: Pixels::from(50.0) }; - /// let scaled_size = size.scale(2.0); - /// assert_eq!(scaled_size, Size { width: ScaledPixels::from(200.0), height: ScaledPixels::from(100.0) }); - /// ``` - pub fn scale(&self, factor: f32) -> Size { - Size { - width: self.width.scale(factor), - height: self.height.scale(factor), - } - } -} - -impl Along for Size -where - T: Clone + Debug + Default + PartialEq, -{ - type Unit = T; - - fn along(&self, axis: Axis) -> T { - match axis { - Axis::Horizontal => self.width.clone(), - Axis::Vertical => self.height.clone(), - } - } - - /// Returns the value of this size along the given axis. - fn apply_along(&self, axis: Axis, f: impl FnOnce(T) -> T) -> Self { - match axis { - Axis::Horizontal => Size { - width: f(self.width.clone()), - height: self.height.clone(), - }, - Axis::Vertical => Size { - width: self.width.clone(), - height: f(self.height.clone()), - }, - } - } -} - -impl Size -where - T: PartialOrd + Clone + Debug + Default + PartialEq, -{ - /// Returns a new `Size` with the maximum width and height from `self` and `other`. - /// - /// # Arguments - /// - /// * `other` - A reference to another `Size` to compare with `self`. - /// - /// # Examples - /// - /// ``` - /// # use gpui::Size; - /// let size1 = Size { width: 30, height: 40 }; - /// let size2 = Size { width: 50, height: 20 }; - /// let max_size = size1.max(&size2); - /// assert_eq!(max_size, Size { width: 50, height: 40 }); - /// ``` - pub fn max(&self, other: &Self) -> Self { - Size { - width: if self.width >= other.width { - self.width.clone() - } else { - other.width.clone() - }, - height: if self.height >= other.height { - self.height.clone() - } else { - other.height.clone() - }, - } - } - - /// Returns a new `Size` with the minimum width and height from `self` and `other`. - /// - /// # Arguments - /// - /// * `other` - A reference to another `Size` to compare with `self`. - /// - /// # Examples - /// - /// ``` - /// # use gpui::Size; - /// let size1 = Size { width: 30, height: 40 }; - /// let size2 = Size { width: 50, height: 20 }; - /// let min_size = size1.min(&size2); - /// assert_eq!(min_size, Size { width: 30, height: 20 }); - /// ``` - pub fn min(&self, other: &Self) -> Self { - Size { - width: if self.width >= other.width { - other.width.clone() - } else { - self.width.clone() - }, - height: if self.height >= other.height { - other.height.clone() - } else { - self.height.clone() - }, - } - } -} - -impl Mul for Size -where - T: Mul + Clone + Debug + Default + PartialEq, - Rhs: Clone + Debug + Default + PartialEq, -{ - type Output = Size; - - fn mul(self, rhs: Rhs) -> Self::Output { - Size { - width: self.width * rhs.clone(), - height: self.height * rhs, - } - } -} - -impl MulAssign for Size -where - T: Mul + Clone + Debug + Default + PartialEq, - S: Clone, -{ - fn mul_assign(&mut self, rhs: S) { - self.width = self.width.clone() * rhs.clone(); - self.height = self.height.clone() * rhs; - } -} - -impl Eq for Size where T: Eq + Clone + Debug + Default + PartialEq {} - -impl Debug for Size -where - T: Clone + Debug + Default + PartialEq, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Size {{ {:?} × {:?} }}", self.width, self.height) - } -} - -impl Display for Size { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} × {}", self.width, self.height) - } -} - -impl From> for Size { - fn from(point: Point) -> Self { - Self { - width: point.x, - height: point.y, - } - } -} - -impl From> for Size { - fn from(size: Size) -> Self { - Size { - width: size.width.into(), - height: size.height.into(), - } - } -} - -impl From> for Size { - fn from(size: Size) -> Self { - Size { - width: size.width.into(), - height: size.height.into(), - } - } -} - -impl Size { - /// Returns a `Size` with both width and height set to fill the available space. - /// - /// This function creates a `Size` instance where both the width and height are set to `Length::Definite(DefiniteLength::Fraction(1.0))`, - /// which represents 100% of the available space in both dimensions. - /// - /// # Returns - /// - /// A `Size` that will fill the available space when used in a layout. - pub fn full() -> Self { - Self { - width: relative(1.).into(), - height: relative(1.).into(), - } - } -} - -impl Size { - /// Returns a `Size` with both width and height set to `auto`, which allows the layout engine to determine the size. - /// - /// This function creates a `Size` instance where both the width and height are set to `Length::Auto`, - /// indicating that their size should be computed based on the layout context, such as the content size or - /// available space. - /// - /// # Returns - /// - /// A `Size` with width and height set to `Length::Auto`. - pub fn auto() -> Self { - Self { - width: Length::Auto, - height: Length::Auto, - } - } -} - -/// Represents a rectangular area in a 2D space with an origin point and a size. -/// -/// The `Bounds` struct is generic over a type `T` which represents the type of the coordinate system. -/// The origin is represented as a `Point` which defines the top left corner of the rectangle, -/// and the size is represented as a `Size` which defines the width and height of the rectangle. -/// -/// # Examples -/// -/// ``` -/// # use gpui::{Bounds, Point, Size}; -/// let origin = Point { x: 0, y: 0 }; -/// let size = Size { width: 10, height: 20 }; -/// let bounds = Bounds::new(origin, size); -/// -/// assert_eq!(bounds.origin, origin); -/// assert_eq!(bounds.size, size); -/// ``` -#[derive(Refineable, Copy, Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)] -#[refineable(Debug)] -#[repr(C)] -pub struct Bounds { - /// The origin point of this area. - pub origin: Point, - /// The size of the rectangle. - pub size: Size, -} - -/// Create a bounds with the given origin and size -pub fn bounds( - origin: Point, - size: Size, -) -> Bounds { - Bounds { origin, size } -} - -impl Bounds { - /// Generate a centered bounds for the given display or primary display if none is provided - pub fn centered(display_id: Option, size: Size, cx: &App) -> Self { - let display = display_id - .and_then(|id| cx.find_display(id)) - .or_else(|| cx.primary_display()); - - display - .map(|display| Bounds::centered_at(display.bounds().center(), size)) - .unwrap_or_else(|| Bounds { - origin: point(px(0.), px(0.)), - size, - }) - } - - /// Generate maximized bounds for the given display or primary display if none is provided - pub fn maximized(display_id: Option, cx: &App) -> Self { - let display = display_id - .and_then(|id| cx.find_display(id)) - .or_else(|| cx.primary_display()); - - display - .map(|display| display.bounds()) - .unwrap_or_else(|| Bounds { - origin: point(px(0.), px(0.)), - size: size(px(1024.), px(768.)), - }) - } -} - -impl Bounds -where - T: Clone + Debug + Default + PartialEq, -{ - /// Creates a new `Bounds` with the specified origin and size. - /// - /// # Arguments - /// - /// * `origin` - A `Point` representing the origin of the bounds. - /// * `size` - A `Size` representing the size of the bounds. - /// - /// # Returns - /// - /// Returns a `Bounds` that has the given origin and size. - pub fn new(origin: Point, size: Size) -> Self { - Bounds { origin, size } - } -} - -impl Bounds -where - T: Sub + Clone + Debug + Default + PartialEq, -{ - /// Constructs a `Bounds` from two corner points: the top left and bottom right corners. - /// - /// This function calculates the origin and size of the `Bounds` based on the provided corner points. - /// The origin is set to the top left corner, and the size is determined by the difference between - /// the x and y coordinates of the bottom right and top left points. - /// - /// # Arguments - /// - /// * `top_left` - A `Point` representing the top left corner of the rectangle. - /// * `bottom_right` - A `Point` representing the bottom right corner of the rectangle. - /// - /// # Returns - /// - /// Returns a `Bounds` that encompasses the area defined by the two corner points. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point}; - /// let top_left = Point { x: 0, y: 0 }; - /// let bottom_right = Point { x: 10, y: 10 }; - /// let bounds = Bounds::from_corners(top_left, bottom_right); - /// - /// assert_eq!(bounds.origin, top_left); - /// assert_eq!(bounds.size.width, 10); - /// assert_eq!(bounds.size.height, 10); - /// ``` - pub fn from_corners(top_left: Point, bottom_right: Point) -> Self { - let origin = Point { - x: top_left.x.clone(), - y: top_left.y.clone(), - }; - let size = Size { - width: bottom_right.x - top_left.x, - height: bottom_right.y - top_left.y, - }; - Bounds { origin, size } - } -} - -impl Bounds -where - T: Sub + Half + Clone + Debug + Default + PartialEq, -{ - /// Constructs a `Bounds` from a corner point and size. The specified corner will be placed at - /// the specified origin. - pub fn from_anchor_and_size(corner: Anchor, origin: Point, size: Size) -> Bounds { - let origin = match corner { - Anchor::TopLeft => origin, - Anchor::TopRight => Point { - x: origin.x - size.width.clone(), - y: origin.y, - }, - Anchor::BottomLeft => Point { - x: origin.x, - y: origin.y - size.height.clone(), - }, - Anchor::BottomRight => Point { - x: origin.x - size.width.clone(), - y: origin.y - size.height.clone(), - }, - Anchor::TopCenter => Point { - x: origin.x - size.width.half(), - y: origin.y, - }, - Anchor::BottomCenter => Point { - x: origin.x - size.width.half(), - y: origin.y - size.height.clone(), - }, - Anchor::LeftCenter => Point { - x: origin.x, - y: origin.y - size.height.half(), - }, - Anchor::RightCenter => Point { - x: origin.x - size.width.clone(), - y: origin.y - size.height.half(), - }, - }; - - Bounds { origin, size } - } -} - -impl Bounds -where - T: Sub + Half + Clone + Debug + Default + PartialEq, -{ - /// Creates a new bounds centered at the given point. - pub fn centered_at(center: Point, size: Size) -> Self { - let origin = Point { - x: center.x - size.width.half(), - y: center.y - size.height.half(), - }; - Self::new(origin, size) - } -} - -impl Bounds -where - T: Add + Half + Clone + Debug + Default + PartialEq, -{ - /// Returns the top center point of the bounds. - pub fn top_center(&self) -> Point { - Point { - x: self.origin.x.clone() + self.size.width.half(), - y: self.origin.y.clone(), - } - } - - /// Returns the bottom center point of the bounds. - pub fn bottom_center(&self) -> Point { - Point { - x: self.origin.x.clone() + self.size.width.half(), - y: self.origin.y.clone() + self.size.height.clone(), - } - } - - /// Returns the left center point of the bounds. - pub fn left_center(&self) -> Point { - Point { - x: self.origin.x.clone(), - y: self.origin.y.clone() + self.size.height.half(), - } - } - - /// Returns the right center point of the bounds. - pub fn right_center(&self) -> Point { - Point { - x: self.origin.x.clone() + self.size.width.clone(), - y: self.origin.y.clone() + self.size.height.half(), - } - } -} - -impl Bounds -where - T: PartialOrd + Add + Clone + Debug + Default + PartialEq, -{ - /// Checks if this `Bounds` intersects with another `Bounds`. - /// - /// Two `Bounds` instances intersect if they overlap in the 2D space they occupy. - /// This method checks if there is any overlapping area between the two bounds. - /// - /// # Arguments - /// - /// * `other` - A reference to another `Bounds` to check for intersection with. - /// - /// # Returns - /// - /// Returns `true` if there is any intersection between the two bounds, `false` otherwise. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let bounds1 = Bounds { - /// origin: Point { x: 0, y: 0 }, - /// size: Size { width: 10, height: 10 }, - /// }; - /// let bounds2 = Bounds { - /// origin: Point { x: 5, y: 5 }, - /// size: Size { width: 10, height: 10 }, - /// }; - /// let bounds3 = Bounds { - /// origin: Point { x: 20, y: 20 }, - /// size: Size { width: 10, height: 10 }, - /// }; - /// - /// assert_eq!(bounds1.intersects(&bounds2), true); // Overlapping bounds - /// assert_eq!(bounds1.intersects(&bounds3), false); // Non-overlapping bounds - /// ``` - pub fn intersects(&self, other: &Bounds) -> bool { - let my_lower_right = self.bottom_right(); - let their_lower_right = other.bottom_right(); - - self.origin.x < their_lower_right.x - && my_lower_right.x > other.origin.x - && self.origin.y < their_lower_right.y - && my_lower_right.y > other.origin.y - } -} - -impl Bounds -where - T: Add + Half + Clone + Debug + Default + PartialEq, -{ - /// Returns the center point of the bounds. - /// - /// Calculates the center by taking the origin's x and y coordinates and adding half the width and height - /// of the bounds, respectively. The center is represented as a `Point` where `T` is the type of the - /// coordinate system. - /// - /// # Returns - /// - /// A `Point` representing the center of the bounds. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let bounds = Bounds { - /// origin: Point { x: 0, y: 0 }, - /// size: Size { width: 10, height: 20 }, - /// }; - /// let center = bounds.center(); - /// assert_eq!(center, Point { x: 5, y: 10 }); - /// ``` - pub fn center(&self) -> Point { - Point { - x: self.origin.x.clone() + self.size.width.clone().half(), - y: self.origin.y.clone() + self.size.height.clone().half(), - } - } -} - -impl Bounds -where - T: Add + Clone + Debug + Default + PartialEq, -{ - /// Calculates the half perimeter of a rectangle defined by the bounds. - /// - /// The half perimeter is calculated as the sum of the width and the height of the rectangle. - /// This method is generic over the type `T` which must implement the `Sub` trait to allow - /// calculation of the width and height from the bounds' origin and size, as well as the `Add` trait - /// to sum the width and height for the half perimeter. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let bounds = Bounds { - /// origin: Point { x: 0, y: 0 }, - /// size: Size { width: 10, height: 20 }, - /// }; - /// let half_perimeter = bounds.half_perimeter(); - /// assert_eq!(half_perimeter, 30); - /// ``` - pub fn half_perimeter(&self) -> T { - self.size.width.clone() + self.size.height.clone() - } -} - -impl Bounds -where - T: Add + Sub + Clone + Debug + Default + PartialEq, -{ - /// Dilates the bounds by a specified amount in all directions. - /// - /// This method expands the bounds by the given `amount`, increasing the size - /// and adjusting the origin so that the bounds grow outwards equally in all directions. - /// The resulting bounds will have its width and height increased by twice the `amount` - /// (since it grows in both directions), and the origin will be moved by `-amount` - /// in both the x and y directions. - /// - /// # Arguments - /// - /// * `amount` - The amount by which to dilate the bounds. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let mut bounds = Bounds { - /// origin: Point { x: 10, y: 10 }, - /// size: Size { width: 10, height: 10 }, - /// }; - /// let expanded_bounds = bounds.dilate(5); - /// assert_eq!(expanded_bounds, Bounds { - /// origin: Point { x: 5, y: 5 }, - /// size: Size { width: 20, height: 20 }, - /// }); - /// ``` - #[must_use] - pub fn dilate(&self, amount: T) -> Bounds { - let double_amount = amount.clone() + amount.clone(); - Bounds { - origin: self.origin.clone() - point(amount.clone(), amount), - size: self.size.clone() + size(double_amount.clone(), double_amount), - } - } - - /// Extends the bounds different amounts in each direction. - #[must_use] - pub fn extend(&self, amount: Edges) -> Bounds { - Bounds { - origin: self.origin.clone() - point(amount.left.clone(), amount.top.clone()), - size: self.size.clone() - + size( - amount.left.clone() + amount.right.clone(), - amount.top.clone() + amount.bottom, - ), - } - } -} - -impl Bounds -where - T: Add - + Sub - + Neg - + Clone - + Debug - + Default - + PartialEq, -{ - /// Inset the bounds by a specified amount. Equivalent to `dilate` with the amount negated. - /// - /// Note that this may panic if T does not support negative values. - pub fn inset(&self, amount: T) -> Self { - self.dilate(-amount) - } -} - -impl + Sub + Clone + Debug + Default + PartialEq> - Bounds -{ - /// Calculates the intersection of two `Bounds` objects. - /// - /// This method computes the overlapping region of two `Bounds`. If the bounds do not intersect, - /// the resulting `Bounds` will have a size with width and height of zero. - /// - /// # Arguments - /// - /// * `other` - A reference to another `Bounds` to intersect with. - /// - /// # Returns - /// - /// Returns a `Bounds` representing the intersection area. If there is no intersection, - /// the returned `Bounds` will have a size with width and height of zero. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let bounds1 = Bounds { - /// origin: Point { x: 0, y: 0 }, - /// size: Size { width: 10, height: 10 }, - /// }; - /// let bounds2 = Bounds { - /// origin: Point { x: 5, y: 5 }, - /// size: Size { width: 10, height: 10 }, - /// }; - /// let intersection = bounds1.intersect(&bounds2); - /// - /// assert_eq!(intersection, Bounds { - /// origin: Point { x: 5, y: 5 }, - /// size: Size { width: 5, height: 5 }, - /// }); - /// ``` - pub fn intersect(&self, other: &Self) -> Self { - let upper_left = self.origin.max(&other.origin); - let bottom_right = self - .bottom_right() - .min(&other.bottom_right()) - .max(&upper_left); - Self::from_corners(upper_left, bottom_right) - } - - /// Computes the union of two `Bounds`. - /// - /// This method calculates the smallest `Bounds` that contains both the current `Bounds` and the `other` `Bounds`. - /// The resulting `Bounds` will have an origin that is the minimum of the origins of the two `Bounds`, - /// and a size that encompasses the furthest extents of both `Bounds`. - /// - /// # Arguments - /// - /// * `other` - A reference to another `Bounds` to create a union with. - /// - /// # Returns - /// - /// Returns a `Bounds` representing the union of the two `Bounds`. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let bounds1 = Bounds { - /// origin: Point { x: 0, y: 0 }, - /// size: Size { width: 10, height: 10 }, - /// }; - /// let bounds2 = Bounds { - /// origin: Point { x: 5, y: 5 }, - /// size: Size { width: 15, height: 15 }, - /// }; - /// let union_bounds = bounds1.union(&bounds2); - /// - /// assert_eq!(union_bounds, Bounds { - /// origin: Point { x: 0, y: 0 }, - /// size: Size { width: 20, height: 20 }, - /// }); - /// ``` - pub fn union(&self, other: &Self) -> Self { - let top_left = self.origin.min(&other.origin); - let bottom_right = self.bottom_right().max(&other.bottom_right()); - Bounds::from_corners(top_left, bottom_right) - } -} - -impl Bounds -where - T: Add + Sub + Clone + Debug + Default + PartialEq, -{ - /// Computes the space available within outer bounds. - pub fn space_within(&self, outer: &Self) -> Edges { - Edges { - top: self.top() - outer.top(), - right: outer.right() - self.right(), - bottom: outer.bottom() - self.bottom(), - left: self.left() - outer.left(), - } - } -} - -impl Mul for Bounds -where - T: Mul + Clone + Debug + Default + PartialEq, - Point: Mul>, - Rhs: Clone + Debug + Default + PartialEq, -{ - type Output = Bounds; - - fn mul(self, rhs: Rhs) -> Self::Output { - Bounds { - origin: self.origin * rhs.clone(), - size: self.size * rhs, - } - } -} - -impl MulAssign for Bounds -where - T: Mul + Clone + Debug + Default + PartialEq, - S: Clone, -{ - fn mul_assign(&mut self, rhs: S) { - self.origin *= rhs.clone(); - self.size *= rhs; - } -} - -impl Div for Bounds -where - Size: Div>, - T: Div + Clone + Debug + Default + PartialEq, - S: Clone, -{ - type Output = Self; - - fn div(self, rhs: S) -> Self { - Self { - origin: self.origin / rhs.clone(), - size: self.size / rhs, - } - } -} - -impl Add> for Bounds -where - T: Add + Clone + Debug + Default + PartialEq, -{ - type Output = Self; - - fn add(self, rhs: Point) -> Self { - Self { - origin: self.origin + rhs, - size: self.size, - } - } -} - -impl Sub> for Bounds -where - T: Sub + Clone + Debug + Default + PartialEq, -{ - type Output = Self; - - fn sub(self, rhs: Point) -> Self { - Self { - origin: self.origin - rhs, - size: self.size, - } - } -} - -impl From> for Point { - fn from(size: Size) -> Self { - Self { - x: size.width, - y: size.height, - } - } -} - -impl Bounds -where - T: Add + Clone + Debug + Default + PartialEq, -{ - /// Returns the top edge of the bounds. - /// - /// # Returns - /// - /// A value of type `T` representing the y-coordinate of the top edge of the bounds. - pub fn top(&self) -> T { - self.origin.y.clone() - } - - /// Returns the bottom edge of the bounds. - /// - /// # Returns - /// - /// A value of type `T` representing the y-coordinate of the bottom edge of the bounds. - pub fn bottom(&self) -> T { - self.origin.y.clone() + self.size.height.clone() - } - - /// Returns the left edge of the bounds. - /// - /// # Returns - /// - /// A value of type `T` representing the x-coordinate of the left edge of the bounds. - pub fn left(&self) -> T { - self.origin.x.clone() - } - - /// Returns the right edge of the bounds. - /// - /// # Returns - /// - /// A value of type `T` representing the x-coordinate of the right edge of the bounds. - pub fn right(&self) -> T { - self.origin.x.clone() + self.size.width.clone() - } - - /// Returns the top right corner point of the bounds. - /// - /// # Returns - /// - /// A `Point` representing the top right corner of the bounds. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let bounds = Bounds { - /// origin: Point { x: 0, y: 0 }, - /// size: Size { width: 10, height: 20 }, - /// }; - /// let top_right = bounds.top_right(); - /// assert_eq!(top_right, Point { x: 10, y: 0 }); - /// ``` - pub fn top_right(&self) -> Point { - Point { - x: self.origin.x.clone() + self.size.width.clone(), - y: self.origin.y.clone(), - } - } - - /// Returns the bottom right corner point of the bounds. - /// - /// # Returns - /// - /// A `Point` representing the bottom right corner of the bounds. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let bounds = Bounds { - /// origin: Point { x: 0, y: 0 }, - /// size: Size { width: 10, height: 20 }, - /// }; - /// let bottom_right = bounds.bottom_right(); - /// assert_eq!(bottom_right, Point { x: 10, y: 20 }); - /// ``` - pub fn bottom_right(&self) -> Point { - Point { - x: self.origin.x.clone() + self.size.width.clone(), - y: self.origin.y.clone() + self.size.height.clone(), - } - } - - /// Returns the bottom left corner point of the bounds. - /// - /// # Returns - /// - /// A `Point` representing the bottom left corner of the bounds. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let bounds = Bounds { - /// origin: Point { x: 0, y: 0 }, - /// size: Size { width: 10, height: 20 }, - /// }; - /// let bottom_left = bounds.bottom_left(); - /// assert_eq!(bottom_left, Point { x: 0, y: 20 }); - /// ``` - pub fn bottom_left(&self) -> Point { - Point { - x: self.origin.x.clone(), - y: self.origin.y.clone() + self.size.height.clone(), - } - } -} - -impl Bounds -where - T: Add + Half + Clone + Debug + Default + PartialEq, -{ - /// Returns the requested corner point of the bounds. - /// - /// # Returns - /// - /// A `Point` representing the corner of the bounds requested by the parameter. - /// - /// # Examples - /// - /// ``` - /// use gpui::{Bounds, Anchor, Point, Size}; - /// let bounds = Bounds { - /// origin: Point { x: 0, y: 0 }, - /// size: Size { width: 10, height: 20 }, - /// }; - /// let bottom_left = bounds.corner(Anchor::BottomLeft); - /// assert_eq!(bottom_left, Point { x: 0, y: 20 }); - /// ``` - pub fn corner(&self, corner: Anchor) -> Point { - match corner { - Anchor::TopLeft => self.origin.clone(), - Anchor::TopRight => self.top_right(), - Anchor::BottomLeft => self.bottom_left(), - Anchor::BottomRight => self.bottom_right(), - Anchor::TopCenter => self.top_center(), - Anchor::BottomCenter => self.bottom_center(), - Anchor::LeftCenter => self.left_center(), - Anchor::RightCenter => self.right_center(), - } - } -} - -impl Bounds -where - T: Add + PartialOrd + Clone + Debug + Default + PartialEq, -{ - /// Checks if the given point is within the bounds. - /// - /// This method determines whether a point lies inside the rectangle defined by the bounds, - /// including the edges. The point is considered inside if its x-coordinate is greater than - /// or equal to the left edge and less than or equal to the right edge, and its y-coordinate - /// is greater than or equal to the top edge and less than or equal to the bottom edge of the bounds. - /// - /// # Arguments - /// - /// * `point` - A reference to a `Point` that represents the point to check. - /// - /// # Returns - /// - /// Returns `true` if the point is within the bounds, `false` otherwise. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Point, Bounds, Size}; - /// let bounds = Bounds { - /// origin: Point { x: 0, y: 0 }, - /// size: Size { width: 10, height: 10 }, - /// }; - /// let inside_point = Point { x: 5, y: 5 }; - /// let outside_point = Point { x: 15, y: 15 }; - /// - /// assert!(bounds.contains(&inside_point)); - /// assert!(!bounds.contains(&outside_point)); - /// ``` - pub fn contains(&self, point: &Point) -> bool { - point.x >= self.origin.x - && point.x < self.origin.x.clone() + self.size.width.clone() - && point.y >= self.origin.y - && point.y < self.origin.y.clone() + self.size.height.clone() - } - - /// Checks if this bounds is completely contained within another bounds. - /// - /// This method determines whether the current bounds is entirely enclosed by the given bounds. - /// A bounds is considered to be contained within another if its origin (top-left corner) and - /// its bottom-right corner are both contained within the other bounds. - /// - /// # Arguments - /// - /// * `other` - A reference to another `Bounds` that might contain this bounds. - /// - /// # Returns - /// - /// Returns `true` if this bounds is completely inside the other bounds, `false` otherwise. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let outer_bounds = Bounds { - /// origin: Point { x: 0, y: 0 }, - /// size: Size { width: 20, height: 20 }, - /// }; - /// let inner_bounds = Bounds { - /// origin: Point { x: 5, y: 5 }, - /// size: Size { width: 10, height: 10 }, - /// }; - /// let overlapping_bounds = Bounds { - /// origin: Point { x: 15, y: 15 }, - /// size: Size { width: 10, height: 10 }, - /// }; - /// - /// assert!(inner_bounds.is_contained_within(&outer_bounds)); - /// assert!(!overlapping_bounds.is_contained_within(&outer_bounds)); - /// ``` - pub fn is_contained_within(&self, other: &Self) -> bool { - other.contains(&self.origin) && other.contains(&self.bottom_right()) - } - - /// Applies a function to the origin and size of the bounds, producing a new `Bounds`. - /// - /// This method allows for converting a `Bounds` to a `Bounds` by specifying a closure - /// that defines how to convert between the two types. The closure is applied to the `origin` and - /// `size` fields, resulting in new bounds of the desired type. - /// - /// # Arguments - /// - /// * `f` - A closure that takes a value of type `T` and returns a value of type `U`. - /// - /// # Returns - /// - /// Returns a new `Bounds` with the origin and size mapped by the provided function. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let bounds = Bounds { - /// origin: Point { x: 10.0, y: 10.0 }, - /// size: Size { width: 10.0, height: 20.0 }, - /// }; - /// let new_bounds = bounds.map(|value| value as f64 * 1.5); - /// - /// assert_eq!(new_bounds, Bounds { - /// origin: Point { x: 15.0, y: 15.0 }, - /// size: Size { width: 15.0, height: 30.0 }, - /// }); - /// ``` - pub fn map(&self, f: impl Fn(T) -> U) -> Bounds - where - U: Clone + Debug + Default + PartialEq, - { - Bounds { - origin: self.origin.map(&f), - size: self.size.map(f), - } - } - - /// Applies a function to the origin of the bounds, producing a new `Bounds` with the new origin - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let bounds = Bounds { - /// origin: Point { x: 10.0, y: 10.0 }, - /// size: Size { width: 10.0, height: 20.0 }, - /// }; - /// let new_bounds = bounds.map_origin(|value| value * 1.5); - /// - /// assert_eq!(new_bounds, Bounds { - /// origin: Point { x: 15.0, y: 15.0 }, - /// size: Size { width: 10.0, height: 20.0 }, - /// }); - /// ``` - pub fn map_origin(self, f: impl Fn(T) -> T) -> Bounds { - Bounds { - origin: self.origin.map(f), - size: self.size, - } - } - - /// Applies a function to the origin of the bounds, producing a new `Bounds` with the new origin - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size}; - /// let bounds = Bounds { - /// origin: Point { x: 10.0, y: 10.0 }, - /// size: Size { width: 10.0, height: 20.0 }, - /// }; - /// let new_bounds = bounds.map_size(|value| value * 1.5); - /// - /// assert_eq!(new_bounds, Bounds { - /// origin: Point { x: 10.0, y: 10.0 }, - /// size: Size { width: 15.0, height: 30.0 }, - /// }); - /// ``` - pub fn map_size(self, f: impl Fn(T) -> T) -> Bounds { - Bounds { - origin: self.origin, - size: self.size.map(f), - } - } -} - -impl Bounds -where - T: Add + Sub + PartialOrd + Clone + Debug + Default + PartialEq, -{ - /// Convert a point to the coordinate space defined by this Bounds - pub fn localize(&self, point: &Point) -> Option> { - self.contains(point) - .then(|| point.relative_to(&self.origin)) - } -} - -/// Checks if the bounds represent an empty area. -/// -/// # Returns -/// -/// Returns `true` if either the width or the height of the bounds is less than or equal to zero, indicating an empty area. -impl Bounds { - /// Checks if the bounds represent an empty area. - /// - /// # Returns - /// - /// Returns `true` if either the width or the height of the bounds is less than or equal to zero, indicating an empty area. - #[must_use] - pub fn is_empty(&self) -> bool { - self.size.width <= T::default() || self.size.height <= T::default() - } -} - -impl> Display for Bounds { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{} - {} (size {})", - self.origin, - self.bottom_right(), - self.size - ) - } -} - -impl Size { - /// Converts the size from physical to logical pixels. - pub fn to_pixels(self, scale_factor: f32) -> Size { - size( - px(self.width.0 as f32 / scale_factor), - px(self.height.0 as f32 / scale_factor), - ) - } -} - -impl Size { - /// Converts the size from logical to physical pixels. - pub fn to_device_pixels(self, scale_factor: f32) -> Size { - size( - DevicePixels((self.width.0 * scale_factor).round() as i32), - DevicePixels((self.height.0 * scale_factor).round() as i32), - ) - } -} - -impl Bounds { - /// Scales the bounds by a given factor, typically used to adjust for display scaling. - /// - /// This method multiplies the origin and size of the bounds by the provided scaling factor, - /// resulting in a new `Bounds` that is proportionally larger or smaller - /// depending on the scaling factor. This can be used to ensure that the bounds are properly - /// scaled for different display densities. - /// - /// # Arguments - /// - /// * `factor` - The scaling factor to apply to the origin and size, typically the display's scaling factor. - /// - /// # Returns - /// - /// Returns a new `Bounds` that represents the scaled bounds. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Bounds, Point, Size, Pixels, ScaledPixels, DevicePixels}; - /// let bounds = Bounds { - /// origin: Point { x: Pixels::from(10.0), y: Pixels::from(20.0) }, - /// size: Size { width: Pixels::from(30.0), height: Pixels::from(40.0) }, - /// }; - /// let display_scale_factor = 2.0; - /// let scaled_bounds = bounds.scale(display_scale_factor); - /// assert_eq!(scaled_bounds, Bounds { - /// origin: Point { - /// x: ScaledPixels::from(20.0), - /// y: ScaledPixels::from(40.0), - /// }, - /// size: Size { - /// width: ScaledPixels::from(60.0), - /// height: ScaledPixels::from(80.0) - /// }, - /// }); - /// ``` - pub fn scale(&self, factor: f32) -> Bounds { - Bounds { - origin: self.origin.scale(factor), - size: self.size.scale(factor), - } - } - - /// Convert the bounds from logical pixels to physical pixels - pub fn to_device_pixels(self, factor: f32) -> Bounds { - Bounds { - origin: point( - DevicePixels((self.origin.x.0 * factor).round() as i32), - DevicePixels((self.origin.y.0 * factor).round() as i32), - ), - size: self.size.to_device_pixels(factor), - } - } -} - -impl Bounds { - /// Convert the bounds from physical pixels to logical pixels - pub fn to_pixels(self, scale_factor: f32) -> Bounds { - Bounds { - origin: point( - px(self.origin.x.0 as f32 / scale_factor), - px(self.origin.y.0 as f32 / scale_factor), - ), - size: self.size.to_pixels(scale_factor), - } - } -} - -/// Represents the edges of a box in a 2D space, such as padding or margin. -/// -/// Each field represents the size of the edge on one side of the box: `top`, `right`, `bottom`, and `left`. -/// -/// # Examples -/// -/// ``` -/// # use gpui::Edges; -/// let edges = Edges { -/// top: 10.0, -/// right: 20.0, -/// bottom: 30.0, -/// left: 40.0, -/// }; -/// -/// assert_eq!(edges.top, 10.0); -/// assert_eq!(edges.right, 20.0); -/// assert_eq!(edges.bottom, 30.0); -/// assert_eq!(edges.left, 40.0); -/// ``` -#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)] -#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -#[repr(C)] -pub struct Edges { - /// The size of the top edge. - pub top: T, - /// The size of the right edge. - pub right: T, - /// The size of the bottom edge. - pub bottom: T, - /// The size of the left edge. - pub left: T, -} - -impl Mul for Edges -where - T: Mul + Clone + Debug + Default + PartialEq, -{ - type Output = Self; - - fn mul(self, rhs: Self) -> Self::Output { - Self { - top: self.top.clone() * rhs.top, - right: self.right.clone() * rhs.right, - bottom: self.bottom.clone() * rhs.bottom, - left: self.left * rhs.left, - } - } -} - -impl MulAssign for Edges -where - T: Mul + Clone + Debug + Default + PartialEq, - S: Clone, -{ - fn mul_assign(&mut self, rhs: S) { - self.top = self.top.clone() * rhs.clone(); - self.right = self.right.clone() * rhs.clone(); - self.bottom = self.bottom.clone() * rhs.clone(); - self.left = self.left.clone() * rhs; - } -} - -impl Copy for Edges {} - -impl Edges { - /// Constructs `Edges` where all sides are set to the same specified value. - /// - /// This function creates an `Edges` instance with the `top`, `right`, `bottom`, and `left` fields all initialized - /// to the same value provided as an argument. This is useful when you want to have uniform edges around a box, - /// such as padding or margin with the same size on all sides. - /// - /// # Arguments - /// - /// * `value` - The value to set for all four sides of the edges. - /// - /// # Returns - /// - /// An `Edges` instance with all sides set to the given value. - /// - /// # Examples - /// - /// ``` - /// # use gpui::Edges; - /// let uniform_edges = Edges::all(10.0); - /// assert_eq!(uniform_edges.top, 10.0); - /// assert_eq!(uniform_edges.right, 10.0); - /// assert_eq!(uniform_edges.bottom, 10.0); - /// assert_eq!(uniform_edges.left, 10.0); - /// ``` - pub fn all(value: T) -> Self { - Self { - top: value.clone(), - right: value.clone(), - bottom: value.clone(), - left: value, - } - } - - /// Applies a function to each field of the `Edges`, producing a new `Edges`. - /// - /// This method allows for converting an `Edges` to an `Edges` by specifying a closure - /// that defines how to convert between the two types. The closure is applied to each field - /// (`top`, `right`, `bottom`, `left`), resulting in new edges of the desired type. - /// - /// # Arguments - /// - /// * `f` - A closure that takes a reference to a value of type `T` and returns a value of type `U`. - /// - /// # Returns - /// - /// Returns a new `Edges` with each field mapped by the provided function. - /// - /// # Examples - /// - /// ``` - /// # use gpui::Edges; - /// let edges = Edges { top: 10, right: 20, bottom: 30, left: 40 }; - /// let edges_float = edges.map(|&value| value as f32 * 1.1); - /// assert_eq!(edges_float, Edges { top: 11.0, right: 22.0, bottom: 33.0, left: 44.0 }); - /// ``` - pub fn map(&self, f: impl Fn(&T) -> U) -> Edges - where - U: Clone + Debug + Default + PartialEq, - { - Edges { - top: f(&self.top), - right: f(&self.right), - bottom: f(&self.bottom), - left: f(&self.left), - } - } - - /// Checks if any of the edges satisfy a given predicate. - /// - /// This method applies a predicate function to each field of the `Edges` and returns `true` if any field satisfies the predicate. - /// - /// # Arguments - /// - /// * `predicate` - A closure that takes a reference to a value of type `T` and returns a `bool`. - /// - /// # Returns - /// - /// Returns `true` if the predicate returns `true` for any of the edge values, `false` otherwise. - /// - /// # Examples - /// - /// ``` - /// # use gpui::Edges; - /// let edges = Edges { - /// top: 10, - /// right: 0, - /// bottom: 5, - /// left: 0, - /// }; - /// - /// assert!(edges.any(|value| *value == 0)); - /// assert!(edges.any(|value| *value > 0)); - /// assert!(!edges.any(|value| *value > 10)); - /// ``` - pub fn any bool>(&self, predicate: F) -> bool { - predicate(&self.top) - || predicate(&self.right) - || predicate(&self.bottom) - || predicate(&self.left) - } -} - -impl Edges { - /// Sets the edges of the `Edges` struct to `auto`, which is a special value that allows the layout engine to automatically determine the size of the edges. - /// - /// This is typically used in layout contexts where the exact size of the edges is not important, or when the size should be calculated based on the content or container. - /// - /// # Returns - /// - /// Returns an `Edges` with all edges set to `Length::Auto`. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Edges, Length}; - /// let auto_edges = Edges::auto(); - /// assert_eq!(auto_edges.top, Length::Auto); - /// assert_eq!(auto_edges.right, Length::Auto); - /// assert_eq!(auto_edges.bottom, Length::Auto); - /// assert_eq!(auto_edges.left, Length::Auto); - /// ``` - pub fn auto() -> Self { - Self { - top: Length::Auto, - right: Length::Auto, - bottom: Length::Auto, - left: Length::Auto, - } - } - - /// Sets the edges of the `Edges` struct to zero, which means no size or thickness. - /// - /// This is typically used when you want to specify that a box (like a padding or margin area) - /// should have no edges, effectively making it non-existent or invisible in layout calculations. - /// - /// # Returns - /// - /// Returns an `Edges` with all edges set to zero length. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{DefiniteLength, Edges, Length, Pixels}; - /// let no_edges = Edges::::zero(); - /// assert_eq!(no_edges.top, Length::Definite(DefiniteLength::from(Pixels::ZERO))); - /// assert_eq!(no_edges.right, Length::Definite(DefiniteLength::from(Pixels::ZERO))); - /// assert_eq!(no_edges.bottom, Length::Definite(DefiniteLength::from(Pixels::ZERO))); - /// assert_eq!(no_edges.left, Length::Definite(DefiniteLength::from(Pixels::ZERO))); - /// ``` - pub fn zero() -> Self { - Self { - top: px(0.).into(), - right: px(0.).into(), - bottom: px(0.).into(), - left: px(0.).into(), - } - } -} - -impl Edges { - /// Sets the edges of the `Edges` struct to zero, which means no size or thickness. - /// - /// This is typically used when you want to specify that a box (like a padding or margin area) - /// should have no edges, effectively making it non-existent or invisible in layout calculations. - /// - /// # Returns - /// - /// Returns an `Edges` with all edges set to zero length. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{px, DefiniteLength, Edges}; - /// let no_edges = Edges::::zero(); - /// assert_eq!(no_edges.top, DefiniteLength::from(px(0.))); - /// assert_eq!(no_edges.right, DefiniteLength::from(px(0.))); - /// assert_eq!(no_edges.bottom, DefiniteLength::from(px(0.))); - /// assert_eq!(no_edges.left, DefiniteLength::from(px(0.))); - /// ``` - pub fn zero() -> Self { - Self { - top: px(0.).into(), - right: px(0.).into(), - bottom: px(0.).into(), - left: px(0.).into(), - } - } - - /// Converts the `DefiniteLength` to `Pixels` based on the parent size and the REM size. - /// - /// This method allows for a `DefiniteLength` value to be converted into pixels, taking into account - /// the size of the parent element (for percentage-based lengths) and the size of a rem unit (for rem-based lengths). - /// - /// # Arguments - /// - /// * `parent_size` - `Size` representing the size of the parent element. - /// * `rem_size` - `Pixels` representing the size of one REM unit. - /// - /// # Returns - /// - /// Returns an `Edges` representing the edges with lengths converted to pixels. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Edges, DefiniteLength, px, AbsoluteLength, rems, Size}; - /// let edges = Edges { - /// top: DefiniteLength::Absolute(AbsoluteLength::Pixels(px(10.0))), - /// right: DefiniteLength::Fraction(0.5), - /// bottom: DefiniteLength::Absolute(AbsoluteLength::Rems(rems(2.0))), - /// left: DefiniteLength::Fraction(0.25), - /// }; - /// let parent_size = Size { - /// width: AbsoluteLength::Pixels(px(200.0)), - /// height: AbsoluteLength::Pixels(px(100.0)), - /// }; - /// let rem_size = px(16.0); - /// let edges_in_pixels = edges.to_pixels(parent_size, rem_size); - /// - /// assert_eq!(edges_in_pixels.top, px(10.0)); // Absolute length in pixels - /// assert_eq!(edges_in_pixels.right, px(100.0)); // 50% of parent width - /// assert_eq!(edges_in_pixels.bottom, px(32.0)); // 2 rems - /// assert_eq!(edges_in_pixels.left, px(50.0)); // 25% of parent width - /// ``` - pub fn to_pixels(self, parent_size: Size, rem_size: Pixels) -> Edges { - Edges { - top: self.top.to_pixels(parent_size.height, rem_size), - right: self.right.to_pixels(parent_size.width, rem_size), - bottom: self.bottom.to_pixels(parent_size.height, rem_size), - left: self.left.to_pixels(parent_size.width, rem_size), - } - } -} - -impl Edges { - /// Sets the edges of the `Edges` struct to zero, which means no size or thickness. - /// - /// This is typically used when you want to specify that a box (like a padding or margin area) - /// should have no edges, effectively making it non-existent or invisible in layout calculations. - /// - /// # Returns - /// - /// Returns an `Edges` with all edges set to zero length. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{AbsoluteLength, Edges, Pixels}; - /// let no_edges = Edges::::zero(); - /// assert_eq!(no_edges.top, AbsoluteLength::Pixels(Pixels::ZERO)); - /// assert_eq!(no_edges.right, AbsoluteLength::Pixels(Pixels::ZERO)); - /// assert_eq!(no_edges.bottom, AbsoluteLength::Pixels(Pixels::ZERO)); - /// assert_eq!(no_edges.left, AbsoluteLength::Pixels(Pixels::ZERO)); - /// ``` - pub fn zero() -> Self { - Self { - top: px(0.).into(), - right: px(0.).into(), - bottom: px(0.).into(), - left: px(0.).into(), - } - } - - /// Converts the `AbsoluteLength` to `Pixels` based on the `rem_size`. - /// - /// If the `AbsoluteLength` is already in pixels, it simply returns the corresponding `Pixels` value. - /// If the `AbsoluteLength` is in rems, it multiplies the number of rems by the `rem_size` to convert it to pixels. - /// - /// # Arguments - /// - /// * `rem_size` - The size of one rem unit in pixels. - /// - /// # Returns - /// - /// Returns an `Edges` representing the edges with lengths converted to pixels. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Edges, AbsoluteLength, Pixels, px, rems}; - /// let edges = Edges { - /// top: AbsoluteLength::Pixels(px(10.0)), - /// right: AbsoluteLength::Rems(rems(1.0)), - /// bottom: AbsoluteLength::Pixels(px(20.0)), - /// left: AbsoluteLength::Rems(rems(2.0)), - /// }; - /// let rem_size = px(16.0); - /// let edges_in_pixels = edges.to_pixels(rem_size); - /// - /// assert_eq!(edges_in_pixels.top, px(10.0)); // Already in pixels - /// assert_eq!(edges_in_pixels.right, px(16.0)); // 1 rem converted to pixels - /// assert_eq!(edges_in_pixels.bottom, px(20.0)); // Already in pixels - /// assert_eq!(edges_in_pixels.left, px(32.0)); // 2 rems converted to pixels - /// ``` - pub fn to_pixels(self, rem_size: Pixels) -> Edges { - Edges { - top: self.top.to_pixels(rem_size), - right: self.right.to_pixels(rem_size), - bottom: self.bottom.to_pixels(rem_size), - left: self.left.to_pixels(rem_size), - } - } -} - -impl Edges { - /// Scales the `Edges` by a given factor, returning `Edges`. - /// - /// This method is typically used for adjusting the edge sizes for different display densities or scaling factors. - /// - /// # Arguments - /// - /// * `factor` - The scaling factor to apply to each edge. - /// - /// # Returns - /// - /// Returns a new `Edges` where each edge is the result of scaling the original edge by the given factor. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Edges, Pixels, ScaledPixels}; - /// let edges = Edges { - /// top: Pixels::from(10.0), - /// right: Pixels::from(20.0), - /// bottom: Pixels::from(30.0), - /// left: Pixels::from(40.0), - /// }; - /// let scaled_edges = edges.scale(2.0); - /// assert_eq!(scaled_edges.top, ScaledPixels::from(20.0)); - /// assert_eq!(scaled_edges.right, ScaledPixels::from(40.0)); - /// assert_eq!(scaled_edges.bottom, ScaledPixels::from(60.0)); - /// assert_eq!(scaled_edges.left, ScaledPixels::from(80.0)); - /// ``` - pub fn scale(&self, factor: f32) -> Edges { - Edges { - top: self.top.scale(factor), - right: self.right.scale(factor), - bottom: self.bottom.scale(factor), - left: self.left.scale(factor), - } - } - - /// Returns the maximum value of any edge. - /// - /// # Returns - /// - /// The maximum `Pixels` value among all four edges. - pub fn max(&self) -> Pixels { - self.top.max(self.right).max(self.bottom).max(self.left) - } -} - -impl From for Edges { - fn from(val: f32) -> Self { - let val: Pixels = val.into(); - val.into() - } -} - -impl From for Edges { - fn from(val: Pixels) -> Self { - Edges { - top: val, - right: val, - bottom: val, - left: val, - } - } -} - -/// Identifies a reference point on a 2D box, used to anchor positioned elements. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Anchor { - /// The top left corner - TopLeft, - /// The top right corner - TopRight, - /// The bottom left corner - BottomLeft, - /// The bottom right corner - BottomRight, - /// The top center position - TopCenter, - /// The bottom center position - BottomCenter, - /// The left center position - LeftCenter, - /// The right center position - RightCenter, -} - -impl Anchor { - /// Returns the directly opposite corner. - /// - /// # Examples - /// - /// ``` - /// # use gpui::Anchor; - /// assert_eq!(Anchor::TopLeft.opposite(), Anchor::BottomRight); - /// ``` - #[must_use] - pub fn opposite(self) -> Self { - match self { - Anchor::TopLeft => Anchor::BottomRight, - Anchor::TopRight => Anchor::BottomLeft, - Anchor::BottomLeft => Anchor::TopRight, - Anchor::BottomRight => Anchor::TopLeft, - Anchor::TopCenter => Anchor::BottomCenter, - Anchor::BottomCenter => Anchor::TopCenter, - Anchor::LeftCenter => Anchor::RightCenter, - Anchor::RightCenter => Anchor::LeftCenter, - } - } - - /// Returns the corner across from this corner, moving along the specified axis. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Axis, Anchor}; - /// let result = Anchor::TopLeft.other_side_along(Axis::Horizontal); - /// assert_eq!(result, Anchor::TopRight); - /// ``` - #[must_use] - pub fn other_side_along(self, axis: Axis) -> Self { - match axis { - Axis::Vertical => match self { - Anchor::TopLeft => Anchor::BottomLeft, - Anchor::TopRight => Anchor::BottomRight, - Anchor::BottomLeft => Anchor::TopLeft, - Anchor::BottomRight => Anchor::TopRight, - Anchor::TopCenter => Anchor::BottomCenter, - Anchor::BottomCenter => Anchor::TopCenter, - Anchor::LeftCenter => Anchor::LeftCenter, - Anchor::RightCenter => Anchor::RightCenter, - }, - Axis::Horizontal => match self { - Anchor::TopLeft => Anchor::TopRight, - Anchor::TopRight => Anchor::TopLeft, - Anchor::BottomLeft => Anchor::BottomRight, - Anchor::BottomRight => Anchor::BottomLeft, - Anchor::TopCenter => Anchor::TopCenter, - Anchor::BottomCenter => Anchor::BottomCenter, - Anchor::LeftCenter => Anchor::RightCenter, - Anchor::RightCenter => Anchor::LeftCenter, - }, - } - } - - /// Returns true if at the center. - #[inline] - pub fn is_center(&self) -> bool { - matches!( - self, - Self::TopCenter | Self::BottomCenter | Self::LeftCenter | Self::RightCenter - ) - } -} - -/// Represents the corners of a box in a 2D space, such as border radius. -/// -/// Each field represents the size of the corner on one side of the box: `top_left`, `top_right`, `bottom_right`, and `bottom_left`. -#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)] -#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -#[repr(C)] -pub struct Corners { - /// The value associated with the top left corner. - pub top_left: T, - /// The value associated with the top right corner. - pub top_right: T, - /// The value associated with the bottom right corner. - pub bottom_right: T, - /// The value associated with the bottom left corner. - pub bottom_left: T, -} - -impl Corners -where - T: Add + Half + Clone + Debug + Default + PartialEq, -{ - /// Constructs `Corners` where all sides are set to the same specified value. - /// - /// This function creates a `Corners` instance with the `top_left`, `top_right`, `bottom_right`, and `bottom_left` fields all initialized - /// to the same value provided as an argument. This is useful when you want to have uniform corners around a box, - /// such as a uniform border radius on a rectangle. - /// - /// # Arguments - /// - /// * `value` - The value to set for all four corners. - /// - /// # Returns - /// - /// An `Corners` instance with all corners set to the given value. - /// - /// # Examples - /// - /// ``` - /// # use gpui::Corners; - /// let uniform_corners = Corners::all(5.0); - /// assert_eq!(uniform_corners.top_left, 5.0); - /// assert_eq!(uniform_corners.top_right, 5.0); - /// assert_eq!(uniform_corners.bottom_right, 5.0); - /// assert_eq!(uniform_corners.bottom_left, 5.0); - /// ``` - pub fn all(value: T) -> Self { - Self { - top_left: value.clone(), - top_right: value.clone(), - bottom_right: value.clone(), - bottom_left: value, - } - } - - /// Returns the requested corner value, supporting all eight corner positions. - /// - /// For the four basic corners (TopLeft, TopRight, BottomLeft, BottomRight), - /// this returns the corresponding field value directly. - /// - /// For the center positions (TopCenter, BottomCenter, LeftCenter, RightCenter), - /// this calculates the average of the two adjacent corners. - /// - /// # Returns - /// - /// A value of type `T` representing the corner requested by the parameter. - /// - /// # Examples - /// - /// Basic corner positions: - /// - /// ``` - /// # use gpui::{Anchor, Corners}; - /// let corners = Corners { - /// top_left: 10, - /// top_right: 20, - /// bottom_left: 30, - /// bottom_right: 40 - /// }; - /// assert_eq!(corners.corner(Anchor::TopLeft), 10); - /// assert_eq!(corners.corner(Anchor::BottomRight), 40); - /// ``` - /// - /// Center positions (calculated as average of adjacent corners): - /// - /// ``` - /// # use gpui::{Anchor, Corners}; - /// let corners = Corners { - /// top_left: 10, - /// top_right: 20, - /// bottom_left: 30, - /// bottom_right: 40 - /// }; - /// assert_eq!(corners.corner(Anchor::TopCenter), 15); - /// assert_eq!(corners.corner(Anchor::BottomCenter), 35); - /// assert_eq!(corners.corner(Anchor::LeftCenter), 20); - /// assert_eq!(corners.corner(Anchor::RightCenter), 30); - /// ``` - #[must_use] - pub fn corner(&self, corner: Anchor) -> T { - match corner { - Anchor::TopLeft => self.top_left.clone(), - Anchor::TopRight => self.top_right.clone(), - Anchor::BottomLeft => self.bottom_left.clone(), - Anchor::BottomRight => self.bottom_right.clone(), - Anchor::TopCenter => (self.top_left.clone() + self.top_right.clone()).half(), - Anchor::BottomCenter => (self.bottom_left.clone() + self.bottom_right.clone()).half(), - Anchor::LeftCenter => (self.top_left.clone() + self.bottom_left.clone()).half(), - Anchor::RightCenter => (self.top_right.clone() + self.bottom_right.clone()).half(), - } - } -} - -impl Corners { - /// Converts the `AbsoluteLength` to `Pixels` based on the provided rem size. - /// - /// # Arguments - /// - /// * `rem_size` - The size of one REM unit in pixels, used for conversion if the `AbsoluteLength` is in REMs. - /// - /// # Returns - /// - /// Returns a `Corners` instance with each corner's length converted to pixels. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Corners, AbsoluteLength, Pixels, Rems, Size}; - /// let corners = Corners { - /// top_left: AbsoluteLength::Pixels(Pixels::from(15.0)), - /// top_right: AbsoluteLength::Rems(Rems(1.0)), - /// bottom_right: AbsoluteLength::Pixels(Pixels::from(30.0)), - /// bottom_left: AbsoluteLength::Rems(Rems(2.0)), - /// }; - /// let rem_size = Pixels::from(16.0); - /// let corners_in_pixels = corners.to_pixels(rem_size); - /// - /// assert_eq!(corners_in_pixels.top_left, Pixels::from(15.0)); - /// assert_eq!(corners_in_pixels.top_right, Pixels::from(16.0)); // 1 rem converted to pixels - /// assert_eq!(corners_in_pixels.bottom_right, Pixels::from(30.0)); - /// assert_eq!(corners_in_pixels.bottom_left, Pixels::from(32.0)); // 2 rems converted to pixels - /// ``` - pub fn to_pixels(self, rem_size: Pixels) -> Corners { - Corners { - top_left: self.top_left.to_pixels(rem_size), - top_right: self.top_right.to_pixels(rem_size), - bottom_right: self.bottom_right.to_pixels(rem_size), - bottom_left: self.bottom_left.to_pixels(rem_size), - } - } -} - -impl Corners { - /// Scales the `Corners` by a given factor, returning `Corners`. - /// - /// This method is typically used for adjusting the corner sizes for different display densities or scaling factors. - /// - /// # Arguments - /// - /// * `factor` - The scaling factor to apply to each corner. - /// - /// # Returns - /// - /// Returns a new `Corners` where each corner is the result of scaling the original corner by the given factor. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Corners, Pixels, ScaledPixels}; - /// let corners = Corners { - /// top_left: Pixels::from(10.0), - /// top_right: Pixels::from(20.0), - /// bottom_right: Pixels::from(30.0), - /// bottom_left: Pixels::from(40.0), - /// }; - /// let scaled_corners = corners.scale(2.0); - /// assert_eq!(scaled_corners.top_left, ScaledPixels::from(20.0)); - /// assert_eq!(scaled_corners.top_right, ScaledPixels::from(40.0)); - /// assert_eq!(scaled_corners.bottom_right, ScaledPixels::from(60.0)); - /// assert_eq!(scaled_corners.bottom_left, ScaledPixels::from(80.0)); - /// ``` - #[must_use] - pub fn scale(&self, factor: f32) -> Corners { - Corners { - top_left: self.top_left.scale(factor), - top_right: self.top_right.scale(factor), - bottom_right: self.bottom_right.scale(factor), - bottom_left: self.bottom_left.scale(factor), - } - } - - /// Returns the maximum value of any corner. - /// - /// # Returns - /// - /// The maximum `Pixels` value among all four corners. - #[must_use] - pub fn max(&self) -> Pixels { - self.top_left - .max(self.top_right) - .max(self.bottom_right) - .max(self.bottom_left) - } -} - -impl + Ord + Clone + Debug + Default + PartialEq> Corners { - /// Clamps corner radii to be less than or equal to half the shortest side of a quad. - /// - /// # Arguments - /// - /// * `size` - The size of the quad which limits the size of the corner radii. - /// - /// # Returns - /// - /// Anchor radii values clamped to fit. - #[must_use] - pub fn clamp_radii_for_quad_size(self, size: Size) -> Corners { - let max = cmp::min(size.width, size.height) / 2.; - Corners { - top_left: cmp::min(self.top_left, max.clone()), - top_right: cmp::min(self.top_right, max.clone()), - bottom_right: cmp::min(self.bottom_right, max.clone()), - bottom_left: cmp::min(self.bottom_left, max), - } - } -} - -impl Corners { - /// Applies a function to each field of the `Corners`, producing a new `Corners`. - /// - /// This method allows for converting a `Corners` to a `Corners` by specifying a closure - /// that defines how to convert between the two types. The closure is applied to each field - /// (`top_left`, `top_right`, `bottom_right`, `bottom_left`), resulting in new corners of the desired type. - /// - /// # Arguments - /// - /// * `f` - A closure that takes a reference to a value of type `T` and returns a value of type `U`. - /// - /// # Returns - /// - /// Returns a new `Corners` with each field mapped by the provided function. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{Corners, Pixels, Rems}; - /// let corners = Corners { - /// top_left: Pixels::from(10.0), - /// top_right: Pixels::from(20.0), - /// bottom_right: Pixels::from(30.0), - /// bottom_left: Pixels::from(40.0), - /// }; - /// let corners_in_rems = corners.map(|&px| Rems(f32::from(px) / 16.0)); - /// assert_eq!(corners_in_rems, Corners { - /// top_left: Rems(0.625), - /// top_right: Rems(1.25), - /// bottom_right: Rems(1.875), - /// bottom_left: Rems(2.5), - /// }); - /// ``` - #[must_use] - pub fn map(&self, f: impl Fn(&T) -> U) -> Corners - where - U: Clone + Debug + Default + PartialEq, - { - Corners { - top_left: f(&self.top_left), - top_right: f(&self.top_right), - bottom_right: f(&self.bottom_right), - bottom_left: f(&self.bottom_left), - } - } -} - -impl Mul for Corners -where - T: Mul + Clone + Debug + Default + PartialEq, -{ - type Output = Self; - - fn mul(self, rhs: Self) -> Self::Output { - Self { - top_left: self.top_left.clone() * rhs.top_left, - top_right: self.top_right.clone() * rhs.top_right, - bottom_right: self.bottom_right.clone() * rhs.bottom_right, - bottom_left: self.bottom_left * rhs.bottom_left, - } - } -} - -impl MulAssign for Corners -where - T: Mul + Clone + Debug + Default + PartialEq, - S: Clone, -{ - fn mul_assign(&mut self, rhs: S) { - self.top_left = self.top_left.clone() * rhs.clone(); - self.top_right = self.top_right.clone() * rhs.clone(); - self.bottom_right = self.bottom_right.clone() * rhs.clone(); - self.bottom_left = self.bottom_left.clone() * rhs; - } -} - -impl Copy for Corners where T: Copy + Clone + Debug + Default + PartialEq {} - -impl From for Corners { - fn from(val: f32) -> Self { - Corners { - top_left: val.into(), - top_right: val.into(), - bottom_right: val.into(), - bottom_left: val.into(), - } - } -} - -impl From for Corners { - fn from(val: Pixels) -> Self { - Corners { - top_left: val, - top_right: val, - bottom_right: val, - bottom_left: val, - } - } -} - -/// Represents an angle in Radians -#[derive( - Clone, - Copy, - Default, - Add, - AddAssign, - Sub, - SubAssign, - Neg, - Div, - DivAssign, - PartialEq, - Serialize, - Deserialize, - Debug, -)] -#[repr(transparent)] -pub struct Radians(pub f32); - -/// Create a `Radian` from a raw value -pub fn radians(value: f32) -> Radians { - Radians(value) -} - -/// A type representing a percentage value. -#[derive( - Clone, - Copy, - Default, - Add, - AddAssign, - Sub, - SubAssign, - Neg, - Div, - DivAssign, - PartialEq, - Serialize, - Deserialize, - Debug, -)] -#[repr(transparent)] -pub struct Percentage(pub f32); - -/// Generate a `Radian` from a percentage of a full circle. -pub fn percentage(value: f32) -> Percentage { - debug_assert!( - (0.0..=1.0).contains(&value), - "Percentage must be between 0 and 1" - ); - Percentage(value) -} - -impl From for Radians { - fn from(value: Percentage) -> Self { - radians(value.0 * std::f32::consts::PI * 2.0) - } -} - -/// Represents a length in pixels, the base unit of measurement in the UI framework. -/// -/// `Pixels` is a value type that represents an absolute length in pixels, which is used -/// for specifying sizes, positions, and distances in the UI. It is the fundamental unit -/// of measurement for all visual elements and layout calculations. -/// -/// The inner value is an `f32`, allowing for sub-pixel precision which can be useful for -/// anti-aliasing and animations. However, when applied to actual pixel grids, the value -/// is typically rounded to the nearest integer. -/// -/// # Examples -/// -/// ``` -/// use gpui::{Pixels, ScaledPixels}; -/// -/// // Define a length of 10 pixels -/// let length = Pixels::from(10.0); -/// -/// // Define a length and scale it by a factor of 2 -/// let scaled_length = length.scale(2.0); -/// assert_eq!(scaled_length, ScaledPixels::from(20.0)); -/// ``` -#[derive( - Clone, - Copy, - Default, - Add, - AddAssign, - Sub, - SubAssign, - Neg, - Div, - DivAssign, - PartialEq, - Serialize, - Deserialize, - JsonSchema, -)] -#[repr(transparent)] -pub struct Pixels(pub(crate) f32); - -impl Div for Pixels { - type Output = f32; - - fn div(self, rhs: Self) -> Self::Output { - self.0 / rhs.0 - } -} - -impl std::ops::DivAssign for Pixels { - fn div_assign(&mut self, rhs: Self) { - *self = Self(self.0 / rhs.0); - } -} - -impl std::ops::RemAssign for Pixels { - fn rem_assign(&mut self, rhs: Self) { - self.0 %= rhs.0; - } -} - -impl std::ops::Rem for Pixels { - type Output = Self; - - fn rem(self, rhs: Self) -> Self { - Self(self.0 % rhs.0) - } -} - -impl Mul for Pixels { - type Output = Self; - - fn mul(self, rhs: f32) -> Self { - Self(self.0 * rhs) - } -} - -impl Mul for f32 { - type Output = Pixels; - - fn mul(self, rhs: Pixels) -> Self::Output { - rhs * self - } -} - -impl Mul for Pixels { - type Output = Self; - - fn mul(self, rhs: usize) -> Self { - self * (rhs as f32) - } -} - -impl Mul for usize { - type Output = Pixels; - - fn mul(self, rhs: Pixels) -> Pixels { - rhs * self - } -} - -impl MulAssign for Pixels { - fn mul_assign(&mut self, rhs: f32) { - self.0 *= rhs; - } -} - -impl Display for Pixels { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}px", self.0) - } -} - -impl Debug for Pixels { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - Display::fmt(self, f) - } -} - -impl std::iter::Sum for Pixels { - fn sum>(iter: I) -> Self { - iter.fold(Self::ZERO, |a, b| a + b) - } -} - -impl<'a> std::iter::Sum<&'a Pixels> for Pixels { - fn sum>(iter: I) -> Self { - iter.fold(Self::ZERO, |a, b| a + *b) - } -} - -impl TryFrom<&'_ str> for Pixels { - type Error = anyhow::Error; - - fn try_from(value: &'_ str) -> Result { - value - .strip_suffix("px") - .context("expected 'px' suffix") - .and_then(|number| Ok(number.parse()?)) - .map(Self) - } -} - -impl Pixels { - /// Represents zero pixels. - pub const ZERO: Pixels = Pixels(0.0); - /// The maximum value that can be represented by `Pixels`. - pub const MAX: Pixels = Pixels(f32::MAX); - /// The minimum value that can be represented by `Pixels`. - pub const MIN: Pixels = Pixels(f32::MIN); - - /// Returns the raw `f32` value of this `Pixels`. - pub fn as_f32(self) -> f32 { - self.0 - } - - /// Floors the `Pixels` value to the nearest whole number. - /// - /// # Returns - /// - /// Returns a new `Pixels` instance with the floored value. - pub fn floor(&self) -> Self { - Self(self.0.floor()) - } - - /// Rounds the `Pixels` value to the nearest whole number. - /// - /// # Returns - /// - /// Returns a new `Pixels` instance with the rounded value. - pub fn round(&self) -> Self { - Self(self.0.round()) - } - - /// Returns the ceiling of the `Pixels` value to the nearest whole number. - /// - /// # Returns - /// - /// Returns a new `Pixels` instance with the ceiling value. - pub fn ceil(&self) -> Self { - Self(self.0.ceil()) - } - - /// Scales the `Pixels` value by a given factor, producing `ScaledPixels`. - /// - /// This method is used when adjusting pixel values for display scaling factors, - /// such as high DPI (dots per inch) or Retina displays, where the pixel density is higher and - /// thus requires scaling to maintain visual consistency and readability. - /// - /// The resulting `ScaledPixels` represent the scaled value which can be used for rendering - /// calculations where display scaling is considered. - #[must_use] - pub fn scale(&self, factor: f32) -> ScaledPixels { - ScaledPixels(self.0 * factor) - } - - /// Raises the `Pixels` value to a given power. - /// - /// # Arguments - /// - /// * `exponent` - The exponent to raise the `Pixels` value by. - /// - /// # Returns - /// - /// Returns a new `Pixels` instance with the value raised to the given exponent. - pub fn pow(&self, exponent: f32) -> Self { - Self(self.0.powf(exponent)) - } - - /// Returns the absolute value of the `Pixels`. - /// - /// # Returns - /// - /// A new `Pixels` instance with the absolute value of the original `Pixels`. - pub fn abs(&self) -> Self { - Self(self.0.abs()) - } - - /// Returns the sign of the `Pixels` value. - /// - /// # Returns - /// - /// Returns: - /// * `1.0` if the value is positive - /// * `-1.0` if the value is negative - pub fn signum(&self) -> f32 { - self.0.signum() - } - - /// Returns the f64 value of `Pixels`. - /// - /// # Returns - /// - /// A f64 value of the `Pixels`. - pub fn to_f64(self) -> f64 { - self.0 as f64 - } -} - -impl Eq for Pixels {} - -impl PartialOrd for Pixels { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Pixels { - fn cmp(&self, other: &Self) -> cmp::Ordering { - self.0.total_cmp(&other.0) - } -} - -impl std::hash::Hash for Pixels { - fn hash(&self, state: &mut H) { - self.0.to_bits().hash(state); - } -} - -impl From for Pixels { - fn from(pixels: f64) -> Self { - Pixels(pixels as f32) - } -} - -impl From for Pixels { - fn from(pixels: f32) -> Self { - Pixels(pixels) - } -} - -impl From for f32 { - fn from(pixels: Pixels) -> Self { - pixels.0 - } -} - -impl From<&Pixels> for f32 { - fn from(pixels: &Pixels) -> Self { - pixels.0 - } -} - -impl From for f64 { - fn from(pixels: Pixels) -> Self { - pixels.0 as f64 - } -} - -impl From for u32 { - fn from(pixels: Pixels) -> Self { - pixels.0 as u32 - } -} - -impl From<&Pixels> for u32 { - fn from(pixels: &Pixels) -> Self { - pixels.0 as u32 - } -} - -impl From for Pixels { - fn from(pixels: u32) -> Self { - Pixels(pixels as f32) - } -} - -impl From for usize { - fn from(pixels: Pixels) -> Self { - pixels.0 as usize - } -} - -impl From for Pixels { - fn from(pixels: usize) -> Self { - Pixels(pixels as f32) - } -} - -/// Represents physical pixels on the display. -/// -/// `DevicePixels` is a unit of measurement that refers to the actual pixels on a device's screen. -/// This type is used when precise pixel manipulation is required, such as rendering graphics or -/// interfacing with hardware that operates on the pixel level. Unlike logical pixels that may be -/// affected by the device's scale factor, `DevicePixels` always correspond to real pixels on the -/// display. -#[derive( - Add, - AddAssign, - Clone, - Copy, - Default, - Div, - Eq, - Hash, - Ord, - PartialEq, - PartialOrd, - Sub, - SubAssign, - Serialize, - Deserialize, -)] -#[repr(transparent)] -pub struct DevicePixels(pub i32); - -impl DevicePixels { - /// Converts the `DevicePixels` value to the number of bytes needed to represent it in memory. - /// - /// This function is useful when working with graphical data that needs to be stored in a buffer, - /// such as images or framebuffers, where each pixel may be represented by a specific number of bytes. - /// - /// # Arguments - /// - /// * `bytes_per_pixel` - The number of bytes used to represent a single pixel. - /// - /// # Returns - /// - /// The number of bytes required to represent the `DevicePixels` value in memory. - /// - /// # Examples - /// - /// ``` - /// # use gpui::DevicePixels; - /// let pixels = DevicePixels(10); // 10 device pixels - /// let bytes_per_pixel = 4; // Assume each pixel is represented by 4 bytes (e.g., RGBA) - /// let total_bytes = pixels.to_bytes(bytes_per_pixel); - /// assert_eq!(total_bytes, 40); // 10 pixels * 4 bytes/pixel = 40 bytes - /// ``` - pub fn to_bytes(self, bytes_per_pixel: u8) -> u32 { - self.0 as u32 * bytes_per_pixel as u32 - } -} - -impl fmt::Debug for DevicePixels { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} px (device)", self.0) - } -} - -impl From for i32 { - fn from(device_pixels: DevicePixels) -> Self { - device_pixels.0 - } -} - -impl From for DevicePixels { - fn from(device_pixels: i32) -> Self { - DevicePixels(device_pixels) - } -} - -impl From for DevicePixels { - fn from(device_pixels: u32) -> Self { - DevicePixels(device_pixels as i32) - } -} - -impl From for u32 { - fn from(device_pixels: DevicePixels) -> Self { - device_pixels.0 as u32 - } -} - -impl From for u64 { - fn from(device_pixels: DevicePixels) -> Self { - device_pixels.0 as u64 - } -} - -impl From for DevicePixels { - fn from(device_pixels: u64) -> Self { - DevicePixels(device_pixels as i32) - } -} - -impl From for usize { - fn from(device_pixels: DevicePixels) -> Self { - device_pixels.0 as usize - } -} - -impl From for DevicePixels { - fn from(device_pixels: usize) -> Self { - DevicePixels(device_pixels as i32) - } -} - -/// Represents scaled pixels that take into account the device's scale factor. -/// -/// `ScaledPixels` are used to ensure that UI elements appear at the correct size on devices -/// with different pixel densities. When a device has a higher scale factor (such as Retina displays), -/// a single logical pixel may correspond to multiple physical pixels. By using `ScaledPixels`, -/// dimensions and positions can be specified in a way that scales appropriately across different -/// display resolutions. -#[derive(Clone, Copy, Default, Add, AddAssign, Sub, SubAssign, Div, DivAssign, PartialEq)] -#[repr(transparent)] -pub struct ScaledPixels(pub f32); - -impl ScaledPixels { - /// Returns the raw `f32` value of this `ScaledPixels`. - pub fn as_f32(self) -> f32 { - self.0 - } - - /// Floors the `ScaledPixels` value to the nearest whole number. - /// - /// # Returns - /// - /// Returns a new `ScaledPixels` instance with the floored value. - pub fn floor(&self) -> Self { - Self(self.0.floor()) - } - - /// Rounds the `ScaledPixels` value to the nearest whole number. - /// - /// # Returns - /// - /// Returns a new `ScaledPixels` instance with the rounded value. - pub fn round(&self) -> Self { - Self(self.0.round()) - } - - /// Ceils the `ScaledPixels` value to the nearest whole number. - /// - /// # Returns - /// - /// Returns a new `ScaledPixels` instance with the ceiled value. - pub fn ceil(&self) -> Self { - Self(self.0.ceil()) - } -} - -impl Eq for ScaledPixels {} - -impl PartialOrd for ScaledPixels { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for ScaledPixels { - fn cmp(&self, other: &Self) -> cmp::Ordering { - self.0.total_cmp(&other.0) - } -} - -impl Debug for ScaledPixels { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}px (scaled)", self.0) - } -} - -impl From for DevicePixels { - fn from(scaled: ScaledPixels) -> Self { - DevicePixels(scaled.0.ceil() as i32) - } -} - -impl From for ScaledPixels { - fn from(device: DevicePixels) -> Self { - ScaledPixels(device.0 as f32) - } -} - -impl From for f64 { - fn from(scaled_pixels: ScaledPixels) -> Self { - scaled_pixels.0 as f64 - } -} - -impl From for u32 { - fn from(pixels: ScaledPixels) -> Self { - pixels.0 as u32 - } -} - -impl From for ScaledPixels { - fn from(pixels: f32) -> Self { - Self(pixels) - } -} - -impl Div for ScaledPixels { - type Output = f32; - - fn div(self, rhs: Self) -> Self::Output { - self.0 / rhs.0 - } -} - -impl std::ops::DivAssign for ScaledPixels { - fn div_assign(&mut self, rhs: Self) { - *self = Self(self.0 / rhs.0); - } -} - -impl std::ops::RemAssign for ScaledPixels { - fn rem_assign(&mut self, rhs: Self) { - self.0 %= rhs.0; - } -} - -impl std::ops::Rem for ScaledPixels { - type Output = Self; - - fn rem(self, rhs: Self) -> Self { - Self(self.0 % rhs.0) - } -} - -impl Mul for ScaledPixels { - type Output = Self; - - fn mul(self, rhs: f32) -> Self { - Self(self.0 * rhs) - } -} - -impl Mul for f32 { - type Output = ScaledPixels; - - fn mul(self, rhs: ScaledPixels) -> Self::Output { - rhs * self - } -} - -impl Mul for ScaledPixels { - type Output = Self; - - fn mul(self, rhs: usize) -> Self { - self * (rhs as f32) - } -} - -impl Mul for usize { - type Output = ScaledPixels; - - fn mul(self, rhs: ScaledPixels) -> ScaledPixels { - rhs * self - } -} - -impl MulAssign for ScaledPixels { - fn mul_assign(&mut self, rhs: f32) { - self.0 *= rhs; - } -} - -/// Represents a length in rems, a unit based on the font-size of the window, which can be assigned with [`Window::set_rem_size`][set_rem_size]. -/// -/// Rems are used for defining lengths that are scalable and consistent across different UI elements. -/// The value of `1rem` is typically equal to the font-size of the root element (often the `` element in browsers), -/// making it a flexible unit that adapts to the user's text size preferences. In this framework, `rems` serve a similar -/// purpose, allowing for scalable and accessible design that can adjust to different display settings or user preferences. -/// -/// For example, if the root element's font-size is `16px`, then `1rem` equals `16px`. A length of `2rems` would then be `32px`. -/// -/// [set_rem_size]: crate::Window::set_rem_size -#[derive(Clone, Copy, Default, Add, Sub, Mul, Div, Neg, PartialEq)] -pub struct Rems(pub f32); - -impl Rems { - /// A length of zero. - pub const ZERO: Self = Self(0.0); - /// Convert this Rem value to pixels. - pub fn to_pixels(self, rem_size: Pixels) -> Pixels { - self * rem_size - } - /// Convert from pixels to Rem - pub fn from_pixels(length: Pixels, window: &gpui::Window) -> Self { - Self(length / window.rem_size()) - } -} - -impl Mul for Rems { - type Output = Pixels; - - fn mul(self, other: Pixels) -> Pixels { - Pixels(self.0 * other.0) - } -} - -impl AddAssign for Rems { - fn add_assign(&mut self, rhs: Rems) { - self.0 += rhs.0 - } -} - -impl Display for Rems { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}rem", self.0) - } -} - -impl Debug for Rems { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - Display::fmt(self, f) - } -} - -impl TryFrom<&'_ str> for Rems { - type Error = anyhow::Error; - - fn try_from(value: &'_ str) -> Result { - value - .strip_suffix("rem") - .context("expected 'rem' suffix") - .and_then(|number| Ok(number.parse()?)) - .map(Self) - } -} - -/// Represents an absolute length in pixels or rems. -/// -/// `AbsoluteLength` can be either a fixed number of pixels, which is an absolute measurement not -/// affected by the current font size, or a number of rems, which is relative to the font size of -/// the root element. It is used for specifying dimensions that are either independent of or -/// related to the typographic scale. -#[derive(Clone, Copy, Neg, PartialEq)] -pub enum AbsoluteLength { - /// A length in pixels. - Pixels(Pixels), - /// A length in rems. - Rems(Rems), -} - -impl AbsoluteLength { - /// Checks if the absolute length is zero. - pub fn is_zero(&self) -> bool { - match self { - AbsoluteLength::Pixels(px) => px.0 == 0.0, - AbsoluteLength::Rems(rems) => rems.0 == 0.0, - } - } -} - -impl From for AbsoluteLength { - fn from(pixels: Pixels) -> Self { - AbsoluteLength::Pixels(pixels) - } -} - -impl From for AbsoluteLength { - fn from(rems: Rems) -> Self { - AbsoluteLength::Rems(rems) - } -} - -impl AbsoluteLength { - /// Converts an `AbsoluteLength` to `Pixels` based on a given `rem_size`. - /// - /// # Arguments - /// - /// * `rem_size` - The size of one rem in pixels. - /// - /// # Returns - /// - /// Returns the `AbsoluteLength` as `Pixels`. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{AbsoluteLength, Pixels, Rems}; - /// let length_in_pixels = AbsoluteLength::Pixels(Pixels::from(42.0)); - /// let length_in_rems = AbsoluteLength::Rems(Rems(2.0)); - /// let rem_size = Pixels::from(16.0); - /// - /// assert_eq!(length_in_pixels.to_pixels(rem_size), Pixels::from(42.0)); - /// assert_eq!(length_in_rems.to_pixels(rem_size), Pixels::from(32.0)); - /// ``` - pub fn to_pixels(self, rem_size: Pixels) -> Pixels { - match self { - AbsoluteLength::Pixels(pixels) => pixels, - AbsoluteLength::Rems(rems) => rems.to_pixels(rem_size), - } - } - - /// Converts an `AbsoluteLength` to `Rems` based on a given `rem_size`. - /// - /// # Arguments - /// - /// * `rem_size` - The size of one rem in pixels. - /// - /// # Returns - /// - /// Returns the `AbsoluteLength` as `Pixels`. - pub fn to_rems(self, rem_size: Pixels) -> Rems { - match self { - AbsoluteLength::Pixels(pixels) => Rems(pixels.0 / rem_size.0), - AbsoluteLength::Rems(rems) => rems, - } - } -} - -impl Default for AbsoluteLength { - fn default() -> Self { - px(0.).into() - } -} - -impl Display for AbsoluteLength { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Pixels(pixels) => write!(f, "{pixels}"), - Self::Rems(rems) => write!(f, "{rems}"), - } - } -} - -impl Debug for AbsoluteLength { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - Display::fmt(self, f) - } -} - -const EXPECTED_ABSOLUTE_LENGTH: &str = "number with 'px' or 'rem' suffix"; - -impl TryFrom<&'_ str> for AbsoluteLength { - type Error = anyhow::Error; - - fn try_from(value: &'_ str) -> Result { - if let Ok(pixels) = value.try_into() { - Ok(Self::Pixels(pixels)) - } else if let Ok(rems) = value.try_into() { - Ok(Self::Rems(rems)) - } else { - Err(anyhow!( - "invalid AbsoluteLength '{value}', expected {EXPECTED_ABSOLUTE_LENGTH}" - )) - } - } -} - -impl JsonSchema for AbsoluteLength { - fn schema_name() -> Cow<'static, str> { - "AbsoluteLength".into() - } - - fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema { - json_schema!({ - "type": "string", - "pattern": r"^-?\d+(\.\d+)?(px|rem)$" - }) - } -} - -impl<'de> Deserialize<'de> for AbsoluteLength { - fn deserialize>(deserializer: D) -> Result { - struct StringVisitor; - - impl de::Visitor<'_> for StringVisitor { - type Value = AbsoluteLength; - - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{EXPECTED_ABSOLUTE_LENGTH}") - } - - fn visit_str(self, value: &str) -> Result { - AbsoluteLength::try_from(value).map_err(E::custom) - } - } - - deserializer.deserialize_str(StringVisitor) - } -} - -impl Serialize for AbsoluteLength { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&format!("{self}")) - } -} - -/// A non-auto length that can be defined in pixels, rems, or percent of parent. -/// -/// This enum represents lengths that have a specific value, as opposed to lengths that are automatically -/// determined by the context. It includes absolute lengths in pixels or rems, and relative lengths as a -/// fraction of the parent's size. -#[derive(Clone, Copy, Neg, PartialEq)] -pub enum DefiniteLength { - /// An absolute length specified in pixels or rems. - Absolute(AbsoluteLength), - /// A relative length specified as a fraction of the parent's size, between 0 and 1. - Fraction(f32), -} - -impl DefiniteLength { - /// Converts the `DefiniteLength` to `Pixels` based on a given `base_size` and `rem_size`. - /// - /// If the `DefiniteLength` is an absolute length, it will be directly converted to `Pixels`. - /// If it is a fraction, the fraction will be multiplied by the `base_size` to get the length in pixels. - /// - /// # Arguments - /// - /// * `base_size` - The base size in `AbsoluteLength` to which the fraction will be applied. - /// * `rem_size` - The size of one rem in pixels, used to convert rems to pixels. - /// - /// # Returns - /// - /// Returns the `DefiniteLength` as `Pixels`. - /// - /// # Examples - /// - /// ``` - /// # use gpui::{DefiniteLength, AbsoluteLength, Pixels, px, rems}; - /// let length_in_pixels = DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0))); - /// let length_in_rems = DefiniteLength::Absolute(AbsoluteLength::Rems(rems(2.0))); - /// let length_as_fraction = DefiniteLength::Fraction(0.5); - /// let base_size = AbsoluteLength::Pixels(px(100.0)); - /// let rem_size = px(16.0); - /// - /// assert_eq!(length_in_pixels.to_pixels(base_size, rem_size), Pixels::from(42.0)); - /// assert_eq!(length_in_rems.to_pixels(base_size, rem_size), Pixels::from(32.0)); - /// assert_eq!(length_as_fraction.to_pixels(base_size, rem_size), Pixels::from(50.0)); - /// ``` - pub fn to_pixels(self, base_size: AbsoluteLength, rem_size: Pixels) -> Pixels { - match self { - DefiniteLength::Absolute(size) => size.to_pixels(rem_size), - DefiniteLength::Fraction(fraction) => match base_size { - AbsoluteLength::Pixels(px) => px * fraction, - AbsoluteLength::Rems(rems) => rems * rem_size * fraction, - }, - } - } -} - -impl Debug for DefiniteLength { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - Display::fmt(self, f) - } -} - -impl Display for DefiniteLength { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - DefiniteLength::Absolute(length) => write!(f, "{length}"), - DefiniteLength::Fraction(fraction) => write!(f, "{}%", (fraction * 100.0) as i32), - } - } -} - -const EXPECTED_DEFINITE_LENGTH: &str = "expected number with 'px', 'rem', or '%' suffix"; - -impl TryFrom<&'_ str> for DefiniteLength { - type Error = anyhow::Error; - - fn try_from(value: &'_ str) -> Result { - if let Some(percentage) = value.strip_suffix('%') { - let fraction: f32 = percentage.parse::().with_context(|| { - format!("invalid DefiniteLength '{value}', expected {EXPECTED_DEFINITE_LENGTH}") - })?; - Ok(DefiniteLength::Fraction(fraction / 100.0)) - } else if let Ok(absolute_length) = value.try_into() { - Ok(DefiniteLength::Absolute(absolute_length)) - } else { - Err(anyhow!( - "invalid DefiniteLength '{value}', expected {EXPECTED_DEFINITE_LENGTH}" - )) - } - } -} - -impl JsonSchema for DefiniteLength { - fn schema_name() -> Cow<'static, str> { - "DefiniteLength".into() - } - - fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema { - json_schema!({ - "type": "string", - "pattern": r"^-?\d+(\.\d+)?(px|rem|%)$" - }) - } -} - -impl<'de> Deserialize<'de> for DefiniteLength { - fn deserialize>(deserializer: D) -> Result { - struct StringVisitor; - - impl de::Visitor<'_> for StringVisitor { - type Value = DefiniteLength; - - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{EXPECTED_DEFINITE_LENGTH}") - } - - fn visit_str(self, value: &str) -> Result { - DefiniteLength::try_from(value).map_err(E::custom) - } - } - - deserializer.deserialize_str(StringVisitor) - } -} - -impl Serialize for DefiniteLength { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&format!("{self}")) - } -} - -impl From for DefiniteLength { - fn from(pixels: Pixels) -> Self { - Self::Absolute(pixels.into()) - } -} - -impl From for DefiniteLength { - fn from(rems: Rems) -> Self { - Self::Absolute(rems.into()) - } -} - -impl From for DefiniteLength { - fn from(length: AbsoluteLength) -> Self { - Self::Absolute(length) - } -} - -impl Default for DefiniteLength { - fn default() -> Self { - Self::Absolute(AbsoluteLength::default()) - } -} - -/// A length that can be defined in pixels, rems, percent of parent, or auto. -#[derive(Clone, Copy, PartialEq)] -pub enum Length { - /// A definite length specified either in pixels, rems, or as a fraction of the parent's size. - Definite(DefiniteLength), - /// An automatic length that is determined by the context in which it is used. - Auto, -} - -impl Debug for Length { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - Display::fmt(self, f) - } -} - -impl Display for Length { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Length::Definite(definite_length) => write!(f, "{}", definite_length), - Length::Auto => write!(f, "auto"), - } - } -} - -const EXPECTED_LENGTH: &str = "expected 'auto' or number with 'px', 'rem', or '%' suffix"; - -impl TryFrom<&'_ str> for Length { - type Error = anyhow::Error; - - fn try_from(value: &'_ str) -> Result { - if value == "auto" { - Ok(Length::Auto) - } else if let Ok(definite_length) = value.try_into() { - Ok(Length::Definite(definite_length)) - } else { - Err(anyhow!( - "invalid Length '{value}', expected {EXPECTED_LENGTH}" - )) - } - } -} - -impl JsonSchema for Length { - fn schema_name() -> Cow<'static, str> { - "Length".into() - } - - fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema { - json_schema!({ - "type": "string", - "pattern": r"^(auto|-?\d+(\.\d+)?(px|rem|%))$" - }) - } -} - -impl<'de> Deserialize<'de> for Length { - fn deserialize>(deserializer: D) -> Result { - struct StringVisitor; - - impl de::Visitor<'_> for StringVisitor { - type Value = Length; - - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{EXPECTED_LENGTH}") - } - - fn visit_str(self, value: &str) -> Result { - Length::try_from(value).map_err(E::custom) - } - } - - deserializer.deserialize_str(StringVisitor) - } -} - -impl Serialize for Length { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&format!("{self}")) - } -} - -/// Constructs a `DefiniteLength` representing a relative fraction of a parent size. -/// -/// This function creates a `DefiniteLength` that is a specified fraction of a parent's dimension. -/// The fraction should be a floating-point number between 0.0 and 1.0, where 1.0 represents 100% of the parent's size. -/// -/// # Arguments -/// -/// * `fraction` - The fraction of the parent's size, between 0.0 and 1.0. -/// -/// # Returns -/// -/// A `DefiniteLength` representing the relative length as a fraction of the parent's size. -pub const fn relative(fraction: f32) -> DefiniteLength { - DefiniteLength::Fraction(fraction) -} - -/// Returns the Golden Ratio, i.e. `~(1.0 + sqrt(5.0)) / 2.0`. -pub const fn phi() -> DefiniteLength { - relative(1.618_034) -} - -/// Constructs a `Rems` value representing a length in rems. -/// -/// # Arguments -/// -/// * `rems` - The number of rems for the length. -/// -/// # Returns -/// -/// A `Rems` representing the specified number of rems. -pub const fn rems(rems: f32) -> Rems { - Rems(rems) -} - -/// Constructs a `Pixels` value representing a length in pixels. -/// -/// # Arguments -/// -/// * `pixels` - The number of pixels for the length. -/// -/// # Returns -/// -/// A `Pixels` representing the specified number of pixels. -pub const fn px(pixels: f32) -> Pixels { - Pixels(pixels) -} - -/// Returns a `Length` representing an automatic length. -/// -/// The `auto` length is often used in layout calculations where the length should be determined -/// by the layout context itself rather than being explicitly set. This is commonly used in CSS -/// for properties like `width`, `height`, `margin`, `padding`, etc., where `auto` can be used -/// to instruct the layout engine to calculate the size based on other factors like the size of the -/// container or the intrinsic size of the content. -/// -/// # Returns -/// -/// A `Length` variant set to `Auto`. -pub const fn auto() -> Length { - Length::Auto -} - -impl From for Length { - fn from(pixels: Pixels) -> Self { - Self::Definite(pixels.into()) - } -} - -impl From for Length { - fn from(rems: Rems) -> Self { - Self::Definite(rems.into()) - } -} - -impl From for Length { - fn from(length: DefiniteLength) -> Self { - Self::Definite(length) - } -} - -impl From for Length { - fn from(length: AbsoluteLength) -> Self { - Self::Definite(length.into()) - } -} - -impl Default for Length { - fn default() -> Self { - Self::Definite(DefiniteLength::default()) - } -} - -impl From<()> for Length { - fn from(_: ()) -> Self { - Self::Definite(DefiniteLength::default()) - } -} - -/// A location in a grid layout. -#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, JsonSchema, Default)] -pub struct GridLocation { - /// The rows this item uses within the grid. - pub row: Range, - /// The columns this item uses within the grid. - pub column: Range, -} - -/// The placement of an item within a grid layout's column or row. -#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, JsonSchema, Default)] -pub enum GridPlacement { - /// The grid line index to place this item. - Line(i16), - /// The number of grid lines to span. - Span(u16), - /// Automatically determine the placement, equivalent to Span(1) - #[default] - Auto, -} - -impl From for taffy::GridPlacement { - fn from(placement: GridPlacement) -> Self { - match placement { - GridPlacement::Line(index) => taffy::GridPlacement::from_line_index(index), - GridPlacement::Span(span) => taffy::GridPlacement::from_span(span), - GridPlacement::Auto => taffy::GridPlacement::Auto, - } - } -} - -/// Provides a trait for types that can calculate half of their value. -/// -/// The `Half` trait is used for types that can be evenly divided, returning a new instance of the same type -/// representing half of the original value. This is commonly used for types that represent measurements or sizes, -/// such as lengths or pixels, where halving is a frequent operation during layout calculations or animations. -pub trait Half { - /// Returns half of the current value. - /// - /// # Returns - /// - /// A new instance of the implementing type, representing half of the original value. - fn half(&self) -> Self; -} - -impl Half for i32 { - fn half(&self) -> Self { - self / 2 - } -} - -impl Half for f32 { - fn half(&self) -> Self { - self / 2. - } -} - -impl Half for DevicePixels { - fn half(&self) -> Self { - Self(self.0 / 2) - } -} - -impl Half for ScaledPixels { - fn half(&self) -> Self { - Self(self.0 / 2.) - } -} - -impl Half for Pixels { - fn half(&self) -> Self { - Self(self.0 / 2.) - } -} - -impl Half for Rems { - fn half(&self) -> Self { - Self(self.0 / 2.) - } -} - -/// A trait for checking if a value is zero. -/// -/// This trait provides a method to determine if a value is considered to be zero. -/// It is implemented for various numeric and length-related types where the concept -/// of zero is applicable. This can be useful for comparisons, optimizations, or -/// determining if an operation has a neutral effect. -pub trait IsZero { - /// Determines if the value is zero. - /// - /// # Returns - /// - /// Returns `true` if the value is zero, `false` otherwise. - fn is_zero(&self) -> bool; -} - -impl IsZero for DevicePixels { - fn is_zero(&self) -> bool { - self.0 == 0 - } -} - -impl IsZero for ScaledPixels { - fn is_zero(&self) -> bool { - self.0 == 0. - } -} - -impl IsZero for Pixels { - fn is_zero(&self) -> bool { - self.0 == 0. - } -} - -impl IsZero for Rems { - fn is_zero(&self) -> bool { - self.0 == 0. - } -} - -impl IsZero for AbsoluteLength { - fn is_zero(&self) -> bool { - match self { - AbsoluteLength::Pixels(pixels) => pixels.is_zero(), - AbsoluteLength::Rems(rems) => rems.is_zero(), - } - } -} - -impl IsZero for DefiniteLength { - fn is_zero(&self) -> bool { - match self { - DefiniteLength::Absolute(length) => length.is_zero(), - DefiniteLength::Fraction(fraction) => *fraction == 0., - } - } -} - -impl IsZero for Length { - fn is_zero(&self) -> bool { - match self { - Length::Definite(length) => length.is_zero(), - Length::Auto => false, - } - } -} - -impl IsZero for Point { - fn is_zero(&self) -> bool { - self.x.is_zero() && self.y.is_zero() - } -} - -impl IsZero for Size -where - T: IsZero + Clone + Debug + Default + PartialEq, -{ - fn is_zero(&self) -> bool { - self.width.is_zero() || self.height.is_zero() - } -} - -impl IsZero for Bounds { - fn is_zero(&self) -> bool { - self.size.is_zero() - } -} - -impl IsZero for Corners -where - T: IsZero + Clone + Debug + Default + PartialEq, -{ - fn is_zero(&self) -> bool { - self.top_left.is_zero() - && self.top_right.is_zero() - && self.bottom_right.is_zero() - && self.bottom_left.is_zero() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_bounds_intersects() { - let bounds1 = Bounds { - origin: Point { x: 0.0, y: 0.0 }, - size: Size { - width: 5.0, - height: 5.0, - }, - }; - let bounds2 = Bounds { - origin: Point { x: 4.0, y: 4.0 }, - size: Size { - width: 5.0, - height: 5.0, - }, - }; - let bounds3 = Bounds { - origin: Point { x: 10.0, y: 10.0 }, - size: Size { - width: 5.0, - height: 5.0, - }, - }; - - // Test Case 1: Intersecting bounds - assert!(bounds1.intersects(&bounds2)); - - // Test Case 2: Non-Intersecting bounds - assert!(!bounds1.intersects(&bounds3)); - - // Test Case 3: Bounds intersecting with themselves - assert!(bounds1.intersects(&bounds1)); - } -} diff --git a/crates/gpui_pre_apple/vendor/gpui/src/platform.rs b/crates/gpui_pre_apple/vendor/gpui/src/platform.rs deleted file mode 100644 index eeba999..0000000 --- a/crates/gpui_pre_apple/vendor/gpui/src/platform.rs +++ /dev/null @@ -1,3113 +0,0 @@ -mod app_menu; -mod keyboard; -mod keystroke; - -#[cfg(all(target_os = "linux", feature = "wayland"))] -#[expect(missing_docs)] -pub mod layer_shell; - -/// Types for configuring parent-anchored popup windows such as menus, dropdowns and tooltips. -pub mod popup; - -#[cfg(any(test, feature = "test-support", feature = "bench-support"))] -mod threaded_dispatcher; - -#[cfg(any(test, feature = "test-support", feature = "bench-support"))] -mod test; - -#[cfg(all(target_os = "macos", any(test, feature = "test-support")))] -mod visual_test; - -#[cfg(all( - feature = "screen-capture", - any(target_os = "windows", target_os = "linux", target_os = "freebsd",) -))] -pub mod scap_screen_capture; - -#[cfg(all( - any(target_os = "windows", target_os = "linux"), - feature = "screen-capture" -))] -pub(crate) type PlatformScreenCaptureFrame = scap::frame::Frame; -#[cfg(not(feature = "screen-capture"))] -pub(crate) type PlatformScreenCaptureFrame = (); -#[cfg(all(target_os = "macos", feature = "screen-capture"))] -pub(crate) type PlatformScreenCaptureFrame = core_video::image_buffer::CVImageBuffer; - -use crate::{ - Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds, - DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Edges, ExternalDragPayload, Font, - FontId, FontMetrics, FontRun, ForegroundExecutor, GlyphId, GpuSpecs, Hsla, ImageSource, Keymap, - LineLayout, Pixels, PlatformGestures, PlatformInput, Point, Priority, RenderGlyphParams, - RenderImage, RenderImageParams, RenderSvgParams, Scene, ShapedGlyph, ShapedRun, SharedString, - Size, SvgRenderer, SystemWindowTab, Task, Window, WindowControlArea, hash, point, px, size, -}; -#[cfg(any(target_os = "linux", target_os = "freebsd"))] -use anyhow::bail; -use anyhow::{Context as _, Result}; -use async_task::Runnable; -use futures::channel::oneshot; -#[cfg(any(test, feature = "test-support", feature = "bench-support"))] -use image::RgbaImage; -use image::codecs::gif::GifDecoder; -use image::{AnimationDecoder as _, DynamicImage, Frame}; -use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; -use scheduler::Instant; -pub use scheduler::RunnableMeta; -use schemars::JsonSchema; -use seahash::SeaHasher; -use serde::{Deserialize, Serialize}; -use smallvec::SmallVec; -use std::borrow::Cow; -use std::hash::{Hash, Hasher}; -use std::io::Cursor; -use std::ops; -use std::time::Duration; -use std::{ - ffi::OsString, - fmt::{self, Debug}, - ops::Range, - path::{Path, PathBuf}, - rc::Rc, - sync::Arc, -}; -use strum::EnumIter; -use uuid::Uuid; - -pub use app_menu::*; -pub use keyboard::*; -pub use keystroke::*; - -#[cfg(any(test, feature = "test-support", feature = "bench-support"))] -pub(crate) use test::*; - -#[cfg(any(test, feature = "test-support"))] -pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream}; - -#[cfg(any(test, feature = "test-support", feature = "bench-support"))] -pub use threaded_dispatcher::ThreadedDispatcher; - -#[cfg(all(target_os = "macos", any(test, feature = "test-support")))] -pub use visual_test::VisualTestPlatform; - -// TODO(jk): return an enum instead of a string -/// Return which compositor we're guessing we'll use. -/// Does not attempt to connect to the given compositor. -#[cfg(any(target_os = "linux", target_os = "freebsd"))] -#[inline] -pub fn guess_compositor() -> &'static str { - if std::env::var_os("ZED_HEADLESS").is_some() { - return "Headless"; - } - - #[cfg(feature = "wayland")] - let wayland_display = std::env::var_os("WAYLAND_DISPLAY"); - #[cfg(not(feature = "wayland"))] - let wayland_display: Option = None; - - #[cfg(feature = "x11")] - let x11_display = std::env::var_os("DISPLAY"); - #[cfg(not(feature = "x11"))] - let x11_display: Option = None; - - let use_wayland = wayland_display.is_some_and(|display| !display.is_empty()); - let use_x11 = x11_display.is_some_and(|display| !display.is_empty()); - - if use_wayland { - "Wayland" - } else if use_x11 { - "X11" - } else { - "Headless" - } -} - -#[expect(missing_docs)] -pub trait Platform: 'static { - fn background_executor(&self) -> BackgroundExecutor; - fn foreground_executor(&self) -> ForegroundExecutor; - fn text_system(&self) -> Arc; - - fn run(&self, on_finish_launching: Box); - fn quit(&self); - fn restart(&self, binary_path: Option, arguments: Vec); - fn activate(&self, ignoring_other_apps: bool); - fn hide(&self); - fn hide_other_apps(&self); - fn unhide_other_apps(&self); - - fn displays(&self) -> Vec>; - fn primary_display(&self) -> Option>; - fn active_window(&self) -> Option; - fn window_stack(&self) -> Option> { - None - } - - fn is_screen_capture_supported(&self) -> bool { - false - } - - fn screen_capture_sources( - &self, - ) -> oneshot::Receiver>>> { - let (sources_tx, sources_rx) = oneshot::channel(); - sources_tx - .send(Err(anyhow::anyhow!( - "gpui was compiled without the screen-capture feature" - ))) - .ok(); - sources_rx - } - - fn open_window( - &self, - handle: AnyWindowHandle, - options: WindowParams, - ) -> anyhow::Result>; - - /// Returns the appearance of the application's windows. - fn window_appearance(&self) -> WindowAppearance; - - /// Overrides the appearance (light/dark) applied to the app's windows, independent - /// of the OS-wide setting. Pass `None` to clear the override and follow the system - /// again. The override is reflected by [`Platform::window_appearance`]. - /// - /// Currently only implemented on macOS, where it sets `NSApplication.appearance` so - /// the native window chrome (the window border and titlebar) of every window matches - /// a dark app theme even when the system is in light mode (or vice versa). A no-op on - /// other platforms. - fn set_window_appearance(&self, _appearance: Option) {} - - /// Returns the window button layout configuration when supported. - fn button_layout(&self) -> Option { - None - } - - fn open_url(&self, url: &str); - fn on_open_urls(&self, callback: Box)>); - fn register_url_scheme(&self, url: &str) -> Task>; - - fn prompt_for_paths( - &self, - options: PathPromptOptions, - ) -> oneshot::Receiver>>>; - fn prompt_for_new_path( - &self, - directory: &Path, - suggested_name: Option<&str>, - ) -> oneshot::Receiver>>; - fn can_select_mixed_files_and_dirs(&self) -> bool; - fn reveal_path(&self, path: &Path); - fn open_with_system(&self, path: &Path); - - fn on_quit(&self, callback: Box bool>); - fn on_reopen(&self, callback: Box); - fn on_system_wake(&self, callback: Box); - - // Mobile platform methods. On mobile the OS owns the application - // lifecycle: apps are backgrounded, foregrounded, and killed at the - // system's discretion, and must react rather than decide. - - /// Registers a callback invoked whenever the application's lifecycle - /// phase changes. See [`AppLifecyclePhase`] for the phase vocabulary and - /// its mapping onto iOS and Android. - /// - /// Desktop platforms never invoke this. - fn on_app_lifecycle(&self, _callback: Box) {} - - /// Registers a callback invoked when the OS signals memory pressure - /// (iOS `didReceiveMemoryWarning`, Android `onTrimMemory`). - /// - /// Desktop platforms never invoke this. - fn on_memory_warning(&self, _callback: Box) {} - - /// The platform's gesture recognition services, if it provides any - /// beyond gpui's portable recognizers. See - /// [`PlatformGestures`](crate::PlatformGestures). - fn gestures(&self) -> Option> { - None - } - - fn set_menus(&self, menus: Vec

, keymap: &Keymap); - fn get_menus(&self) -> Option> { - None - } - - fn set_dock_menu(&self, menu: Vec, keymap: &Keymap); - fn perform_dock_menu_action(&self, _action: usize) {} - fn add_recent_document(&self, _path: &Path) {} - fn update_jump_list( - &self, - _menus: Vec, - _entries: Vec>, - ) -> Task>> { - Task::ready(Vec::new()) - } - fn on_app_menu_action(&self, callback: Box); - fn on_will_open_app_menu(&self, callback: Box); - fn on_validate_app_menu_command(&self, callback: Box bool>); - - fn thermal_state(&self) -> ThermalState; - fn on_thermal_state_change(&self, callback: Box); - - /// Sets the application's process-wide identity and user-visible name. - /// - /// The identifier is used for platform identity mechanisms such as the - /// Windows AppUserModelID. The name is used wherever the operating system - /// presents the application to the user. Call this once, early in startup, - /// before opening windows or posting notifications. - fn set_app_identity(&self, identifier: &str, name: &str) { - _ = (identifier, name); - } - - /// Posts a notification to the operating system's notification center. - /// - /// Posting a notification whose [`SystemNotification::tag`] matches an - /// earlier one replaces that notification where the platform supports it. - /// No-op on platforms without notification support, or when delivery is - /// unavailable (e.g. authorization was denied). - fn show_system_notification(&self, notification: SystemNotification) { - _ = notification; - } - - /// Removes the delivered or pending notification with this tag. - /// - /// Best-effort: some platforms cannot retract a notification once shown, - /// in which case it ages out of the notification center on its own. - fn dismiss_system_notification(&self, tag: &str) { - _ = tag; - } - - /// Registers the callback invoked when the user activates a system - /// notification, either by clicking its body or one of its action - /// buttons. - /// - /// Implementations must invoke the callback on the main thread. - fn on_system_notification_response( - &self, - callback: Box, - ) { - _ = callback; - } - - fn compositor_name(&self) -> &'static str { - "" - } - fn app_path(&self) -> Result; - fn path_for_auxiliary_executable(&self, name: &str) -> Result; - - fn set_cursor_style(&self, style: CursorStyle); - - /// Hides the mouse cursor until the user moves the mouse over one of - /// this application's windows. - fn hide_cursor_until_mouse_moves(&self); - - /// Returns whether the mouse cursor is currently visible. - fn is_cursor_visible(&self) -> bool; - - fn should_auto_hide_scrollbars(&self) -> bool; - - fn read_from_clipboard(&self) -> Option; - fn write_to_clipboard(&self, item: ClipboardItem); - - /// Reads the clipboard, resolving once its contents are available. - /// - /// Most platforms read synchronously and return a ready task. Platforms - /// whose clipboard access is inherently asynchronous and permission-gated - /// (e.g. the browser's async clipboard API) override this method; on those - /// platforms [`Platform::read_from_clipboard`] cannot return the clipboard - /// contents, so callers that can await should prefer this method. - fn read_from_clipboard_async(&self) -> Task, ClipboardReadError>> { - Task::ready(Ok(self.read_from_clipboard())) - } - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - fn read_from_primary(&self) -> Option; - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - fn write_to_primary(&self, item: ClipboardItem); - - #[cfg(target_os = "macos")] - fn read_from_find_pasteboard(&self) -> Option; - #[cfg(target_os = "macos")] - fn write_to_find_pasteboard(&self, item: ClipboardItem); - - fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task>; - fn read_credentials(&self, url: &str) -> Task)>>>; - fn delete_credentials(&self, url: &str) -> Task>; - - fn keyboard_layout(&self) -> Box; - fn keyboard_mapper(&self) -> Rc; - fn on_keyboard_layout_change(&self, callback: Box); -} - -/// A handle to a platform's display, e.g. a monitor or laptop screen. -pub trait PlatformDisplay: Debug { - /// Get the ID for this display - fn id(&self) -> DisplayId; - - /// Returns a stable identifier for this display that can be persisted and used - /// across system restarts. - fn uuid(&self) -> Result; - - /// Get the bounds for this display - fn bounds(&self) -> Bounds; - - /// Get the visible bounds for this display, excluding taskbar/dock areas. - /// This is the usable area where windows can be placed without being obscured. - /// Defaults to the full display bounds if not overridden. - fn visible_bounds(&self) -> Bounds { - self.bounds() - } - - /// Get the default bounds for this display to place a window - fn default_bounds(&self) -> Bounds { - let bounds = self.bounds(); - let center = bounds.center(); - let clipped_window_size = DEFAULT_WINDOW_SIZE.min(&bounds.size); - - let offset = clipped_window_size / 2.0; - let origin = point(center.x - offset.width, center.y - offset.height); - Bounds::new(origin, clipped_window_size) - } -} - -/// A notification posted to the operating system's notification center, -/// rather than rendered as in-app UI. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SystemNotification { - /// Stable identity for the notification. Posting a new notification with - /// the same tag replaces the previous one where the platform supports it, - /// and responses carry the tag back to the application. - pub tag: SharedString, - /// The notification's headline. - pub title: SharedString, - /// Additional text displayed below the title. - pub body: SharedString, - /// Buttons offered on the notification. Platforms that cannot display - /// action buttons show the notification without them. - pub actions: Vec, -} - -/// A button offered on a [`SystemNotification`]. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct SystemNotificationAction { - /// Identifies the action in [`SystemNotificationResponse::action_id`] - /// when the user presses this button. - pub id: SharedString, - /// The button's user-visible label. - pub label: SharedString, -} - -/// The user's activation of a [`SystemNotification`]. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SystemNotificationResponse { - /// The [`SystemNotification::tag`] of the activated notification. - pub tag: SharedString, - /// The pressed action button's [`SystemNotificationAction::id`], or - /// `None` when the user activated the notification body itself. - pub action_id: Option, -} - -/// Thermal state of the system -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ThermalState { - /// System has no thermal constraints - Nominal, - /// System is slightly constrained, reduce discretionary work - Fair, - /// System is moderately constrained, reduce CPU/GPU intensive work - Serious, - /// System is critically constrained, minimize all resource usage - Critical, -} - -/// Metadata for a given [ScreenCaptureSource] -#[derive(Clone)] -pub struct SourceMetadata { - /// Opaque identifier of this screen. - pub id: u64, - /// Human-readable label for this source. - pub label: Option, - /// Whether this source is the main display. - pub is_main: Option, - /// Video resolution of this source. - pub resolution: Size, -} - -/// A source of on-screen video content that can be captured. -pub trait ScreenCaptureSource { - /// Returns metadata for this source. - fn metadata(&self) -> Result; - - /// Start capture video from this source, invoking the given callback - /// with each frame. - fn stream( - &self, - foreground_executor: &ForegroundExecutor, - frame_callback: Box, - ) -> oneshot::Receiver>>; -} - -/// A video stream captured from a screen. -pub trait ScreenCaptureStream { - /// Returns metadata for this source. - fn metadata(&self) -> Result; -} - -/// A frame of video captured from a screen. -pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame); - -/// An opaque identifier for a hardware display -#[derive(PartialEq, Eq, Hash, Copy, Clone)] -pub struct DisplayId(pub(crate) u64); - -impl DisplayId { - /// Create a new `DisplayId` from a raw platform display identifier. - pub fn new(id: u64) -> Self { - Self(id) - } -} - -impl From for DisplayId { - fn from(id: u64) -> Self { - Self(id) - } -} - -impl From for u64 { - fn from(id: DisplayId) -> Self { - id.0 - } -} - -impl Debug for DisplayId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "DisplayId({})", self.0) - } -} - -/// Which part of the window to resize -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ResizeEdge { - /// The top edge - Top, - /// The top right corner - TopRight, - /// The right edge - Right, - /// The bottom right corner - BottomRight, - /// The bottom edge - Bottom, - /// The bottom left corner - BottomLeft, - /// The left edge - Left, - /// The top left corner - TopLeft, -} - -/// A type to describe the appearance of a window -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)] -pub enum WindowDecorations { - #[default] - /// Server side decorations - Server, - /// Client side decorations - Client, -} - -/// A type to describe how this window is currently configured -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)] -pub enum Decorations { - /// The window is configured to use server side decorations - #[default] - Server, - /// The window is configured to use client side decorations - Client { - /// The edge tiling state - tiling: Tiling, - }, -} - -/// What window controls this platform supports -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct WindowControls { - /// Whether this platform supports fullscreen - pub fullscreen: bool, - /// Whether this platform supports maximize - pub maximize: bool, - /// Whether this platform supports minimize - pub minimize: bool, - /// Whether this platform supports a window menu - pub window_menu: bool, -} - -impl Default for WindowControls { - fn default() -> Self { - // Assume that we can do anything, unless told otherwise - Self { - fullscreen: true, - maximize: true, - minimize: true, - window_menu: true, - } - } -} - -/// A window control button type used in [`WindowButtonLayout`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum WindowButton { - /// The minimize button - Minimize, - /// The maximize button - Maximize, - /// The close button - Close, -} - -impl WindowButton { - /// Returns a stable element ID for rendering this button. - pub fn id(&self) -> &'static str { - match self { - WindowButton::Minimize => "minimize", - WindowButton::Maximize => "maximize", - WindowButton::Close => "close", - } - } - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - fn index(&self) -> usize { - match self { - WindowButton::Minimize => 0, - WindowButton::Maximize => 1, - WindowButton::Close => 2, - } - } -} - -/// Maximum number of [`WindowButton`]s per side in the titlebar. -pub const MAX_BUTTONS_PER_SIDE: usize = 3; - -/// Describes which [`WindowButton`]s appear on each side of the titlebar. -/// -/// On Linux, this is read from the desktop environment's configuration -/// (e.g. GNOME's `gtk-decoration-layout` gsetting) via [`WindowButtonLayout::parse`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct WindowButtonLayout { - /// Buttons on the left side of the titlebar. - pub left: [Option; MAX_BUTTONS_PER_SIDE], - /// Buttons on the right side of the titlebar. - pub right: [Option; MAX_BUTTONS_PER_SIDE], -} - -#[cfg(any(target_os = "linux", target_os = "freebsd"))] -impl WindowButtonLayout { - /// Returns Zed's built-in fallback button layout for Linux titlebars. - pub fn linux_default() -> Self { - Self { - left: [None; MAX_BUTTONS_PER_SIDE], - right: [ - Some(WindowButton::Minimize), - Some(WindowButton::Maximize), - Some(WindowButton::Close), - ], - } - } - - /// Parses a GNOME-style `button-layout` string (e.g. `"close,minimize:maximize"`). - pub fn parse(layout_string: &str) -> Result { - fn parse_side( - s: &str, - seen_buttons: &mut [bool; MAX_BUTTONS_PER_SIDE], - unrecognized: &mut Vec, - ) -> [Option; MAX_BUTTONS_PER_SIDE] { - let mut result = [None; MAX_BUTTONS_PER_SIDE]; - let mut i = 0; - for name in s.split(',') { - let trimmed = name.trim(); - if trimmed.is_empty() { - continue; - } - let button = match trimmed { - "minimize" => Some(WindowButton::Minimize), - "maximize" => Some(WindowButton::Maximize), - "close" => Some(WindowButton::Close), - other => { - unrecognized.push(other.to_string()); - None - } - }; - if let Some(button) = button { - if seen_buttons[button.index()] { - continue; - } - if let Some(slot) = result.get_mut(i) { - *slot = Some(button); - seen_buttons[button.index()] = true; - i += 1; - } - } - } - result - } - - let (left_str, right_str) = layout_string.split_once(':').unwrap_or(("", layout_string)); - let mut unrecognized = Vec::new(); - let mut seen_buttons = [false; MAX_BUTTONS_PER_SIDE]; - let layout = Self { - left: parse_side(left_str, &mut seen_buttons, &mut unrecognized), - right: parse_side(right_str, &mut seen_buttons, &mut unrecognized), - }; - - if !unrecognized.is_empty() - && layout.left.iter().all(Option::is_none) - && layout.right.iter().all(Option::is_none) - { - bail!( - "button layout string {:?} contains no valid buttons (unrecognized: {})", - layout_string, - unrecognized.join(", ") - ); - } - - Ok(layout) - } - - /// Formats the layout back into a GNOME-style `button-layout` string. - #[cfg(test)] - pub fn format(&self) -> String { - fn format_side(buttons: &[Option; MAX_BUTTONS_PER_SIDE]) -> String { - buttons - .iter() - .flatten() - .map(|button| match button { - WindowButton::Minimize => "minimize", - WindowButton::Maximize => "maximize", - WindowButton::Close => "close", - }) - .collect::>() - .join(",") - } - - format!("{}:{}", format_side(&self.left), format_side(&self.right)) - } -} - -/// A type to describe which sides of the window are currently tiled in some way -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)] -pub struct Tiling { - /// Whether the top edge is tiled - pub top: bool, - /// Whether the left edge is tiled - pub left: bool, - /// Whether the right edge is tiled - pub right: bool, - /// Whether the bottom edge is tiled - pub bottom: bool, -} - -impl Tiling { - /// Initializes a [`Tiling`] type with all sides tiled - pub fn tiled() -> Self { - Self { - top: true, - left: true, - right: true, - bottom: true, - } - } - - /// Whether any edge is tiled - pub fn is_tiled(&self) -> bool { - self.top || self.left || self.right || self.bottom - } -} - -/// Callbacks for the accessibility adapter. -pub struct A11yCallbacks { - /// Called when the adapter is activated (a screen reader connects). - pub activation: Box Option + Send + 'static>, - /// Called when an action is requested by the screen reader. - pub action: Box, - /// Called when the adapter is deactivated (screen reader disconnects). - pub deactivation: Box, -} - -#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] -#[expect(missing_docs)] -pub struct RequestFrameOptions { - /// Whether a presentation is required. - pub require_presentation: bool, - /// Force refresh of all rendering states when true. - pub force_render: bool, -} - -/// The application's lifecycle phase, as owned and reported by a mobile OS. -/// -/// `Inactive` means visible but not receiving input (a system dialog on -/// top), while `Background` means not visible at all, with process death -/// possible at any time thereafter. -/// -/// | Phase | iOS | Android | -/// |--------------|------------------------------|--------------| -/// | `Active` | `didBecomeActive` | `onResume` | -/// | `Inactive` | `willResignActive` | `onPause` | -/// | `Background` | `didEnterBackground` | `onStop` | -/// | `Foreground` | `willEnterForeground` | `onStart` | -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum AppLifecyclePhase { - /// Foreground and receiving input. - Active, - /// Foreground (visible) but not receiving input. - Inactive, - /// Not visible. The GPU surface may be destroyed while backgrounded and - /// the process may be killed without further notice. - Background, - /// Becoming visible again, before input is restored. - Foreground, -} - -/// Regions of a window that are obscured or reserved by the system. -/// -/// Mobile applications often share space in their window with system-specific -/// geometry, from keyboards to camera notches. In GPUI, all this is abstracted -/// into a single "inset" which should be overlaid on the window's bounds. -/// It is up to the application develop to determine how to handle these cases. -#[derive(Debug, Clone, Default, PartialEq)] -pub struct WindowInsets { - /// Regions covered by system UI or hardware: status bar, display - /// cutouts/notch, home indicator, navigation bars. - /// (iOS: `safeAreaInsets`. Android: `WindowInsets` of types - /// `systemBars() | displayCutout()`.) - pub safe_area: Edges, - /// The region covered by the keyboard, when present. - /// (iOS: derived from `keyboardWillShow`/frame-change notifications. - /// Android: `WindowInsets.Type.ime()`.) - pub ime: Edges, -} - -impl WindowInsets { - /// The combined inset content should avoid. - pub fn effective(&self) -> Edges { - Edges { - top: self.safe_area.top.max(self.ime.top), - right: self.safe_area.right.max(self.ime.right), - bottom: self.safe_area.bottom.max(self.ime.bottom), - left: self.safe_area.left.max(self.ime.left), - } - } -} - -/// A change in the state of the focused text input. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum TextInputStateChange { - /// An editable element gained focus. - FocusGained, - /// The focused editable element lost focus. - FocusLost, - /// The selection or caret moved - SelectionChanged, - /// The document content changed outside of platform-initiated edits. - ContentChanged, -} - -#[expect(missing_docs)] -pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle { - fn bounds(&self) -> Bounds; - fn is_maximized(&self) -> bool; - fn window_bounds(&self) -> WindowBounds; - fn content_size(&self) -> Size; - fn resize(&mut self, size: Size); - fn scale_factor(&self) -> f32; - fn appearance(&self) -> WindowAppearance; - fn display(&self) -> Option>; - fn mouse_position(&self) -> Point; - fn modifiers(&self) -> Modifiers; - fn capslock(&self) -> Capslock; - fn set_input_handler(&mut self, input_handler: PlatformInputHandler); - fn take_input_handler(&mut self) -> Option; - /// Apply the focused text region's [`TextInputConfiguration`] to the - /// platform's text input session (e.g. attributes of the hidden editable - /// element on web). Called only when the configuration changes, because - /// reconfiguring a live input session can restart the IME connection. - fn set_text_input_configuration(&mut self, _configuration: TextInputConfiguration) {} - fn prompt( - &self, - level: PromptLevel, - msg: &str, - detail: Option<&str>, - answers: &[PromptButton], - ) -> Option>; - fn activate(&self); - /// Requests that the operating system draw attention to this window. - fn request_attention(&self) {} - fn is_active(&self) -> bool; - fn is_hovered(&self) -> bool; - fn background_appearance(&self) -> WindowBackgroundAppearance; - fn set_title(&mut self, title: &str); - fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance); - fn minimize(&self); - fn zoom(&self); - fn toggle_fullscreen(&self); - fn is_fullscreen(&self) -> bool; - fn frame_waker(&self) -> Option> { - None - } - fn on_request_frame(&self, callback: Box); - fn on_input(&self, callback: Box DispatchEventResult>); - fn on_active_status_change(&self, callback: Box); - fn on_hover_status_change(&self, callback: Box); - fn on_resize(&self, callback: Box, f32)>); - fn on_moved(&self, callback: Box); - fn on_should_close(&self, callback: Box bool>); - fn on_hit_test_window_control(&self, callback: Box Option>); - fn on_close(&self, callback: Box); - fn on_appearance_changed(&self, callback: Box); - fn on_button_layout_changed(&self, _callback: Box) {} - fn draw(&self, scene: &Scene); - fn schedule_frame(&self) {} - fn sprite_atlas(&self) -> Arc; - fn is_subpixel_rendering_supported(&self) -> bool; - - // macOS specific methods - fn get_title(&self) -> String { - String::new() - } - fn tabbed_windows(&self) -> Option> { - None - } - fn tab_bar_visible(&self) -> bool { - false - } - fn set_edited(&mut self, _edited: bool) {} - fn set_document_path(&self, _path: Option<&std::path::Path>) {} - fn toggle_simple_fullscreen(&self) {} - fn is_simple_fullscreen(&self) -> bool { - false - } - #[cfg(target_os = "macos")] - fn set_traffic_light_position(&self, _position: Point) {} - fn show_character_palette(&self) {} - fn titlebar_double_click(&self, _is_resizable: bool, _is_minimizable: bool) {} - fn on_move_tab_to_new_window(&self, _callback: Box) {} - fn on_merge_all_windows(&self, _callback: Box) {} - fn on_select_previous_tab(&self, _callback: Box) {} - fn on_select_next_tab(&self, _callback: Box) {} - fn on_toggle_tab_bar(&self, _callback: Box) {} - fn merge_all_windows(&self) {} - fn move_tab_to_new_window(&self) {} - fn toggle_window_tab_overview(&self) {} - fn set_tabbing_identifier(&self, _identifier: Option) {} - - #[cfg(target_os = "windows")] - fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND; - - // Linux specific methods - fn inner_window_bounds(&self) -> WindowBounds { - self.window_bounds() - } - fn request_decorations(&self, _decorations: WindowDecorations) {} - fn show_window_menu(&self, _position: Point) {} - fn start_window_move(&self) {} - fn can_start_external_drag(&self) -> bool { - false - } - fn start_external_drag(&self, _payload: &ExternalDragPayload) -> bool { - false - } - fn start_window_resize(&self, _edge: ResizeEdge) {} - fn set_exclusive_zone(&self, _zone: Pixels) {} - #[cfg(all(target_os = "linux", feature = "wayland"))] - fn set_exclusive_edge(&self, _edge: layer_shell::Anchor) {} - fn set_input_region(&self, _region: Option<&[Bounds]>) {} - fn window_decorations(&self) -> Decorations { - Decorations::Server - } - fn set_app_id(&mut self, _app_id: &str) {} - fn map_window(&mut self) -> anyhow::Result<()> { - Ok(()) - } - fn window_controls(&self) -> WindowControls { - WindowControls::default() - } - fn set_client_inset(&self, _inset: Pixels) {} - fn gpu_specs(&self) -> Option; - - fn update_ime_position(&self, _bounds: Bounds); - - // Mobile platform methods. - - /// The regions of this window currently obscured or reserved by the - /// system. Zero on platforms without such regions. - fn insets(&self) -> WindowInsets { - WindowInsets::default() - } - - /// Registers a callback invoked whenever [`Self::insets`] change. - /// - /// Contract: fires continuously during animated transitions (Android - /// `WindowInsetsAnimation` progress; on iOS the platform interpolates - /// the keyboard animation curve on frame ticks) and is exact at rest. - fn on_insets_changed(&self, _callback: Box) {} - - /// Sets the handler for the system back action (Android back - /// button/gesture; no source on iOS or desktop). - fn set_back_handler(&self, _callback: Box) {} - - /// Declares whether the application would currently handle the system - /// back action (e.g. navigation depth > 0). - fn set_back_enabled(&self, _enabled: bool) {} - - /// Requests that the soft keyboard be shown. - fn show_soft_keyboard(&self) {} - - /// Requests that the soft keyboard be hidden. - fn hide_soft_keyboard(&self) {} - - /// Inform the operating system that the text input state has changed - fn text_input_state_changed(&self, _change: TextInputStateChange) {} - - fn play_system_bell(&self) {} - - /// Initialize the accessibility adapter with callbacks. - fn a11y_init(&self, _callbacks: A11yCallbacks) {} - - /// Provide a TreeUpdate to the accessibility adapter. - fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {} - - /// Inform the adapter of updated window bounds. - fn a11y_update_window_bounds(&self) {} - - #[cfg(any(test, feature = "test-support", feature = "bench-support"))] - fn as_test(&mut self) -> Option<&mut TestWindow> { - None - } - - /// Renders the given scene to a texture and returns the pixel data as an RGBA image. - /// This does not present the frame to screen - useful for visual testing where we want - /// to capture what would be rendered without displaying it or requiring the window to be visible. - #[cfg(any(test, feature = "test-support"))] - fn render_to_image(&self, _scene: &Scene) -> Result { - anyhow::bail!("render_to_image not implemented for this platform") - } -} - -/// A renderer for headless windows that can produce real rendered output. -#[cfg(any(test, feature = "test-support", feature = "bench-support"))] -pub trait PlatformHeadlessRenderer { - /// Render a scene and return the result as an RGBA image. - fn render_scene_to_image( - &mut self, - scene: &Scene, - size: Size, - ) -> Result; - - /// Render a scene to an offscreen target without reading the result back. - /// - /// This is the headless analogue of presenting a frame: it performs the - /// same CPU-side scene encoding and GPU submission as drawing to a real - /// window, but doesn't block on GPU completion or copy pixels back. - fn render_scene(&mut self, scene: &Scene, size: Size) -> Result<()>; - - /// Returns the sprite atlas used by this renderer. - fn sprite_atlas(&self) -> Arc; -} - -/// Type alias for runnables with metadata. -/// Previously an enum with a single variant, now simplified to a direct type alias. -#[doc(hidden)] -pub type RunnableVariant = Runnable; - -#[doc(hidden)] -pub type TimerResolutionGuard = gpui_util::Deferred>; - -#[doc(hidden)] -pub enum TasksIncluded { - OnlyCompleted, - CompletedAndRunning, -} - -/// This type is public so that our test macro can generate and use it, but it should not -/// be considered part of our public API. -#[doc(hidden)] -pub trait PlatformDispatcher: Send + Sync { - fn is_main_thread(&self) -> bool; - fn dispatch(&self, runnable: RunnableVariant, priority: Priority); - fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority); - fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant); - - fn dispatch_on_main_thread_when_idle( - &self, - runnable: RunnableVariant, - timeout: Option, - ) { - let _ = timeout; - self.dispatch_on_main_thread(runnable, Priority::Low); - } - - fn idle_time_remaining(&self) -> Option { - None - } - - fn spawn_realtime(&self, f: Box); - - fn now(&self) -> Instant { - Instant::now() - } - - fn increase_timer_resolution(&self) -> TimerResolutionGuard { - gpui_util::defer(Box::new(|| {})) - } - - #[cfg(any(test, feature = "test-support", feature = "bench-support"))] - fn as_test(&self) -> Option<&TestDispatcher> { - None - } - - // This cfg must match the `threaded_dispatcher` module's, which implements - // this method whenever it compiles. - #[cfg(any(test, feature = "test-support", feature = "bench-support"))] - fn as_threaded(&self) -> Option<&ThreadedDispatcher> { - None - } -} - -#[expect(missing_docs)] -pub trait PlatformTextSystem: Send + Sync { - fn add_fonts(&self, fonts: Vec>) -> Result<()>; - /// Get all available font names. - fn all_font_names(&self) -> Vec; - /// Get the font ID for a font descriptor. - fn font_id(&self, descriptor: &Font) -> Result; - /// Prewarm any system font caches needed to shape text. - fn prewarm_fonts(&self, _font_ids: &[FontId]) {} - /// Get metrics for a font. - fn font_metrics(&self, font_id: FontId) -> FontMetrics; - /// Get typographic bounds for a glyph. - fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result>; - /// Get the advance width for a glyph. - fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result>; - /// Get the glyph ID for a character. - fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option; - /// Get raster bounds for a glyph. - fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result>; - /// Rasterize a glyph. - fn rasterize_glyph( - &self, - params: &RenderGlyphParams, - raster_bounds: Bounds, - ) -> Result<(Size, Vec)>; - /// Layout a line of text with the given font runs. - fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout; - /// Returns the recommended text rendering mode for the given font and size. - fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels) - -> TextRenderingMode; - /// Returns the dilation level to use for a glyph painted in the given color. - fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 { - 0 - } -} - -#[expect(missing_docs)] -pub struct NoopTextSystem; - -#[expect(missing_docs)] -impl NoopTextSystem { - #[allow(dead_code)] - pub fn new() -> Self { - Self - } -} - -impl PlatformTextSystem for NoopTextSystem { - fn add_fonts(&self, _fonts: Vec>) -> Result<()> { - Ok(()) - } - - fn all_font_names(&self) -> Vec { - Vec::new() - } - - fn font_id(&self, _descriptor: &Font) -> Result { - Ok(FontId(1)) - } - - fn font_metrics(&self, _font_id: FontId) -> FontMetrics { - FontMetrics { - units_per_em: 1000, - ascent: 1025.0, - descent: -275.0, - line_gap: 0.0, - underline_position: -95.0, - underline_thickness: 60.0, - cap_height: 698.0, - x_height: 516.0, - bounding_box: Bounds { - origin: Point { - x: -260.0, - y: -245.0, - }, - size: Size { - width: 1501.0, - height: 1364.0, - }, - }, - } - } - - fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result> { - Ok(Bounds { - origin: Point { x: 54.0, y: 0.0 }, - size: size(392.0, 528.0), - }) - } - - fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result> { - Ok(size(600.0 * glyph_id.0 as f32, 0.0)) - } - - fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option { - Some(GlyphId(ch.len_utf16() as u32)) - } - - fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result> { - Ok(Default::default()) - } - - fn rasterize_glyph( - &self, - _params: &RenderGlyphParams, - raster_bounds: Bounds, - ) -> Result<(Size, Vec)> { - Ok((raster_bounds.size, Vec::new())) - } - - fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout { - let mut position = px(0.); - let metrics = self.font_metrics(FontId(0)); - let em_width = font_size - * self - .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap()) - .unwrap() - .width - / metrics.units_per_em as f32; - let mut glyphs = Vec::new(); - for (ix, c) in text.char_indices() { - if let Some(glyph) = self.glyph_for_char(FontId(0), c) { - glyphs.push(ShapedGlyph { - id: glyph, - position: point(position, px(0.)), - index: ix, - is_emoji: glyph.0 == 2, - }); - if glyph.0 == 2 { - position += em_width * 2.0; - } else { - position += em_width; - } - } else { - position += em_width - } - } - let mut runs = Vec::default(); - if !glyphs.is_empty() { - runs.push(ShapedRun { - font_id: FontId(0), - glyphs, - }); - } else { - position = px(0.); - } - - LineLayout { - font_size, - width: position, - ascent: font_size * (metrics.ascent / metrics.units_per_em as f32), - descent: font_size * (metrics.descent / metrics.units_per_em as f32), - runs, - len: text.len(), - } - } - - fn recommended_rendering_mode( - &self, - _font_id: FontId, - _font_size: Pixels, - ) -> TextRenderingMode { - TextRenderingMode::Grayscale - } -} - -// Adapted from https://github.com/microsoft/terminal/blob/1283c0f5b99a2961673249fa77c6b986efb5086c/src/renderer/atlas/dwrite.cpp -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -/// Compute gamma correction ratios for subpixel text rendering. -#[allow(dead_code)] -pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] { - const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [ - [0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0], // gamma = 1.0 - [0.0166 / 4.0, -0.0807 / 4.0, 0.2227 / 4.0, -0.0751 / 4.0], // gamma = 1.1 - [0.0350 / 4.0, -0.1760 / 4.0, 0.4325 / 4.0, -0.1370 / 4.0], // gamma = 1.2 - [0.0543 / 4.0, -0.2821 / 4.0, 0.6302 / 4.0, -0.1876 / 4.0], // gamma = 1.3 - [0.0739 / 4.0, -0.3963 / 4.0, 0.8167 / 4.0, -0.2287 / 4.0], // gamma = 1.4 - [0.0933 / 4.0, -0.5161 / 4.0, 0.9926 / 4.0, -0.2616 / 4.0], // gamma = 1.5 - [0.1121 / 4.0, -0.6395 / 4.0, 1.1588 / 4.0, -0.2877 / 4.0], // gamma = 1.6 - [0.1300 / 4.0, -0.7649 / 4.0, 1.3159 / 4.0, -0.3080 / 4.0], // gamma = 1.7 - [0.1469 / 4.0, -0.8911 / 4.0, 1.4644 / 4.0, -0.3234 / 4.0], // gamma = 1.8 - [0.1627 / 4.0, -1.0170 / 4.0, 1.6051 / 4.0, -0.3347 / 4.0], // gamma = 1.9 - [0.1773 / 4.0, -1.1420 / 4.0, 1.7385 / 4.0, -0.3426 / 4.0], // gamma = 2.0 - [0.1908 / 4.0, -1.2652 / 4.0, 1.8650 / 4.0, -0.3476 / 4.0], // gamma = 2.1 - [0.2031 / 4.0, -1.3864 / 4.0, 1.9851 / 4.0, -0.3501 / 4.0], // gamma = 2.2 - ]; - - const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32; - const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32; - - let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10; - let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index]; - - [ - ratios[0] * NORM13, - ratios[1] * NORM24, - ratios[2] * NORM13, - ratios[3] * NORM24, - ] -} - -#[derive(PartialEq, Eq, Hash, Clone)] -#[expect(missing_docs)] -pub enum AtlasKey { - Glyph(RenderGlyphParams), - Svg(RenderSvgParams), - Image(RenderImageParams), -} - -impl AtlasKey { - #[cfg_attr( - all( - any(target_os = "linux", target_os = "freebsd"), - not(any(feature = "x11", feature = "wayland")) - ), - allow(dead_code) - )] - /// Returns the texture kind for this atlas key. - pub fn texture_kind(&self) -> AtlasTextureKind { - match self { - AtlasKey::Glyph(params) => { - if params.is_emoji { - AtlasTextureKind::Polychrome - } else if params.subpixel_rendering { - AtlasTextureKind::Subpixel - } else { - AtlasTextureKind::Monochrome - } - } - AtlasKey::Svg(_) => AtlasTextureKind::Monochrome, - AtlasKey::Image(_) => AtlasTextureKind::Polychrome, - } - } -} - -impl From for AtlasKey { - fn from(params: RenderGlyphParams) -> Self { - Self::Glyph(params) - } -} - -impl From for AtlasKey { - fn from(params: RenderSvgParams) -> Self { - Self::Svg(params) - } -} - -impl From for AtlasKey { - fn from(params: RenderImageParams) -> Self { - Self::Image(params) - } -} - -#[expect(missing_docs)] -pub trait PlatformAtlas { - fn get_or_insert_with<'a>( - &self, - key: &AtlasKey, - build: &mut dyn FnMut() -> Result, Cow<'a, [u8]>)>>, - ) -> Result>; - fn remove(&self, key: &AtlasKey); - - #[cfg(any(test, feature = "test-support", feature = "bench-support"))] - fn contains(&self, _key: &AtlasKey) -> bool { - false - } -} - -#[doc(hidden)] -pub struct AtlasTextureList { - pub textures: Vec>, - pub free_list: Vec, -} - -impl Default for AtlasTextureList { - fn default() -> Self { - Self { - textures: Vec::default(), - free_list: Vec::default(), - } - } -} - -impl ops::Index for AtlasTextureList { - type Output = Option; - - fn index(&self, index: usize) -> &Self::Output { - &self.textures[index] - } -} - -impl AtlasTextureList { - #[allow(unused)] - pub fn drain(&mut self) -> std::vec::Drain<'_, Option> { - self.free_list.clear(); - self.textures.drain(..) - } - - #[allow(dead_code)] - pub fn iter_mut(&mut self) -> impl DoubleEndedIterator { - self.textures.iter_mut().flatten() - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[repr(C)] -#[expect(missing_docs)] -pub struct AtlasTile { - /// The texture this tile belongs to. - pub texture_id: AtlasTextureId, - /// The unique ID of this tile within its texture. - pub tile_id: TileId, - /// Padding around the tile content in pixels. - pub padding: u32, - /// The bounds of this tile within the texture. - pub bounds: Bounds, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -#[repr(C)] -#[expect(missing_docs)] -pub struct AtlasTextureId { - // We use u32 instead of usize for Metal Shader Language compatibility - /// The index of this texture in the atlas. - pub index: u32, - /// The kind of content stored in this texture. - pub kind: AtlasTextureKind, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -#[repr(C)] -#[cfg_attr( - all( - any(target_os = "linux", target_os = "freebsd"), - not(any(feature = "x11", feature = "wayland")) - ), - allow(dead_code) -)] -#[expect(missing_docs)] -pub enum AtlasTextureKind { - Monochrome = 0, - Polychrome = 1, - Subpixel = 2, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -#[repr(C)] -#[expect(missing_docs)] -pub struct TileId(pub u32); - -impl From for TileId { - fn from(id: etagere::AllocId) -> Self { - Self(id.serialize()) - } -} - -impl From for etagere::AllocId { - fn from(id: TileId) -> Self { - Self::deserialize(id.0) - } -} - -#[expect(missing_docs)] -pub struct PlatformInputHandler { - cx: AsyncWindowContext, - handler: Box, -} - -#[expect(missing_docs)] -#[cfg_attr( - all( - any(target_os = "linux", target_os = "freebsd"), - not(any(feature = "x11", feature = "wayland")) - ), - allow(dead_code) -)] -impl PlatformInputHandler { - pub fn new(cx: AsyncWindowContext, handler: Box) -> Self { - Self { cx, handler } - } - - pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option { - self.cx - .update(|window, cx| { - self.handler - .selected_text_range(ignore_disabled_input, window, cx) - }) - .ok() - .flatten() - } - - #[cfg_attr(target_os = "windows", allow(dead_code))] - pub fn marked_text_range(&mut self) -> Option> { - self.cx - .update(|window, cx| self.handler.marked_text_range(window, cx)) - .ok() - .flatten() - } - - #[cfg_attr( - any(target_os = "linux", target_os = "freebsd", target_os = "windows"), - allow(dead_code) - )] - pub fn text_for_range( - &mut self, - range_utf16: Range, - adjusted: &mut Option>, - ) -> Option { - self.cx - .update(|window, cx| { - self.handler - .text_for_range(range_utf16, adjusted, window, cx) - }) - .ok() - .flatten() - } - - pub fn replace_text_in_range(&mut self, replacement_range: Option>, text: &str) { - self.cx - .update(|window, cx| { - self.handler - .replace_text_in_range(replacement_range, text, window, cx); - }) - .ok(); - } - - pub fn replace_and_mark_text_in_range( - &mut self, - range_utf16: Option>, - new_text: &str, - new_selected_range: Option>, - ) { - self.cx - .update(|window, cx| { - self.handler.replace_and_mark_text_in_range( - range_utf16, - new_text, - new_selected_range, - window, - cx, - ) - }) - .ok(); - } - - #[cfg_attr(target_os = "windows", allow(dead_code))] - pub fn unmark_text(&mut self) { - self.cx - .update(|window, cx| self.handler.unmark_text(window, cx)) - .ok(); - } - - pub fn paste(&mut self, item: ClipboardItem) { - self.cx - .update(|window, cx| self.handler.paste(item, window, cx)) - .ok(); - } - - pub fn bounds_for_range(&mut self, range_utf16: Range) -> Option> { - self.cx - .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx)) - .ok() - .flatten() - } - - #[allow(dead_code)] - pub fn apple_press_and_hold_enabled(&mut self) -> bool { - self.handler.apple_press_and_hold_enabled() - } - - pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) { - self.handler.replace_text_in_range(None, input, window, cx); - } - - pub fn compute_ime_candidate_bounds( - marked_range: Option>, - selection: &UTF16Selection, - mut bounds_for_range: impl FnMut(Range) -> Option>, - ) -> Option> { - if let Some(marked_range) = marked_range { - // Default to the start of the marked (composing) range. - let mut line_start = marked_range.start; - - // Walk backward from the caret looking for a line break. A change in - // the Y coordinate means we crossed into the previous visual line, so - // the line start is one position after the break point. - let caret = selection.range.end; - if let Some(caret_bounds) = bounds_for_range(caret..caret) { - for i in (marked_range.start..caret).rev() { - if let Some(b) = bounds_for_range(i..i) { - if (b.origin.y - caret_bounds.origin.y).abs() > px(0.1) { - line_start = i + 1; - break; - } - } - } - } - bounds_for_range(line_start..line_start) - } else { - // No active composition — use the selection endpoint. - let offset = if selection.reversed { - selection.range.start - } else { - selection.range.end - }; - bounds_for_range(offset..offset) - } - } - - pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option> { - let marked_range = self.handler.marked_text_range(window, cx); - let selection = self.handler.selected_text_range(true, window, cx)?; - Self::compute_ime_candidate_bounds(marked_range, &selection, |range| { - self.handler.bounds_for_range(range, window, cx) - }) - } - - pub fn ime_candidate_bounds(&mut self) -> Option> { - let marked_range = self.marked_text_range(); - let selection = self.selected_text_range(true)?; - Self::compute_ime_candidate_bounds(marked_range, &selection, |range| { - self.bounds_for_range(range) - }) - } - - #[allow(unused)] - pub fn character_index_for_point(&mut self, point: Point) -> Option { - self.cx - .update(|window, cx| self.handler.character_index_for_point(point, window, cx)) - .ok() - .flatten() - } - - /// See [`InputHandler::set_selected_text_range`]. - pub fn set_selected_text_range(&mut self, range_utf16: Range) { - self.cx - .update(|window, cx| { - self.handler - .set_selected_text_range(range_utf16, window, cx) - }) - .ok(); - } - - /// See [`InputHandler::element_bounds`]. - pub fn element_bounds(&mut self) -> Option> { - self.cx - .update(|window, cx| self.handler.element_bounds(window, cx)) - .ok() - .flatten() - } - - /// See [`InputHandler::text_length_utf16`]. - pub fn text_length_utf16(&mut self) -> Option { - self.cx - .update(|window, cx| self.handler.text_length_utf16(window, cx)) - .ok() - .flatten() - } - - #[allow(dead_code)] - pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool { - self.handler.accepts_text_input(window, cx) - } - - #[allow(dead_code)] - pub fn query_accepts_text_input(&mut self) -> bool { - self.cx - .update(|window, cx| self.handler.accepts_text_input(window, cx)) - .unwrap_or(true) - } - - /// See [`InputHandler::prefers_ime_for_printable_keys`]. - /// - /// This is not a pure delegation to the handler: while a multi-stroke binding is pending this - /// returns `false` regardless of the handler's preference, because the next printable key may - /// complete a binding whose prefix already bypassed the IME. - pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool { - self.cx - .update(|window, cx| { - // The next printable key may complete a chord whose prefix bypassed the IME. - !window.has_pending_keystrokes() - && self.handler.prefers_ime_for_printable_keys(window, cx) - }) - .unwrap_or(false) - } - - /// See [`InputHandler::text_input_configuration`]. - pub fn text_input_configuration( - &mut self, - window: &mut Window, - cx: &mut App, - ) -> TextInputConfiguration { - self.handler.text_input_configuration(window, cx) - } - - /// See [`InputHandler::text_input_editable_range`]. - pub fn text_input_editable_range(&mut self) -> Option> { - self.cx - .update(|window, cx| self.handler.text_input_editable_range(window, cx)) - .ok() - .flatten() - } -} - -/// A struct representing a selection in a text buffer, in UTF16 characters. -/// This is different from a range because the head may be before the tail. -#[derive(Debug)] -pub struct UTF16Selection { - /// The range of text in the document this selection corresponds to - /// in UTF16 characters. - pub range: Range, - /// Whether the head of this selection is at the start (true), or end (false) - /// of the range - pub reversed: bool, -} - -/// Zed's interface for handling text input from the platform's IME system -/// This is currently a 1:1 exposure of the NSTextInputClient API: -/// -/// -pub trait InputHandler: 'static { - /// Get the range of the user's currently selected text, if any - /// Corresponds to [selectedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438242-selectedrange) - /// - /// Return value is in terms of UTF-16 characters, from 0 to the length of the document - fn selected_text_range( - &mut self, - ignore_disabled_input: bool, - window: &mut Window, - cx: &mut App, - ) -> Option; - - /// Get the range of the currently marked text, if any - /// Corresponds to [markedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438250-markedrange) - /// - /// Return value is in terms of UTF-16 characters, from 0 to the length of the document - fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option>; - - /// Get the text for the given document range in UTF-16 characters - /// Corresponds to [attributedSubstring(forProposedRange: actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438238-attributedsubstring) - /// - /// range_utf16 is in terms of UTF-16 characters - fn text_for_range( - &mut self, - range_utf16: Range, - adjusted_range: &mut Option>, - window: &mut Window, - cx: &mut App, - ) -> Option; - - /// Replace the text in the given document range with the given text - /// Corresponds to [insertText(_:replacementRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438258-inserttext) - /// - /// replacement_range is in terms of UTF-16 characters - fn replace_text_in_range( - &mut self, - replacement_range: Option>, - text: &str, - window: &mut Window, - cx: &mut App, - ); - - /// Replace the text in the given document range with the given text, - /// and mark the given text as part of an IME 'composing' state - /// Corresponds to [setMarkedText(_:selectedRange:replacementRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438246-setmarkedtext) - /// - /// range_utf16 is in terms of UTF-16 characters - /// new_selected_range is in terms of UTF-16 characters - fn replace_and_mark_text_in_range( - &mut self, - range_utf16: Option>, - new_text: &str, - new_selected_range: Option>, - window: &mut Window, - cx: &mut App, - ); - - /// Remove the IME 'composing' state from the document - /// Corresponds to [unmarkText()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438239-unmarktext) - fn unmark_text(&mut self, window: &mut Window, cx: &mut App); - - /// Insert a platform-initiated paste at the current selection. - /// - /// Platforms that deliver paste as an input event rather than through an - /// application-defined action (e.g. the DOM `paste` event on web) call - /// this with the full clipboard contents. The default implementation - /// inserts only the plain-text portion of the item. - fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut App) { - if let Some(text) = item.text() { - self.replace_text_in_range(None, &text, window, cx); - } - } - - /// Get the bounds of the given document range in screen coordinates - /// Corresponds to [firstRect(forCharacterRange:actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438240-firstrect) - /// - /// This is used for positioning the IME candidate window - fn bounds_for_range( - &mut self, - range_utf16: Range, - window: &mut Window, - cx: &mut App, - ) -> Option>; - - /// Get the character offset for the given point in terms of UTF16 characters - /// - /// Corresponds to [characterIndexForPoint:](https://developer.apple.com/documentation/appkit/nstextinputclient/characterindex(for:)) - fn character_index_for_point( - &mut self, - point: Point, - window: &mut Window, - cx: &mut App, - ) -> Option; - - /// Set the range of the user's currently selected text. - /// - /// This is the reverse data-flow direction from [`Self::selected_text_range`]: - /// platforms call it when the system text machinery moves the selection on the - /// application's behalf — e.g. the user drags a system selection handle or - /// invokes Select All from system UI (iOS `UITextInput setSelectedTextRange:`, - /// Android `InputConnection.setSelection`). - /// - /// range_utf16 is in terms of UTF-16 characters, from 0 to the length of the document - fn set_selected_text_range( - &mut self, - _range_utf16: Range, - _window: &mut Window, - _cx: &mut App, - ) { - } - - /// Get the bounds of the focused text element in window coordinates, if known. - /// - /// This is the pull counterpart to the [`PlatformWindow::update_ime_position`] - /// push: mobile platforms ask for the focused element's geometry when they - /// need it (e.g. to frame system text-interaction UI overlaid on the focused - /// element). - fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option> { - None - } - - /// Get the length of the document in UTF-16 characters, if known. - fn text_length_utf16(&mut self, _window: &mut Window, _cx: &mut App) -> Option { - None - } - - /// Allows a given input context to opt into getting raw key repeats instead of - /// sending these to the platform. - /// TODO: Ideally we should be able to set ApplePressAndHoldEnabled in NSUserDefaults - /// (which is how iTerm does it) but it doesn't seem to work for me. - #[allow(dead_code)] - fn apple_press_and_hold_enabled(&mut self) -> bool { - true - } - - /// Returns whether this handler is accepting text input to be inserted. - fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool { - true - } - - /// The contiguous range of text, in UTF-16 code units, that platform text - /// input may read and edit around the current selection. - /// - /// Platforms that mirror document text into an IME-editable buffer clamp - /// the mirrored window to this range, so multi-step IME edit gestures - /// (word deletion, autocorrect rewrites, suggestion picks) cannot reach - /// content outside it. The range should contain the current selection; - /// when it cannot (a selection spanning a region boundary), platforms - /// degrade the mirrored IME context rather than widening the range. - /// `None` places no bound. - fn text_input_editable_range( - &mut self, - _window: &mut Window, - _cx: &mut App, - ) -> Option> { - None - } - - /// Returns whether printable keys should be routed to the IME before keybinding - /// matching when a non-ASCII input source (e.g. Japanese, Korean, Chinese IME) - /// is active. This prevents multi-stroke keybindings like `jj` from intercepting - /// keys that the IME should compose. - /// - /// Defaults to `false`. The editor overrides this based on whether it expects - /// character input (e.g. Vim insert mode returns `true`, normal mode returns `false`). - /// The terminal keeps the default `false` so that raw keys reach the terminal process. - fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool { - false - } - - /// Get this handler's preferences for platform text assistance. - /// - /// GPUI re-queries this every frame and forwards it to the platform window - /// only when it changes, so implementations must be cheap and may vary the - /// result with application state (e.g. with the cursor's position). - fn text_input_configuration( - &mut self, - _window: &mut Window, - _cx: &mut App, - ) -> TextInputConfiguration { - TextInputConfiguration::default() - } -} - -/// Platform text-assistance preferences for the focused text region. -/// -/// Returned by [`InputHandler::text_input_configuration`] and forwarded to the -/// platform whenever it changes; the platform maps the fields onto its native -/// input-session attributes (on web, DOM attributes of the hidden editable -/// element such as `autocorrect` and `enterkeyhint`). -/// -/// The default disables all text assistance and requests no particular action -/// key presentation. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct TextInputConfiguration { - /// Whether the platform may automatically correct entered text. - pub autocorrect: bool, - /// How software keyboards automatically capitalize entered text. - pub autocapitalize: Autocapitalize, - /// Whether software keyboards may offer word suggestions and spellcheck. - pub suggestions: bool, - /// The action advertised on a software keyboard's confirm ("enter") key. - pub input_action: TextInputAction, -} - -/// Automatic capitalization applied by software keyboards. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum Autocapitalize { - /// No automatic capitalization. - #[default] - None, - /// Capitalize the first letter of each word. - Words, - /// Capitalize the first letter of each sentence. - Sentences, - /// Capitalize every letter. - Characters, -} - -/// The action a software keyboard advertises on its confirm ("enter") key. -/// -/// This affects only how the key is presented (icon or label); pressing it is -/// still delivered as ordinary input. -/// -/// The variants are the HTML `enterkeyhint` attribute's value set -/// (), -/// which also maps onto Android's `IME_ACTION_*` constants and iOS's -/// `UIReturnKeyType`; [`TextInputAction::Unspecified`] means "emit no hint". -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum TextInputAction { - /// Let the platform choose its default presentation. - #[default] - Unspecified, - /// Inserting a line break. - Enter, - /// Committing the field's value. - Done, - /// Navigating to the typed target. - Go, - /// Moving to the next field. - Next, - /// Moving to the previous field. - Previous, - /// Executing a search. - Search, - /// Sending a message. - Send, -} - -/// The variables that can be configured when creating a new window -#[derive(Debug)] -pub struct WindowOptions { - /// Specifies the state and bounds of the window in screen coordinates. - /// - `None`: Inherit the bounds. - /// - `Some(WindowBounds)`: Open a window with corresponding state and its restore size. - pub window_bounds: Option, - - /// The titlebar configuration of the window - pub titlebar: Option, - - /// Whether the window should be focused when created - pub focus: bool, - - /// Whether the window should be shown when created - pub show: bool, - - /// The kind of window to create - pub kind: WindowKind, - - /// Whether the window can be moved by the user. When `false`, the user cannot drag - /// the window (on macOS this sets `NSWindow.isMovable`, which also disables the - /// Window-menu tiling items); programmatic moves are still allowed. - pub is_movable: bool, - - /// Whether the application owns dragging of the (custom) titlebar, rather than - /// AppKit. Only has an effect on macOS. - /// - /// Set this to `true` for windows that draw their own titlebar and move the window - /// themselves via [`Window::start_window_move`]. It marks the whole content view as - /// app-owned titlebar content, so AppKit neither drags the window from the titlebar - /// nor delays titlebar clicks while disambiguating double-clicks (a delay first - /// observed on macOS 27). It is independent of `is_movable`, so such windows stay - /// user-movable (via their own drag) and keep the Window-menu tiling items enabled. - /// - /// Leave this `false` for windows that rely on AppKit's native titlebar dragging. - pub app_owns_titlebar_drag: bool, - - /// The minimum interval between animation frames while the window is inactive. - /// - /// Set to `None` to disable inactive-window animation frame throttling. - pub inactive_frame_interval: Option, - - /// Whether the window should be resizable by the user - pub is_resizable: bool, - - /// Whether the window should be minimized by the user - pub is_minimizable: bool, - - /// The display to create the window on, if this is None, - /// the window will be created on the main display - pub display_id: Option, - - /// The appearance of the window background. - pub window_background: WindowBackgroundAppearance, - - /// Application identifier of the window. Can by used by desktop environments to group applications together. - pub app_id: Option, - - /// Window minimum size - pub window_min_size: Option>, - - /// Whether to use client or server-side decorations on X11 and Wayland. - /// The platform may ignore requests it cannot satisfy. - pub window_decorations: Option, - - /// Icon image (X11 only) - pub icon: Option>, - - /// Tab group name, allows opening the window as a native tab on macOS 10.12+. Windows with the same tabbing identifier will be grouped together. - pub tabbing_identifier: Option, -} - -/// The variables that can be configured when creating a new window -#[derive(Debug)] -#[cfg_attr( - all( - any(target_os = "linux", target_os = "freebsd"), - not(any(feature = "x11", feature = "wayland")) - ), - allow(dead_code) -)] -#[allow(missing_docs)] -pub struct WindowParams { - pub bounds: Bounds, - - /// The titlebar configuration of the window - #[cfg_attr(feature = "wayland", allow(dead_code))] - pub titlebar: Option, - - /// The kind of window to create - #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] - pub kind: WindowKind, - - /// Whether the window should be movable by the user - #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] - pub is_movable: bool, - - /// Whether the application owns dragging of the (custom) titlebar (macOS only) - #[cfg_attr( - any(target_os = "linux", target_os = "freebsd", target_os = "windows"), - allow(dead_code) - )] - pub app_owns_titlebar_drag: bool, - - /// Whether the window should be resizable by the user - #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] - pub is_resizable: bool, - - /// Whether the window should be minimized by the user - #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] - pub is_minimizable: bool, - - #[cfg_attr( - any(target_os = "linux", target_os = "freebsd", target_os = "windows"), - allow(dead_code) - )] - pub focus: bool, - - #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] - pub show: bool, - - /// An image to set as the window icon (x11 only) - #[cfg_attr(feature = "wayland", allow(dead_code))] - pub icon: Option>, - - #[cfg_attr(feature = "wayland", allow(dead_code))] - pub display_id: Option, - - #[cfg_attr(feature = "wayland", allow(dead_code))] - pub app_id: Option, - - pub window_min_size: Option>, - - #[cfg(target_os = "macos")] - pub tabbing_identifier: Option, -} - -/// Represents the status of how a window should be opened. -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum WindowBounds { - /// Indicates that the window should open in a windowed state with the given bounds. - Windowed(Bounds), - /// Indicates that the window should open in a maximized state. - /// The bounds provided here represent the restore size of the window. - Maximized(Bounds), - /// Indicates that the window should open in fullscreen mode. - /// The bounds provided here represent the restore size of the window. - Fullscreen(Bounds), -} - -impl Default for WindowBounds { - fn default() -> Self { - WindowBounds::Windowed(Bounds::default()) - } -} - -impl WindowBounds { - /// Retrieve the inner bounds - pub fn get_bounds(&self) -> Bounds { - match self { - WindowBounds::Windowed(bounds) => *bounds, - WindowBounds::Maximized(bounds) => *bounds, - WindowBounds::Fullscreen(bounds) => *bounds, - } - } - - /// Creates a new window bounds that centers the window on the screen. - pub fn centered(size: Size, cx: &App) -> Self { - WindowBounds::Windowed(Bounds::centered(None, size, cx)) - } -} - -impl Default for WindowOptions { - fn default() -> Self { - Self { - window_bounds: None, - titlebar: Some(TitlebarOptions { - title: Default::default(), - appears_transparent: Default::default(), - traffic_light_position: Default::default(), - }), - focus: true, - show: true, - kind: WindowKind::Normal, - is_movable: true, - app_owns_titlebar_drag: false, - inactive_frame_interval: Some(Duration::from_micros(33_333)), - is_resizable: true, - is_minimizable: true, - display_id: None, - window_background: WindowBackgroundAppearance::default(), - icon: None, - app_id: None, - window_min_size: None, - window_decorations: None, - tabbing_identifier: None, - } - } -} - -/// The options that can be configured for a window's titlebar -#[derive(Debug, Default)] -pub struct TitlebarOptions { - /// The initial title of the window - pub title: Option, - - /// Should the default system titlebar be hidden to allow for a custom-drawn titlebar? (macOS and Windows only) - /// Refer to [`WindowOptions::window_decorations`] on Linux - pub appears_transparent: bool, - - /// The position of the macOS traffic light buttons - pub traffic_light_position: Option>, -} - -/// The kind of window to create -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum WindowKind { - /// A normal application window - Normal, - - /// A window that appears above all other windows, usually used for alerts or popups - /// use sparingly! - PopUp, - - /// A parent-anchored, platform-native popup window for menus, comboboxes, context menus and - /// tooltips. Unlike [`WindowKind::PopUp`], it is positioned relative to a parent window. - /// - /// The popup's size comes from [`WindowOptions::window_bounds`], whose origin is ignored. - /// See [`popup::PopupOptions`] for the placement options. Platforms without a native - /// implementation reject it with [`popup::PopupNotSupportedError`]. - AnchoredPopup(popup::PopupOptions), - - /// A floating window that appears on top of its parent window - Floating, - - /// A Wayland LayerShell window, used to draw overlays or backgrounds for applications such as - /// docks, notifications or wallpapers. - #[cfg(all(target_os = "linux", feature = "wayland"))] - LayerShell(layer_shell::LayerShellOptions), - - /// A window that appears on top of its parent window and blocks interaction with it - /// until the modal window is closed - Dialog, -} - -/// The appearance of the window, as defined by the operating system. -/// -/// On macOS, this corresponds to named [`NSAppearance`](https://developer.apple.com/documentation/appkit/nsappearance) -/// values. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -pub enum WindowAppearance { - /// A light appearance. - /// - /// On macOS, this corresponds to the `aqua` appearance. - #[default] - Light, - - /// A light appearance with vibrant colors. - /// - /// On macOS, this corresponds to the `NSAppearanceNameVibrantLight` appearance. - VibrantLight, - - /// A dark appearance. - /// - /// On macOS, this corresponds to the `darkAqua` appearance. - Dark, - - /// A dark appearance with vibrant colors. - /// - /// On macOS, this corresponds to the `NSAppearanceNameVibrantDark` appearance. - VibrantDark, -} - -/// The appearance of the background of the window itself, when there is -/// no content or the content is transparent. -#[derive(Copy, Clone, Debug, Default, PartialEq)] -pub enum WindowBackgroundAppearance { - /// Opaque. - /// - /// This lets the window manager know that content behind this - /// window does not need to be drawn. - /// - /// Actual color depends on the system and themes should define a fully - /// opaque background color instead. - #[default] - Opaque, - /// Plain alpha transparency. - Transparent, - /// Transparency, but the contents behind the window are blurred. - /// - /// Not always supported. - Blurred, - /// The Mica backdrop material, supported on Windows 11. - MicaBackdrop, - /// The Mica Alt backdrop material, supported on Windows 11. - MicaAltBackdrop, -} - -/// The text rendering mode to use for drawing glyphs. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -pub enum TextRenderingMode { - /// Use the platform's default text rendering mode. - #[default] - PlatformDefault, - /// Use subpixel (ClearType-style) text rendering. - Subpixel, - /// Use grayscale text rendering. - Grayscale, -} - -/// The options that can be configured for a file dialog prompt -#[derive(Clone, Debug)] -pub struct PathPromptOptions { - /// Should the prompt allow files to be selected? - pub files: bool, - /// Should the prompt allow directories to be selected? - pub directories: bool, - /// Should the prompt allow multiple files to be selected? - pub multiple: bool, - /// The prompt to show to a user when selecting a path - pub prompt: Option, -} - -/// What kind of prompt styling to show -#[derive(Copy, Clone, Debug, PartialEq)] -pub enum PromptLevel { - /// A prompt that is shown when the user should be notified of something - Info, - - /// A prompt that is shown when the user needs to be warned of a potential problem - Warning, - - /// A prompt that is shown when a critical problem has occurred - Critical, -} - -/// Prompt Button -#[derive(Clone, Debug, PartialEq)] -pub enum PromptButton { - /// Ok button - Ok(SharedString), - /// Cancel button - Cancel(SharedString), - /// Other button - Other(SharedString), -} - -impl PromptButton { - /// Create a button with label - pub fn new(label: impl Into) -> Self { - PromptButton::Other(label.into()) - } - - /// Create an Ok button - pub fn ok(label: impl Into) -> Self { - PromptButton::Ok(label.into()) - } - - /// Create a Cancel button - pub fn cancel(label: impl Into) -> Self { - PromptButton::Cancel(label.into()) - } - - /// Returns true if this button is a cancel button. - #[allow(dead_code)] - pub fn is_cancel(&self) -> bool { - matches!(self, PromptButton::Cancel(_)) - } - - /// Returns the label of the button - pub fn label(&self) -> &SharedString { - match self { - PromptButton::Ok(label) => label, - PromptButton::Cancel(label) => label, - PromptButton::Other(label) => label, - } - } -} - -impl From<&str> for PromptButton { - fn from(value: &str) -> Self { - match value.to_lowercase().as_str() { - "ok" => PromptButton::Ok("OK".into()), - "cancel" => PromptButton::Cancel("Cancel".into()), - _ => PromptButton::Other(SharedString::from(value.to_owned())), - } - } -} - -/// The style of the cursor (pointer) -#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] -pub enum CursorStyle { - /// The default cursor - #[default] - Arrow, - - /// A text input cursor - /// corresponds to the CSS cursor value `text` - IBeam, - - /// A crosshair cursor - /// corresponds to the CSS cursor value `crosshair` - Crosshair, - - /// A closed hand cursor - /// corresponds to the CSS cursor value `grabbing` - ClosedHand, - - /// An open hand cursor - /// corresponds to the CSS cursor value `grab` - OpenHand, - - /// A pointing hand cursor - /// corresponds to the CSS cursor value `pointer` - PointingHand, - - /// A resize left cursor - /// corresponds to the CSS cursor value `w-resize` - ResizeLeft, - - /// A resize right cursor - /// corresponds to the CSS cursor value `e-resize` - ResizeRight, - - /// A resize cursor to the left and right - /// corresponds to the CSS cursor value `ew-resize` - ResizeLeftRight, - - /// A resize up cursor - /// corresponds to the CSS cursor value `n-resize` - ResizeUp, - - /// A resize down cursor - /// corresponds to the CSS cursor value `s-resize` - ResizeDown, - - /// A resize cursor directing up and down - /// corresponds to the CSS cursor value `ns-resize` - ResizeUpDown, - - /// A resize cursor directing up-left and down-right - /// corresponds to the CSS cursor value `nesw-resize` - ResizeUpLeftDownRight, - - /// A resize cursor directing up-right and down-left - /// corresponds to the CSS cursor value `nwse-resize` - ResizeUpRightDownLeft, - - /// A cursor indicating that the item/column can be resized horizontally. - /// corresponds to the CSS cursor value `col-resize` - ResizeColumn, - - /// A cursor indicating that the item/row can be resized vertically. - /// corresponds to the CSS cursor value `row-resize` - ResizeRow, - - /// A text input cursor for vertical layout - /// corresponds to the CSS cursor value `vertical-text` - IBeamCursorForVerticalLayout, - - /// A cursor indicating that the operation is not allowed - /// corresponds to the CSS cursor value `not-allowed` - OperationNotAllowed, - - /// A cursor indicating that the operation will result in a link - /// corresponds to the CSS cursor value `alias` - DragLink, - - /// A cursor indicating that the operation will result in a copy - /// corresponds to the CSS cursor value `copy` - DragCopy, - - /// A cursor indicating that the operation will result in a context menu - /// corresponds to the CSS cursor value `context-menu` - ContextualMenu, -} - -/// A clipboard item that should be copied to the clipboard -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ClipboardItem { - /// The entries in this clipboard item. - pub entries: Vec, -} - -/// An error produced by [`Platform::read_from_clipboard_async`]. -/// -/// Callers surface these failures to users, so the variants distinguish -/// conditions that call for different user-facing guidance. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum ClipboardReadError { - /// The platform clipboard is not available in this context, e.g. the - /// browser does not expose the async clipboard API or the page is not a - /// secure context. - Unavailable, - /// The platform refused access, e.g. the user declined the browser's - /// clipboard permission prompt or paste confirmation. - Denied(String), - /// The clipboard contents could not be converted into a - /// [`ClipboardItem`]. - UnsupportedContent, -} - -impl std::fmt::Display for ClipboardReadError { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Unavailable => formatter.write_str("the clipboard is unavailable"), - Self::Denied(message) => { - write!(formatter, "clipboard access was denied: {message}") - } - Self::UnsupportedContent => { - formatter.write_str("the clipboard contents are unsupported") - } - } - } -} - -impl std::error::Error for ClipboardReadError {} - -/// Either a ClipboardString or a ClipboardImage -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum ClipboardEntry { - /// A string entry - String(ClipboardString), - /// An image entry - Image(Image), - /// A file entry - ExternalPaths(crate::ExternalPaths), -} - -impl ClipboardItem { - /// Create a new ClipboardItem::String with no associated metadata - pub fn new_string(text: String) -> Self { - Self { - entries: vec![ClipboardEntry::String(ClipboardString::new(text))], - } - } - - /// Create a new ClipboardItem::String with the given text and associated metadata - pub fn new_string_with_metadata(text: String, metadata: String) -> Self { - Self { - entries: vec![ClipboardEntry::String(ClipboardString { - text, - metadata: Some(metadata), - })], - } - } - - /// Create a new ClipboardItem::String with the given text and associated metadata - pub fn new_string_with_json_metadata(text: String, metadata: T) -> Self { - Self { - entries: vec![ClipboardEntry::String( - ClipboardString::new(text).with_json_metadata(metadata), - )], - } - } - - /// Create a new ClipboardItem::Image with the given image with no associated metadata - pub fn new_image(image: &Image) -> Self { - Self { - entries: vec![ClipboardEntry::Image(image.clone())], - } - } - - /// Concatenates together all the ClipboardString entries in the item. - /// Returns None if there were no ClipboardString entries. - pub fn text(&self) -> Option { - let mut answer = String::new(); - - for entry in self.entries.iter() { - if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry { - answer.push_str(text); - } - } - - if answer.is_empty() { - for entry in self.entries.iter() { - if let ClipboardEntry::ExternalPaths(paths) = entry { - for path in &paths.0 { - use std::fmt::Write as _; - _ = write!(answer, "{}", path.display()); - } - } - } - } - - if !answer.is_empty() { - Some(answer) - } else { - None - } - } - - /// If this item is one ClipboardEntry::String, returns its metadata. - #[cfg_attr(not(target_os = "windows"), allow(dead_code))] - pub fn metadata(&self) -> Option<&String> { - match self.entries().first() { - Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => { - clipboard_string.metadata.as_ref() - } - _ => None, - } - } - - /// Get the item's entries - pub fn entries(&self) -> &[ClipboardEntry] { - &self.entries - } - - /// Get owned versions of the item's entries - pub fn into_entries(self) -> impl Iterator { - self.entries.into_iter() - } -} - -impl From for ClipboardEntry { - fn from(value: ClipboardString) -> Self { - Self::String(value) - } -} - -impl From for ClipboardEntry { - fn from(value: String) -> Self { - Self::from(ClipboardString::from(value)) - } -} - -impl From for ClipboardEntry { - fn from(value: Image) -> Self { - Self::Image(value) - } -} - -impl From for ClipboardItem { - fn from(value: ClipboardEntry) -> Self { - Self { - entries: vec![value], - } - } -} - -impl From for ClipboardItem { - fn from(value: String) -> Self { - Self::from(ClipboardEntry::from(value)) - } -} - -impl From for ClipboardItem { - fn from(value: Image) -> Self { - Self::from(ClipboardEntry::from(value)) - } -} - -/// One of the editor's supported image formats (e.g. PNG, JPEG) - used when dealing with images in the clipboard -#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)] -pub enum ImageFormat { - // Sorted from most to least likely to be pasted into an editor, - // which matters when we iterate through them trying to see if - // clipboard content matches them. - /// .png - Png, - /// .jpeg or .jpg - Jpeg, - /// .webp - Webp, - /// .gif - Gif, - /// .svg - Svg, - /// .bmp - Bmp, - /// .tif or .tiff - Tiff, - /// .ico - Ico, - /// Netpbm image formats (.pbm, .ppm, .pgm). - Pnm, -} - -impl ImageFormat { - /// Returns the mime type for the ImageFormat - pub const fn mime_type(self) -> &'static str { - match self { - ImageFormat::Png => "image/png", - ImageFormat::Jpeg => "image/jpeg", - ImageFormat::Webp => "image/webp", - ImageFormat::Gif => "image/gif", - ImageFormat::Svg => "image/svg+xml", - ImageFormat::Bmp => "image/bmp", - ImageFormat::Tiff => "image/tiff", - ImageFormat::Ico => "image/ico", - ImageFormat::Pnm => "image/x-portable-anymap", - } - } - - /// Returns the file extension for this image format (without leading dot). - pub const fn extension(self) -> &'static str { - match self { - ImageFormat::Png => "png", - ImageFormat::Jpeg => "jpg", - ImageFormat::Webp => "webp", - ImageFormat::Gif => "gif", - ImageFormat::Svg => "svg", - ImageFormat::Bmp => "bmp", - ImageFormat::Tiff => "tiff", - ImageFormat::Ico => "ico", - ImageFormat::Pnm => "pnm", - } - } - - /// Returns the ImageFormat for the given mime type, including known aliases. - pub fn from_mime_type(mime_type: &str) -> Option { - use strum::IntoEnumIterator; - Self::iter() - .find(|format| format.mime_type() == mime_type) - .or_else(|| Self::from_mime_type_alias(mime_type)) - } - - /// Non-canonical mime types that some producers use in the wild. - /// Unlike `mime_type()` which returns the single canonical form, - /// these are legacy or shortened variants we still need to recognize. - fn from_mime_type_alias(mime_type: &str) -> Option { - match mime_type { - "image/jpg" => Some(Self::Jpeg), - "image/tif" => Some(Self::Tiff), - _ => None, - } - } -} - -/// An image, with a format and certain bytes -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Image { - /// The image format the bytes represent (e.g. PNG) - pub format: ImageFormat, - /// The raw image bytes - pub bytes: Vec, - /// The unique ID for the image - pub id: u64, -} - -pub(crate) fn decode_static_image( - bytes: &[u8], - format: image::ImageFormat, -) -> Result> { - let decoder = image::ImageReader::with_format(Cursor::new(bytes), format) - .into_decoder() - .context("creating image decoder")?; - decode_static_image_from_decoder(decoder) -} - -pub(crate) fn decode_static_image_from_decoder( - mut decoder: impl image::ImageDecoder, -) -> Result> { - let orientation = decoder - .orientation() - .context("reading decoder's orientation")?; - let mut image = DynamicImage::from_decoder(decoder).context("decoding image")?; - image.apply_orientation(orientation); - - let mut data = image.into_rgba8(); - for pixel in data.chunks_exact_mut(4) { - pixel.swap(0, 2); - } - - Ok(SmallVec::from_elem(Frame::new(data), 1)) -} - -impl Hash for Image { - fn hash(&self, state: &mut H) { - state.write_u64(self.id); - } -} - -impl Image { - /// An empty image containing no data - pub fn empty() -> Self { - Self::from_bytes(ImageFormat::Png, Vec::new()) - } - - /// Create an image from a format and bytes - pub fn from_bytes(format: ImageFormat, bytes: Vec) -> Self { - Self { - id: hash(&bytes), - format, - bytes, - } - } - - /// Get this image's ID - pub fn id(&self) -> u64 { - self.id - } - - /// Use the GPUI `use_asset` API to make this image renderable - pub fn use_render_image( - self: Arc, - window: &mut Window, - cx: &mut App, - ) -> Option> { - ImageSource::Image(self) - .use_data(None, window, cx) - .and_then(|result| result.ok()) - } - - /// Use the GPUI `get_asset` API to make this image renderable - pub fn get_render_image( - self: Arc, - window: &mut Window, - cx: &mut App, - ) -> Option> { - ImageSource::Image(self) - .get_data(None, window, cx) - .and_then(|result| result.ok()) - } - - /// Use the GPUI `remove_asset` API to drop this image, if possible. - pub fn remove_asset(self: Arc, cx: &mut App) { - ImageSource::Image(self).remove_asset(cx); - } - - /// Check whether this image is present in GPUI's asset cache (loading or - /// loaded), without fetching it. - #[cfg(any(test, feature = "test-support"))] - pub fn is_asset_cached(self: &Arc, cx: &App) -> bool { - ImageSource::Image(self.clone()).is_asset_cached(cx) - } - - /// Convert the clipboard image to an `ImageData` object. - pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result> { - let frames = match self.format { - ImageFormat::Gif => { - let decoder = GifDecoder::new(Cursor::new(&self.bytes))?; - let mut frames = SmallVec::new(); - - for frame in decoder.into_frames() { - match frame { - Ok(mut frame) => { - // Convert from RGBA to BGRA. - for pixel in frame.buffer_mut().chunks_exact_mut(4) { - pixel.swap(0, 2); - } - frames.push(frame); - } - Err(err) => { - log::debug!("Skipping GIF frame due to decode error: {err}"); - } - } - } - - if frames.is_empty() { - anyhow::bail!("GIF could not be decoded: all frames failed"); - } - - frames - } - ImageFormat::Png => decode_static_image(&self.bytes, image::ImageFormat::Png)?, - ImageFormat::Jpeg => decode_static_image(&self.bytes, image::ImageFormat::Jpeg)?, - ImageFormat::Webp => decode_static_image(&self.bytes, image::ImageFormat::WebP)?, - ImageFormat::Bmp => decode_static_image(&self.bytes, image::ImageFormat::Bmp)?, - ImageFormat::Tiff => decode_static_image(&self.bytes, image::ImageFormat::Tiff)?, - ImageFormat::Ico => decode_static_image(&self.bytes, image::ImageFormat::Ico)?, - ImageFormat::Svg => { - return svg_renderer - .render_single_frame(&self.bytes, 1.0) - .map_err(Into::into); - } - ImageFormat::Pnm => decode_static_image(&self.bytes, image::ImageFormat::Pnm)?, - }; - - Ok(Arc::new(RenderImage::new(frames))) - } - - /// Get the format of the clipboard image - pub fn format(&self) -> ImageFormat { - self.format - } - - /// Get the raw bytes of the clipboard image - pub fn bytes(&self) -> &[u8] { - self.bytes.as_slice() - } -} - -/// A clipboard item that should be copied to the clipboard -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ClipboardString { - /// The text content. - pub text: String, - /// Optional metadata associated with this clipboard string. - pub metadata: Option, -} - -impl ClipboardString { - /// Create a new clipboard string with the given text - pub fn new(text: String) -> Self { - Self { - text, - metadata: None, - } - } - - /// Return a new clipboard item with the metadata replaced by the given metadata, - /// after serializing it as JSON. - pub fn with_json_metadata(mut self, metadata: T) -> Self { - self.metadata = Some(serde_json::to_string(&metadata).unwrap()); - self - } - - /// Get the text of the clipboard string - pub fn text(&self) -> &String { - &self.text - } - - /// Get the owned text of the clipboard string - pub fn into_text(self) -> String { - self.text - } - - /// Get the metadata of the clipboard string, formatted as JSON - pub fn metadata_json(&self) -> Option - where - T: for<'a> Deserialize<'a>, - { - self.metadata - .as_ref() - .and_then(|m| serde_json::from_str(m).ok()) - } - - #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] - /// Compute a hash of the given text for clipboard change detection. - pub fn text_hash(text: &str) -> u64 { - let mut hasher = SeaHasher::new(); - text.hash(&mut hasher); - hasher.finish() - } -} - -impl From for ClipboardString { - fn from(value: String) -> Self { - Self { - text: value, - metadata: None, - } - } -} - -#[cfg(test)] -mod image_tests { - use super::*; - use std::sync::Arc; - - #[test] - fn test_image_to_image_data_applies_exif_orientation() { - let image = Image::from_bytes( - ImageFormat::Jpeg, - include_bytes!("../examples/image/exif-orientation-rotate-180.jpg").to_vec(), - ); - - let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap(); - - assert_eq!(render_image.size(0), size(16.into(), 32.into())); - - let bytes = render_image.as_bytes(0).unwrap(); - assert_eq!(&bytes[..4], &[255, 255, 255, 255]); - assert_eq!(&bytes[(16 * 32 - 1) * 4..], &[0, 0, 0, 255]); - } - - #[test] - fn test_svg_image_to_image_data_converts_to_bgra() { - let image = Image::from_bytes( - ImageFormat::Svg, - br##" - -"## - .to_vec(), - ); - - let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap(); - let bytes = render_image.as_bytes(0).unwrap(); - - for pixel in bytes.chunks_exact(4) { - assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]); - } - } -} - -#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))] -mod tests { - use super::*; - use std::collections::HashSet; - - #[test] - fn test_window_button_layout_parse_standard() { - let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap(); - assert_eq!( - layout.left, - [ - Some(WindowButton::Close), - Some(WindowButton::Minimize), - None - ] - ); - assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]); - } - - #[test] - fn test_window_button_layout_parse_right_only() { - let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap(); - assert_eq!(layout.left, [None, None, None]); - assert_eq!( - layout.right, - [ - Some(WindowButton::Minimize), - Some(WindowButton::Maximize), - Some(WindowButton::Close) - ] - ); - } - - #[test] - fn test_window_button_layout_parse_left_only() { - let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap(); - assert_eq!( - layout.left, - [ - Some(WindowButton::Close), - Some(WindowButton::Minimize), - Some(WindowButton::Maximize) - ] - ); - assert_eq!(layout.right, [None, None, None]); - } - - #[test] - fn test_window_button_layout_parse_with_whitespace() { - let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap(); - assert_eq!( - layout.left, - [ - Some(WindowButton::Close), - Some(WindowButton::Minimize), - None - ] - ); - assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]); - } - - #[test] - fn test_window_button_layout_parse_empty() { - let layout = WindowButtonLayout::parse("").unwrap(); - assert_eq!(layout.left, [None, None, None]); - assert_eq!(layout.right, [None, None, None]); - } - - #[test] - fn test_window_button_layout_parse_intentionally_empty() { - let layout = WindowButtonLayout::parse(":").unwrap(); - assert_eq!(layout.left, [None, None, None]); - assert_eq!(layout.right, [None, None, None]); - } - - #[test] - fn test_window_button_layout_parse_invalid_buttons() { - let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap(); - assert_eq!( - layout.left, - [ - Some(WindowButton::Close), - Some(WindowButton::Minimize), - None - ] - ); - assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]); - } - - #[test] - fn test_window_button_layout_parse_deduplicates_same_side_buttons() { - let layout = WindowButtonLayout::parse("close,close,minimize").unwrap(); - assert_eq!( - layout.right, - [ - Some(WindowButton::Close), - Some(WindowButton::Minimize), - None - ] - ); - assert_eq!(layout.format(), ":close,minimize"); - } - - #[test] - fn test_window_button_layout_parse_deduplicates_buttons_across_sides() { - let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap(); - assert_eq!(layout.left, [Some(WindowButton::Close), None, None]); - assert_eq!( - layout.right, - [ - Some(WindowButton::Maximize), - Some(WindowButton::Minimize), - None - ] - ); - - let button_ids: Vec<_> = layout - .left - .iter() - .chain(layout.right.iter()) - .flatten() - .map(WindowButton::id) - .collect(); - let unique_button_ids = button_ids.iter().copied().collect::>(); - assert_eq!(unique_button_ids.len(), button_ids.len()); - assert_eq!(layout.format(), "close:maximize,minimize"); - } - - #[test] - fn test_window_button_layout_parse_gnome_style() { - let layout = WindowButtonLayout::parse("close").unwrap(); - assert_eq!(layout.left, [None, None, None]); - assert_eq!(layout.right, [Some(WindowButton::Close), None, None]); - } - - #[test] - fn test_window_button_layout_parse_elementary_style() { - let layout = WindowButtonLayout::parse("close:maximize").unwrap(); - assert_eq!(layout.left, [Some(WindowButton::Close), None, None]); - assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]); - } - - #[test] - fn test_window_button_layout_round_trip() { - let cases = [ - "close:minimize,maximize", - "minimize,maximize,close:", - ":close", - "close:", - "close:maximize", - ":", - ]; - - for case in cases { - let layout = WindowButtonLayout::parse(case).unwrap(); - assert_eq!(layout.format(), case, "Round-trip failed for: {}", case); - } - } - - #[test] - fn test_window_button_layout_linux_default() { - let layout = WindowButtonLayout::linux_default(); - assert_eq!(layout.left, [None, None, None]); - assert_eq!( - layout.right, - [ - Some(WindowButton::Minimize), - Some(WindowButton::Maximize), - Some(WindowButton::Close) - ] - ); - - let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap(); - assert_eq!(round_tripped, layout); - } - - #[test] - fn test_window_button_layout_parse_all_invalid() { - assert!(WindowButtonLayout::parse("asdfghjkl").is_err()); - } -} diff --git a/crates/gpui_pre_apple/vendor/gpui/src/scene.rs b/crates/gpui_pre_apple/vendor/gpui/src/scene.rs deleted file mode 100644 index 46c1acf..0000000 --- a/crates/gpui_pre_apple/vendor/gpui/src/scene.rs +++ /dev/null @@ -1,1022 +0,0 @@ -// todo("windows"): remove -#![cfg_attr(windows, allow(dead_code))] - -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -use crate::{ - bounds_tree::BoundsTree, point, AtlasTextureId, AtlasTile, Background, Bounds, ContentMask, - Corners, Edges, Hsla, Pixels, Point, Radians, ScaledPixels, Size, -}; -use std::{ - fmt::Debug, - iter::Peekable, - ops::{Add, Range, Sub}, - slice, -}; - -#[allow(non_camel_case_types, unused)] -#[expect(missing_docs)] -pub type PathVertex_ScaledPixels = PathVertex; - -#[expect(missing_docs)] -pub type DrawOrder = u32; - -/// A boolean stored as a `u32` so that GPU-facing structs contain no -/// compiler-inserted padding bytes, which would be undefined behavior to -/// reinterpret as `&[u8]` when writing instance buffers. Guaranteed to be -/// `0` or `1` by construction; shaders read it as a `u32`/`uint`. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -#[repr(transparent)] -pub struct PaddedBool32(u32); - -impl From for PaddedBool32 { - fn from(value: bool) -> Self { - PaddedBool32(value as u32) - } -} - -#[derive(Default)] -#[expect(missing_docs)] -pub struct Scene { - /// Immutable rounded clip nodes referenced by primitive masks. - pub rounded_clips: Vec>, - clip_indices: std::collections::HashMap<[u32; 13], u32>, - pub(crate) paint_operations: Vec, - primitive_bounds: BoundsTree, - layer_stack: Vec, - pub shadows: Vec, - pub quads: Vec, - pub paths: Vec>, - pub underlines: Vec, - pub monochrome_sprites: Vec, - pub subpixel_sprites: Vec, - pub polychrome_sprites: Vec, - pub surfaces: Vec, -} - -#[expect(missing_docs)] -impl Scene { - pub fn clear(&mut self) { - self.rounded_clips.clear(); - self.clip_indices.clear(); - self.paint_operations.clear(); - self.primitive_bounds.clear(); - self.layer_stack.clear(); - self.paths.clear(); - self.shadows.clear(); - self.quads.clear(); - self.underlines.clear(); - self.monochrome_sprites.clear(); - self.subpixel_sprites.clear(); - self.polychrome_sprites.clear(); - self.surfaces.clear(); - } - - pub fn len(&self) -> usize { - self.paint_operations.len() - } - - /// Intern a clip node for this frame. Identical ancestor chains share nodes. - pub fn insert_clip(&mut self, clip: crate::RoundedClip) -> u32 { - assert!( - clip.parent as usize <= self.rounded_clips.len(), - "clip parent must already exist in this scene" - ); - let key = [ - clip.bounds.origin.x.0.to_bits(), - clip.bounds.origin.y.0.to_bits(), - clip.bounds.size.width.0.to_bits(), - clip.bounds.size.height.0.to_bits(), - clip.radii_x.top_left.0.to_bits(), - clip.radii_x.top_right.0.to_bits(), - clip.radii_x.bottom_right.0.to_bits(), - clip.radii_x.bottom_left.0.to_bits(), - clip.radii_y.top_left.0.to_bits(), - clip.radii_y.top_right.0.to_bits(), - clip.radii_y.bottom_right.0.to_bits(), - clip.radii_y.bottom_left.0.to_bits(), - clip.parent, - ]; - if let Some(index) = self.clip_indices.get(&key) { - return *index; - } - let index = u32::try_from(self.rounded_clips.len()).expect("scene clip index overflow") + 1; - self.rounded_clips.push(clip); - self.clip_indices.insert(key, index); - index - } - - fn import_clip(&mut self, mut index: u32, previous: &Scene) -> u32 { - let mut chain = Vec::new(); - while index != 0 { - let clip = previous.rounded_clips[index as usize - 1]; - index = clip.parent; - chain.push(clip); - } - let mut parent = 0; - for mut clip in chain.into_iter().rev() { - clip.parent = parent; - parent = self.insert_clip(clip); - } - parent - } - - pub fn push_layer(&mut self, bounds: Bounds) { - let order = self.primitive_bounds.insert(bounds); - self.layer_stack.push(order); - self.paint_operations - .push(PaintOperation::StartLayer(bounds)); - } - - pub fn pop_layer(&mut self) { - self.layer_stack.pop(); - self.paint_operations.push(PaintOperation::EndLayer); - } - - pub fn insert_primitive(&mut self, primitive: impl Into) { - let mut primitive = primitive.into(); - let clipped_bounds = primitive - .bounds() - .intersect(&primitive.content_mask().bounds); - - if clipped_bounds.is_empty() { - return; - } - - let order = self - .layer_stack - .last() - .copied() - .unwrap_or_else(|| self.primitive_bounds.insert(clipped_bounds)); - match &mut primitive { - Primitive::Shadow(shadow) => { - shadow.order = order; - self.shadows.push(*shadow); - } - Primitive::Quad(quad) => { - quad.order = order; - self.quads.push(*quad); - } - Primitive::Path(path) => { - path.order = order; - path.id = PathId(self.paths.len()); - self.paths.push(path.clone()); - } - Primitive::Underline(underline) => { - underline.order = order; - self.underlines.push(*underline); - } - Primitive::MonochromeSprite(sprite) => { - sprite.order = order; - self.monochrome_sprites.push(*sprite); - } - Primitive::SubpixelSprite(sprite) => { - sprite.order = order; - self.subpixel_sprites.push(*sprite); - } - Primitive::PolychromeSprite(sprite) => { - sprite.order = order; - self.polychrome_sprites.push(*sprite); - } - Primitive::Surface(surface) => { - surface.order = order; - self.surfaces.push(surface.clone()); - } - } - self.paint_operations - .push(PaintOperation::Primitive(primitive)); - } - - pub fn replay(&mut self, range: Range, prev_scene: &Scene) { - let mut clip_remapping = std::collections::HashMap::new(); - for operation in &prev_scene.paint_operations[range] { - match operation { - PaintOperation::Primitive(primitive) => { - let mut primitive = primitive.clone(); - let mask = primitive.content_mask_mut(); - if mask.clip_index != 0 { - mask.clip_index = *clip_remapping - .entry(mask.clip_index) - .or_insert_with(|| self.import_clip(mask.clip_index, prev_scene)); - } - self.insert_primitive(primitive); - } - PaintOperation::StartLayer(bounds) => self.push_layer(*bounds), - PaintOperation::EndLayer => self.pop_layer(), - } - } - } - - pub fn finish(&mut self) { - self.shadows.sort_by_key(|shadow| shadow.order); - self.quads.sort_by_key(|quad| quad.order); - self.paths.sort_by_key(|path| path.order); - self.underlines.sort_by_key(|underline| underline.order); - self.monochrome_sprites - .sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id)); - self.subpixel_sprites - .sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id)); - self.polychrome_sprites - .sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id)); - self.surfaces.sort_by_key(|surface| surface.order); - } - - #[cfg_attr( - all( - any(target_os = "linux", target_os = "freebsd"), - not(any(feature = "x11", feature = "wayland")) - ), - allow(dead_code) - )] - pub fn batches(&self) -> impl Iterator + '_ { - BatchIterator { - shadows_start: 0, - shadows_iter: self.shadows.iter().peekable(), - quads_start: 0, - quads_iter: self.quads.iter().peekable(), - paths_start: 0, - paths_iter: self.paths.iter().peekable(), - underlines_start: 0, - underlines_iter: self.underlines.iter().peekable(), - monochrome_sprites_start: 0, - monochrome_sprites_iter: self.monochrome_sprites.iter().peekable(), - subpixel_sprites_start: 0, - subpixel_sprites_iter: self.subpixel_sprites.iter().peekable(), - polychrome_sprites_start: 0, - polychrome_sprites_iter: self.polychrome_sprites.iter().peekable(), - surfaces_start: 0, - surfaces_iter: self.surfaces.iter().peekable(), - } - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Default)] -#[cfg_attr( - all( - any(target_os = "linux", target_os = "freebsd"), - not(any(feature = "x11", feature = "wayland")) - ), - allow(dead_code) -)] -pub(crate) enum PrimitiveKind { - Shadow, - #[default] - Quad, - Path, - Underline, - MonochromeSprite, - SubpixelSprite, - PolychromeSprite, - Surface, -} - -pub(crate) enum PaintOperation { - Primitive(Primitive), - StartLayer(Bounds), - EndLayer, -} - -#[derive(Clone)] -#[expect(missing_docs)] -pub enum Primitive { - Shadow(Shadow), - Quad(Quad), - Path(Path), - Underline(Underline), - MonochromeSprite(MonochromeSprite), - SubpixelSprite(SubpixelSprite), - PolychromeSprite(PolychromeSprite), - Surface(PaintSurface), -} - -#[expect(missing_docs)] -impl Primitive { - pub fn bounds(&self) -> &Bounds { - match self { - Primitive::Shadow(shadow) => &shadow.bounds, - Primitive::Quad(quad) => &quad.bounds, - Primitive::Path(path) => &path.bounds, - Primitive::Underline(underline) => &underline.bounds, - Primitive::MonochromeSprite(sprite) => &sprite.bounds, - Primitive::SubpixelSprite(sprite) => &sprite.bounds, - Primitive::PolychromeSprite(sprite) => &sprite.bounds, - Primitive::Surface(surface) => &surface.bounds, - } - } - - pub fn content_mask(&self) -> &ContentMask { - match self { - Primitive::Shadow(shadow) => &shadow.content_mask, - Primitive::Quad(quad) => &quad.content_mask, - Primitive::Path(path) => &path.content_mask, - Primitive::Underline(underline) => &underline.content_mask, - Primitive::MonochromeSprite(sprite) => &sprite.content_mask, - Primitive::SubpixelSprite(sprite) => &sprite.content_mask, - Primitive::PolychromeSprite(sprite) => &sprite.content_mask, - Primitive::Surface(surface) => &surface.content_mask, - } - } - - pub fn content_mask_mut(&mut self) -> &mut ContentMask { - match self { - Primitive::Shadow(shadow) => &mut shadow.content_mask, - Primitive::Quad(quad) => &mut quad.content_mask, - Primitive::Path(path) => &mut path.content_mask, - Primitive::Underline(underline) => &mut underline.content_mask, - Primitive::MonochromeSprite(sprite) => &mut sprite.content_mask, - Primitive::SubpixelSprite(sprite) => &mut sprite.content_mask, - Primitive::PolychromeSprite(sprite) => &mut sprite.content_mask, - Primitive::Surface(surface) => &mut surface.content_mask, - } - } -} - -#[cfg_attr( - all( - any(target_os = "linux", target_os = "freebsd"), - not(any(feature = "x11", feature = "wayland")) - ), - allow(dead_code) -)] -struct BatchIterator<'a> { - shadows_start: usize, - shadows_iter: Peekable>, - quads_start: usize, - quads_iter: Peekable>, - paths_start: usize, - paths_iter: Peekable>>, - underlines_start: usize, - underlines_iter: Peekable>, - monochrome_sprites_start: usize, - monochrome_sprites_iter: Peekable>, - subpixel_sprites_start: usize, - subpixel_sprites_iter: Peekable>, - polychrome_sprites_start: usize, - polychrome_sprites_iter: Peekable>, - surfaces_start: usize, - surfaces_iter: Peekable>, -} - -impl<'a> Iterator for BatchIterator<'a> { - type Item = PrimitiveBatch; - - fn next(&mut self) -> Option { - let mut orders_and_kinds = [ - ( - self.shadows_iter.peek().map(|s| s.order), - PrimitiveKind::Shadow, - ), - (self.quads_iter.peek().map(|q| q.order), PrimitiveKind::Quad), - (self.paths_iter.peek().map(|q| q.order), PrimitiveKind::Path), - ( - self.underlines_iter.peek().map(|u| u.order), - PrimitiveKind::Underline, - ), - ( - self.monochrome_sprites_iter.peek().map(|s| s.order), - PrimitiveKind::MonochromeSprite, - ), - ( - self.subpixel_sprites_iter.peek().map(|s| s.order), - PrimitiveKind::SubpixelSprite, - ), - ( - self.polychrome_sprites_iter.peek().map(|s| s.order), - PrimitiveKind::PolychromeSprite, - ), - ( - self.surfaces_iter.peek().map(|s| s.order), - PrimitiveKind::Surface, - ), - ]; - orders_and_kinds.sort_by_key(|(order, kind)| (order.unwrap_or(u32::MAX), *kind)); - - let first = orders_and_kinds[0]; - let second = orders_and_kinds[1]; - let (batch_kind, max_order_and_kind) = if first.0.is_some() { - (first.1, (second.0.unwrap_or(u32::MAX), second.1)) - } else { - return None; - }; - - match batch_kind { - PrimitiveKind::Shadow => { - let shadows_start = self.shadows_start; - let mut shadows_end = shadows_start + 1; - self.shadows_iter.next(); - while self - .shadows_iter - .next_if(|shadow| (shadow.order, batch_kind) < max_order_and_kind) - .is_some() - { - shadows_end += 1; - } - self.shadows_start = shadows_end; - Some(PrimitiveBatch::Shadows(shadows_start..shadows_end)) - } - PrimitiveKind::Quad => { - let quads_start = self.quads_start; - let mut quads_end = quads_start + 1; - self.quads_iter.next(); - while self - .quads_iter - .next_if(|quad| (quad.order, batch_kind) < max_order_and_kind) - .is_some() - { - quads_end += 1; - } - self.quads_start = quads_end; - Some(PrimitiveBatch::Quads(quads_start..quads_end)) - } - PrimitiveKind::Path => { - let paths_start = self.paths_start; - let mut paths_end = paths_start + 1; - self.paths_iter.next(); - while self - .paths_iter - .next_if(|path| (path.order, batch_kind) < max_order_and_kind) - .is_some() - { - paths_end += 1; - } - self.paths_start = paths_end; - Some(PrimitiveBatch::Paths(paths_start..paths_end)) - } - PrimitiveKind::Underline => { - let underlines_start = self.underlines_start; - let mut underlines_end = underlines_start + 1; - self.underlines_iter.next(); - while self - .underlines_iter - .next_if(|underline| (underline.order, batch_kind) < max_order_and_kind) - .is_some() - { - underlines_end += 1; - } - self.underlines_start = underlines_end; - Some(PrimitiveBatch::Underlines(underlines_start..underlines_end)) - } - PrimitiveKind::MonochromeSprite => { - let texture_id = self.monochrome_sprites_iter.peek().unwrap().tile.texture_id; - let sprites_start = self.monochrome_sprites_start; - let mut sprites_end = sprites_start + 1; - self.monochrome_sprites_iter.next(); - while self - .monochrome_sprites_iter - .next_if(|sprite| { - (sprite.order, batch_kind) < max_order_and_kind - && sprite.tile.texture_id == texture_id - }) - .is_some() - { - sprites_end += 1; - } - self.monochrome_sprites_start = sprites_end; - Some(PrimitiveBatch::MonochromeSprites { - texture_id, - range: sprites_start..sprites_end, - }) - } - PrimitiveKind::SubpixelSprite => { - let texture_id = self.subpixel_sprites_iter.peek().unwrap().tile.texture_id; - let sprites_start = self.subpixel_sprites_start; - let mut sprites_end = sprites_start + 1; - self.subpixel_sprites_iter.next(); - while self - .subpixel_sprites_iter - .next_if(|sprite| { - (sprite.order, batch_kind) < max_order_and_kind - && sprite.tile.texture_id == texture_id - }) - .is_some() - { - sprites_end += 1; - } - self.subpixel_sprites_start = sprites_end; - Some(PrimitiveBatch::SubpixelSprites { - texture_id, - range: sprites_start..sprites_end, - }) - } - PrimitiveKind::PolychromeSprite => { - let texture_id = self.polychrome_sprites_iter.peek().unwrap().tile.texture_id; - let sprites_start = self.polychrome_sprites_start; - let mut sprites_end = sprites_start + 1; - self.polychrome_sprites_iter.next(); - while self - .polychrome_sprites_iter - .next_if(|sprite| { - (sprite.order, batch_kind) < max_order_and_kind - && sprite.tile.texture_id == texture_id - }) - .is_some() - { - sprites_end += 1; - } - self.polychrome_sprites_start = sprites_end; - Some(PrimitiveBatch::PolychromeSprites { - texture_id, - range: sprites_start..sprites_end, - }) - } - PrimitiveKind::Surface => { - let surfaces_start = self.surfaces_start; - let mut surfaces_end = surfaces_start + 1; - self.surfaces_iter.next(); - while self - .surfaces_iter - .next_if(|surface| (surface.order, batch_kind) < max_order_and_kind) - .is_some() - { - surfaces_end += 1; - } - self.surfaces_start = surfaces_end; - Some(PrimitiveBatch::Surfaces(surfaces_start..surfaces_end)) - } - } - } -} - -#[derive(Debug)] -#[cfg_attr( - all( - any(target_os = "linux", target_os = "freebsd"), - not(any(feature = "x11", feature = "wayland")) - ), - allow(dead_code) -)] -#[allow(missing_docs)] -pub enum PrimitiveBatch { - Shadows(Range), - Quads(Range), - Paths(Range), - Underlines(Range), - MonochromeSprites { - texture_id: AtlasTextureId, - range: Range, - }, - #[cfg_attr(target_os = "macos", allow(dead_code))] - SubpixelSprites { - texture_id: AtlasTextureId, - range: Range, - }, - PolychromeSprites { - texture_id: AtlasTextureId, - range: Range, - }, - Surfaces(Range), -} - -impl PrimitiveBatch { - #[expect(missing_docs)] - pub fn label(&self) -> String { - match self { - Self::Shadows(range) => format!("shadows ({})", range.len()), - Self::Quads(range) => format!("quads ({})", range.len()), - Self::Paths(range) => format!("paths ({})", range.len()), - Self::Underlines(range) => format!("underlines ({})", range.len()), - Self::MonochromeSprites { texture_id, range } => { - format!( - "monochrome sprites ({}) on atlas {}", - range.len(), - texture_id.index - ) - } - Self::SubpixelSprites { texture_id, range } => { - format!( - "subpixel sprites ({}) on atlas {}", - range.len(), - texture_id.index - ) - } - Self::PolychromeSprites { texture_id, range } => { - format!( - "polychrome sprites ({}) on atlas {}", - range.len(), - texture_id.index - ) - } - Self::Surfaces(range) => format!("surfaces ({})", range.len()), - } - } -} - -#[derive(Default, Debug, Copy, Clone)] -#[repr(C)] -#[expect(missing_docs)] -pub struct Quad { - pub order: DrawOrder, - pub border_style: BorderStyle, - pub bounds: Bounds, - pub content_mask: ContentMask, - pub background: Background, - pub border_color: Hsla, - pub corner_radii: Corners, - pub border_widths: Edges, -} - -impl From for Primitive { - fn from(quad: Quad) -> Self { - Primitive::Quad(quad) - } -} - -#[derive(Debug, Copy, Clone)] -#[repr(C)] -#[expect(missing_docs)] -pub struct Underline { - pub order: DrawOrder, - pub pad: u32, // align to 8 bytes - pub bounds: Bounds, - pub content_mask: ContentMask, - pub color: Hsla, - pub thickness: ScaledPixels, - pub wavy: PaddedBool32, -} - -impl From for Primitive { - fn from(underline: Underline) -> Self { - Primitive::Underline(underline) - } -} - -#[derive(Debug, Copy, Clone)] -#[repr(C)] -#[expect(missing_docs)] -pub struct Shadow { - pub order: DrawOrder, - pub blur_radius: ScaledPixels, - pub bounds: Bounds, - pub corner_radii: Corners, - pub content_mask: ContentMask, - pub color: Hsla, - pub element_bounds: Bounds, - pub element_corner_radii: Corners, - /// 0 = drop shadow (rendered outside the element), 1 = inset shadow (rendered inside). - pub inset: u32, - pub pad: u32, // align to 8 bytes -} - -impl From for Primitive { - fn from(shadow: Shadow) -> Self { - Primitive::Shadow(shadow) - } -} - -/// The style of a border. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] -#[repr(C)] -pub enum BorderStyle { - /// A solid border. - #[default] - Solid = 0, - /// A dashed border. - Dashed = 1, -} - -/// A data type representing a 2 dimensional transformation that can be applied to an element. -#[derive(Debug, Clone, Copy, PartialEq)] -#[repr(C)] -pub struct TransformationMatrix { - /// 2x2 matrix containing rotation and scale, - /// stored row-major - pub rotation_scale: [[f32; 2]; 2], - /// translation vector - pub translation: [f32; 2], -} - -impl Eq for TransformationMatrix {} - -impl TransformationMatrix { - /// The unit matrix, has no effect. - pub fn unit() -> Self { - Self { - rotation_scale: [[1.0, 0.0], [0.0, 1.0]], - translation: [0.0, 0.0], - } - } - - /// Move the origin by a given point - pub fn translate(mut self, point: Point) -> Self { - self.compose(Self { - rotation_scale: [[1.0, 0.0], [0.0, 1.0]], - translation: [point.x.0, point.y.0], - }) - } - - /// Clockwise rotation in radians around the origin - pub fn rotate(self, angle: Radians) -> Self { - self.compose(Self { - rotation_scale: [ - [angle.0.cos(), -angle.0.sin()], - [angle.0.sin(), angle.0.cos()], - ], - translation: [0.0, 0.0], - }) - } - - /// Scale around the origin - pub fn scale(self, size: Size) -> Self { - self.compose(Self { - rotation_scale: [[size.width, 0.0], [0.0, size.height]], - translation: [0.0, 0.0], - }) - } - - /// Perform matrix multiplication with another transformation - /// to produce a new transformation that is the result of - /// applying both transformations: first, `other`, then `self`. - #[inline] - pub fn compose(self, other: TransformationMatrix) -> TransformationMatrix { - if other == Self::unit() { - return self; - } - // Perform matrix multiplication - TransformationMatrix { - rotation_scale: [ - [ - self.rotation_scale[0][0] * other.rotation_scale[0][0] - + self.rotation_scale[0][1] * other.rotation_scale[1][0], - self.rotation_scale[0][0] * other.rotation_scale[0][1] - + self.rotation_scale[0][1] * other.rotation_scale[1][1], - ], - [ - self.rotation_scale[1][0] * other.rotation_scale[0][0] - + self.rotation_scale[1][1] * other.rotation_scale[1][0], - self.rotation_scale[1][0] * other.rotation_scale[0][1] - + self.rotation_scale[1][1] * other.rotation_scale[1][1], - ], - ], - translation: [ - self.translation[0] - + self.rotation_scale[0][0] * other.translation[0] - + self.rotation_scale[0][1] * other.translation[1], - self.translation[1] - + self.rotation_scale[1][0] * other.translation[0] - + self.rotation_scale[1][1] * other.translation[1], - ], - } - } - - /// Apply transformation to a point, mainly useful for debugging - pub fn apply(&self, point: Point) -> Point { - let input = [point.x.0, point.y.0]; - let mut output = self.translation; - for (i, output_cell) in output.iter_mut().enumerate() { - for (k, input_cell) in input.iter().enumerate() { - *output_cell += self.rotation_scale[i][k] * *input_cell; - } - } - Point::new(output[0].into(), output[1].into()) - } -} - -impl Default for TransformationMatrix { - fn default() -> Self { - Self::unit() - } -} - -#[derive(Copy, Clone, Debug)] -#[repr(C)] -#[expect(missing_docs)] -pub struct MonochromeSprite { - pub order: DrawOrder, - pub pad: u32, - pub bounds: Bounds, - pub content_mask: ContentMask, - pub color: Hsla, - pub tile: AtlasTile, - pub transformation: TransformationMatrix, -} - -impl From for Primitive { - fn from(sprite: MonochromeSprite) -> Self { - Primitive::MonochromeSprite(sprite) - } -} - -#[derive(Copy, Clone, Debug)] -#[repr(C)] -#[expect(missing_docs)] -pub struct SubpixelSprite { - pub order: DrawOrder, - pub pad: u32, // align to 8 bytes - pub bounds: Bounds, - pub content_mask: ContentMask, - pub color: Hsla, - pub tile: AtlasTile, - pub transformation: TransformationMatrix, -} - -impl From for Primitive { - fn from(sprite: SubpixelSprite) -> Self { - Primitive::SubpixelSprite(sprite) - } -} - -#[derive(Copy, Clone, Debug)] -#[repr(C)] -#[expect(missing_docs)] -pub struct PolychromeSprite { - pub order: DrawOrder, - pub pad: u32, - pub grayscale: PaddedBool32, - pub opacity: f32, - pub bounds: Bounds, - pub content_mask: ContentMask, - pub corner_radii: Corners, - pub tile: AtlasTile, -} - -impl From for Primitive { - fn from(sprite: PolychromeSprite) -> Self { - Primitive::PolychromeSprite(sprite) - } -} - -#[derive(Clone, Debug)] -#[allow(missing_docs)] -pub struct PaintSurface { - pub order: DrawOrder, - pub bounds: Bounds, - pub content_mask: ContentMask, - #[cfg(target_os = "macos")] - pub image_buffer: core_video::pixel_buffer::CVPixelBuffer, -} - -impl From for Primitive { - fn from(surface: PaintSurface) -> Self { - Primitive::Surface(surface) - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -#[expect(missing_docs)] -pub struct PathId(pub usize); - -/// A line made up of a series of vertices and control points. -#[derive(Clone, Debug)] -#[expect(missing_docs)] -pub struct Path { - pub id: PathId, - pub order: DrawOrder, - pub bounds: Bounds

, - pub content_mask: ContentMask

, - pub vertices: Vec>, - pub color: Background, - start: Point

, - current: Point

, - contour_count: usize, -} - -impl Path { - /// Create a new path with the given starting point. - pub fn new(start: Point) -> Self { - Self { - id: PathId(0), - order: DrawOrder::default(), - vertices: Vec::new(), - start, - current: start, - bounds: Bounds { - origin: start, - size: Default::default(), - }, - content_mask: Default::default(), - color: Default::default(), - contour_count: 0, - } - } - - /// Scale this path by the given factor. - pub fn scale(&self, factor: f32) -> Path { - Path { - id: self.id, - order: self.order, - bounds: self.bounds.scale(factor), - content_mask: self.content_mask.scale(factor), - vertices: self - .vertices - .iter() - .map(|vertex| vertex.scale(factor)) - .collect(), - start: self.start.map(|start| start.scale(factor)), - current: self.current.scale(factor), - contour_count: self.contour_count, - color: self.color, - } - } - - /// Move the start, current point to the given point. - pub fn move_to(&mut self, to: Point) { - self.contour_count += 1; - self.start = to; - self.current = to; - } - - /// Draw a straight line from the current point to the given point. - pub fn line_to(&mut self, to: Point) { - self.contour_count += 1; - if self.contour_count > 1 { - self.push_triangle( - (self.start, self.current, to), - (point(0., 1.), point(0., 1.), point(0., 1.)), - ); - } - self.current = to; - } - - /// Draw a curve from the current point to the given point, using the given control point. - pub fn curve_to(&mut self, to: Point, ctrl: Point) { - self.contour_count += 1; - if self.contour_count > 1 { - self.push_triangle( - (self.start, self.current, to), - (point(0., 1.), point(0., 1.), point(0., 1.)), - ); - } - - self.push_triangle( - (self.current, ctrl, to), - (point(0., 0.), point(0.5, 0.), point(1., 1.)), - ); - self.current = to; - } - - /// Push a triangle to the Path. - pub fn push_triangle( - &mut self, - xy: (Point, Point, Point), - st: (Point, Point, Point), - ) { - self.bounds = self - .bounds - .union(&Bounds { - origin: xy.0, - size: Default::default(), - }) - .union(&Bounds { - origin: xy.1, - size: Default::default(), - }) - .union(&Bounds { - origin: xy.2, - size: Default::default(), - }); - - self.vertices.push(PathVertex { - xy_position: xy.0, - st_position: st.0, - content_mask: Default::default(), - }); - self.vertices.push(PathVertex { - xy_position: xy.1, - st_position: st.1, - content_mask: Default::default(), - }); - self.vertices.push(PathVertex { - xy_position: xy.2, - st_position: st.2, - content_mask: Default::default(), - }); - } -} - -impl Path -where - T: Clone + Debug + Default + PartialEq + PartialOrd + Add + Sub, -{ - #[allow(unused)] - #[expect(missing_docs)] - pub fn clipped_bounds(&self) -> Bounds { - self.bounds.intersect(&self.content_mask.bounds) - } -} - -impl From> for Primitive { - fn from(path: Path) -> Self { - Primitive::Path(path) - } -} - -#[derive(Clone, Debug)] -#[repr(C)] -#[expect(missing_docs)] -pub struct PathVertex { - pub xy_position: Point

, - pub st_position: Point, - pub content_mask: ContentMask

, -} - -#[expect(missing_docs)] -impl PathVertex { - pub fn scale(&self, factor: f32) -> PathVertex { - PathVertex { - xy_position: self.xy_position.scale(factor), - st_position: self.st_position, - content_mask: self.content_mask.scale(factor), - } - } -} diff --git a/crates/gpui_pre_apple/vendor/gpui/src/window.rs b/crates/gpui_pre_apple/vendor/gpui/src/window.rs deleted file mode 100644 index 86ef651..0000000 --- a/crates/gpui_pre_apple/vendor/gpui/src/window.rs +++ /dev/null @@ -1,8373 +0,0 @@ -#[cfg(feature = "profiler")] -use crate::profiler; -#[cfg(feature = "profiler")] -use crate::DebugFrameOverlayMode; -#[cfg(any(feature = "inspector", debug_assertions))] -use crate::Inspector; -use crate::{ - point, prelude::*, px, rems, size, transparent_black, Action, AnyDrag, AnyElement, - AnyImageCache, AnyTooltip, AnyView, App, AppContext, Arena, Asset, AsyncWindowContext, - AtlasTile, AvailableSpace, Background, BorderStyle, Bounds, BoxShadow, Capslock, Context, - Corners, CursorHideMode, CursorStyle, Decorations, DevicePixels, DispatchActionListener, - DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, - FileDropEvent, FontId, Global, GlobalElementId, GlyphId, GpuSpecs, Hsla, InputHandler, IsZero, - KeyBinding, KeyContext, KeyDownEvent, KeyEvent, Keystroke, KeystrokeEvent, LayoutId, - LineLayoutIndex, Modifiers, ModifiersChangedEvent, MonochromeSprite, MouseButton, MouseEvent, - MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, - PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, Priority, PromptButton, - PromptLevel, Quad, Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, - Replay, ResizeEdge, ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style, - SubpixelSprite, SubscriberSet, Subscription, SystemWindowTab, SystemWindowTabController, - TabStopMap, TaffyLayoutEngine, Task, TextInputConfiguration, TextInputStateChange, - TextRenderingMode, TextStyle, TextStyleRefinement, ThermalState, TransformationMatrix, - Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance, WindowBounds, - WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, - SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y, -}; - -use crate::gestures::{GestureTuning, RecognizedTouchGesture, TouchGestureRecognizer}; -use crate::interactive::TouchEvent; -use anyhow::{anyhow, Context as _, Result}; -use collections::{FxHashMap, FxHashSet}; -#[cfg(target_os = "macos")] -use core_video::pixel_buffer::CVPixelBuffer; -use derive_more::{Deref, DerefMut}; -use futures::channel::oneshot; -use futures::FutureExt; -use gpui_util::post_inc; -use gpui_util::{measure, ResultExt}; -use itertools::FoldWhile::{Continue, Done}; -use itertools::Itertools; -use parking_lot::RwLock; -use raw_window_handle::{HandleError, HasDisplayHandle, HasWindowHandle}; -use refineable::Refineable; -use scheduler::Instant; -use slotmap::SlotMap; -use smallvec::SmallVec; -use std::{ - any::{Any, TypeId}, - borrow::Cow, - cell::{Cell, RefCell}, - cmp, - fmt::{Debug, Display}, - hash::{Hash, Hasher}, - marker::PhantomData, - mem, - ops::{DerefMut, Range}, - rc::Rc, - sync::{ - atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, - Arc, Weak, - }, - time::Duration, -}; -use uuid::Uuid; - -pub(crate) mod a11y; -mod prompts; - -pub use a11y::A11ySubtreeBuilder; - -use self::a11y::A11y; -#[cfg(not(target_family = "wasm"))] -use self::a11y::ROOT_NODE_ID; -use crate::util::{ - atomic_incr_if_not_zero, ceil_to_device_pixel, floor_to_device_pixel, round_half_toward_zero, - round_half_toward_zero_f64, round_stroke_to_device_pixel, round_to_device_pixel, -}; -pub use prompts::*; - -/// Default window size used when no explicit size is provided. -pub const DEFAULT_WINDOW_SIZE: Size = size(px(1536.), px(1095.)); - -/// A 6:5 aspect ratio minimum window size to be used for functional, -/// additional-to-main-Zed windows, like the settings and rules library windows. -pub const DEFAULT_ADDITIONAL_WINDOW_SIZE: Size = Size { - width: Pixels(900.), - height: Pixels(750.), -}; - -/// Represents the two different phases when dispatching events. -#[derive(Default, Copy, Clone, Debug, Eq, PartialEq)] -pub enum DispatchPhase { - /// After the capture phase comes the bubble phase, in which mouse event listeners are - /// invoked front to back and keyboard event listeners are invoked from the focused element - /// to the root of the element tree. This is the phase you'll most commonly want to use when - /// registering event listeners. - #[default] - Bubble, - /// During the initial capture phase, mouse event listeners are invoked back to front, and keyboard - /// listeners are invoked from the root of the tree downward toward the focused element. This phase - /// is used for special purposes such as clearing the "pressed" state for click events. If - /// you stop event propagation during this phase, you need to know what you're doing. Handlers - /// outside of the immediate region may rely on detecting non-local events during this phase. - Capture, -} - -impl DispatchPhase { - /// Returns true if this represents the "bubble" phase. - #[inline] - pub fn bubble(self) -> bool { - self == DispatchPhase::Bubble - } - - /// Returns true if this represents the "capture" phase. - #[inline] - pub fn capture(self) -> bool { - self == DispatchPhase::Capture - } -} - -struct WindowInvalidatorInner { - #[cfg(feature = "profiler")] - pub window_id: WindowId, - pub dirty: bool, - pub draw_phase: DrawPhase, - pub dirty_views: FxHashSet, - pub update_count: usize, - #[cfg(feature = "profiler")] - pub frame_dirty: FrameDirtyAccumulator, - pub platform_waker: Option>, -} - -/// Per-frame invalidation bookkeeping, drained at draw time and emitted to the -/// frame profiler. Tracks when the current frame first became dirty and how -/// many invalidations were coalesced into it, whenever the profiler is -/// compiled in. Retention of the resulting per-frame records is what -/// `profiler::trace_enabled()` controls, not this measurement. -#[cfg(feature = "profiler")] -#[derive(Default)] -struct FrameDirtyAccumulator { - dirty_at: Option, - invalidations: u64, -} - -#[derive(Clone)] -pub(crate) struct WindowInvalidator { - inner: Rc>, -} - -impl WindowInvalidator { - pub fn new(#[allow(unused_variables)] window_id: WindowId) -> Self { - WindowInvalidator { - inner: Rc::new(RefCell::new(WindowInvalidatorInner { - #[cfg(feature = "profiler")] - window_id, - dirty: true, - draw_phase: DrawPhase::None, - dirty_views: FxHashSet::default(), - update_count: 0, - #[cfg(feature = "profiler")] - frame_dirty: FrameDirtyAccumulator::default(), - platform_waker: None, - })), - } - } - - pub fn invalidate_view(&self, entity: EntityId, cx: &mut App) -> bool { - let mut inner = self.inner.borrow_mut(); - inner.update_count += 1; - inner.dirty_views.insert(entity); - if inner.draw_phase == DrawPhase::None { - #[cfg(feature = "profiler")] - let dirty_at = Self::record_frame_dirty(&mut inner); - let became_dirty = !inner.dirty; - inner.dirty = true; - let waker = became_dirty.then(|| inner.platform_waker.clone()).flatten(); - #[cfg(feature = "profiler")] - let window_id = inner.window_id; - drop(inner); - #[cfg(feature = "profiler")] - if became_dirty { - profiler::journal::record_frame_pending(window_id, dirty_at); - } - cx.push_effect(Effect::Notify { emitter: entity }); - if let Some(waker) = waker { - waker(); - } - true - } else { - false - } - } - - pub fn is_dirty(&self) -> bool { - self.inner.borrow().dirty - } - - pub fn set_dirty(&self, dirty: bool) { - let mut inner = self.inner.borrow_mut(); - let became_dirty = dirty && !inner.dirty; - inner.dirty = dirty; - if dirty { - inner.update_count += 1; - } - #[cfg(feature = "profiler")] - let dirty_at = dirty.then(|| Self::record_frame_dirty(&mut inner)); - let waker = became_dirty.then(|| inner.platform_waker.clone()).flatten(); - #[cfg(feature = "profiler")] - let window_id = inner.window_id; - drop(inner); - #[cfg(feature = "profiler")] - if became_dirty && let Some(dirty_at) = dirty_at { - profiler::journal::record_frame_pending(window_id, dirty_at); - } - if let Some(waker) = waker { - waker(); - } - } - - pub fn set_platform_waker(&self, waker: Option>) { - let mut inner = self.inner.borrow_mut(); - inner.platform_waker = waker; - let waker = inner.dirty.then(|| inner.platform_waker.clone()).flatten(); - drop(inner); - if let Some(waker) = waker { - waker(); - } - } - - /// Wakes the platform's frame-request source so a frame request is - /// delivered even if the platform stops requesting frames for idle - /// windows. No-op on platforms without a frame waker. - pub fn wake_platform(&self) { - let waker = self.inner.borrow().platform_waker.clone(); - if let Some(waker) = waker { - waker(); - } - } - - pub fn set_phase(&self, phase: DrawPhase) { - self.inner.borrow_mut().draw_phase = phase - } - - pub fn update_count(&self) -> usize { - self.inner.borrow().update_count - } - - #[cfg(feature = "profiler")] - fn record_frame_dirty(inner: &mut WindowInvalidatorInner) -> Instant { - let dirty_at = *inner.frame_dirty.dirty_at.get_or_insert_with(Instant::now); - inner.frame_dirty.invalidations += 1; - dirty_at - } - - #[cfg(feature = "profiler")] - fn take_frame_dirty(&self) -> FrameDirtyAccumulator { - mem::take(&mut self.inner.borrow_mut().frame_dirty) - } - - pub fn take_views(&self) -> FxHashSet { - mem::take(&mut self.inner.borrow_mut().dirty_views) - } - - pub fn replace_views(&self, views: FxHashSet) { - self.inner.borrow_mut().dirty_views = views; - } - - pub fn not_drawing(&self) -> bool { - self.inner.borrow().draw_phase == DrawPhase::None - } - - #[track_caller] - pub fn debug_assert_paint(&self) { - debug_assert!( - matches!(self.inner.borrow().draw_phase, DrawPhase::Paint), - "this method can only be called during paint" - ); - } - - #[track_caller] - pub fn debug_assert_prepaint(&self) { - debug_assert!( - matches!(self.inner.borrow().draw_phase, DrawPhase::Prepaint), - "this method can only be called during request_layout, or prepaint" - ); - } - - #[track_caller] - pub fn debug_assert_paint_or_prepaint(&self) { - debug_assert!( - matches!( - self.inner.borrow().draw_phase, - DrawPhase::Paint | DrawPhase::Prepaint - ), - "this method can only be called during request_layout, prepaint, or paint" - ); - } -} - -type AnyObserver = Box bool + 'static>; - -pub(crate) type AnyWindowFocusListener = - Box bool + 'static>; - -pub(crate) struct WindowFocusEvent { - pub(crate) previous_focus_path: SmallVec<[FocusId; 8]>, - pub(crate) current_focus_path: SmallVec<[FocusId; 8]>, -} - -impl WindowFocusEvent { - pub fn is_focus_in(&self, focus_id: FocusId) -> bool { - !self.previous_focus_path.contains(&focus_id) && self.current_focus_path.contains(&focus_id) - } - - pub fn is_focus_out(&self, focus_id: FocusId) -> bool { - self.previous_focus_path.contains(&focus_id) && !self.current_focus_path.contains(&focus_id) - } -} - -/// This is provided when subscribing for `Context::on_focus_out` events. -pub struct FocusOutEvent { - /// A weak focus handle representing what was blurred. - pub blurred: WeakFocusHandle, -} - -slotmap::new_key_type! { - /// A globally unique identifier for a focusable element. - pub struct FocusId; -} - -thread_local! { - /// Fallback arena used when no app-specific arena is active. - /// In production, each window draw sets CURRENT_ELEMENT_ARENA to the app's arena. - pub(crate) static ELEMENT_ARENA: RefCell = RefCell::new(Arena::new(1024 * 1024)); - - /// Points to the current App's element arena during draw operations. - /// This allows multiple test Apps to have isolated arenas, preventing - /// cross-session corruption when the scheduler interleaves their tasks. - static CURRENT_ELEMENT_ARENA: Cell>> = const { Cell::new(None) }; -} - -/// Whether a window draw is currently in progress on this thread. -/// -/// This holds exactly while an `ElementArenaScope` is active: nested scopes -/// restore the previous (still set) arena pointer, so `CURRENT_ELEMENT_ARENA` -/// is `Some` from the outermost draw's start to its end. -/// -/// The `on_request_frame` callback uses this to defer draw requests that -/// arrive re-entrantly while a draw is already on the stack (e.g. via nested -/// message pumping in the Windows window procedure), instead of running a -/// nested draw or panicking on the already-borrowed App. -fn draw_in_progress() -> bool { - CURRENT_ELEMENT_ARENA.with(|current| current.get().is_some()) -} - -/// Allocates an element in the current arena. Uses the app-specific arena if one -/// is active (during draw), otherwise falls back to the thread-local ELEMENT_ARENA. -pub(crate) fn with_element_arena(f: impl FnOnce(&mut Arena) -> R) -> R { - CURRENT_ELEMENT_ARENA.with(|current| { - if let Some(arena_ptr) = current.get() { - // SAFETY: The pointer is valid for the duration of the draw operation - // that set it, and we're being called during that same draw. - let arena_cell = unsafe { &*arena_ptr }; - f(&mut arena_cell.borrow_mut()) - } else { - ELEMENT_ARENA.with_borrow_mut(f) - } - }) -} - -/// Scope guard that sets CURRENT_ELEMENT_ARENA for the duration of a draw -/// operation and tracks the arena's scope depth, so that a nested draw's -/// `ArenaClearNeeded::clear` is deferred rather than freeing memory the outer -/// draw still references (see `Arena::clear`). -/// -/// Call [`ElementArenaScope::exit`] with the same arena that was entered to -/// obtain the [`ArenaClearNeeded`] token the draw now owes; requiring `exit` -/// makes it impossible to request a clear before the scope has ended. The -/// scope's teardown — restoring the thread-local and balancing `begin_scope` -/// with `end_scope` — happens in `Drop`, so the arena's scope depth stays -/// balanced on every path, including when a panic unwinds a draw before `exit` -/// is reached. (If teardown lived only in `exit`, such a panic would leave the -/// scope depth permanently elevated and defer every future clear, leaking -/// memory unboundedly.) -pub(crate) struct ElementArenaScope { - /// The entered arena: compared against the argument in `exit`, and - /// dereferenced in `Drop` to end its scope (see the SAFETY note there). - entered: *const RefCell, - previous: Option<*const RefCell>, - exited: bool, -} - -impl ElementArenaScope { - /// Enter a scope where element allocations use the given arena. - pub(crate) fn enter(arena: &RefCell) -> Self { - arena.borrow_mut().begin_scope(); - let previous = CURRENT_ELEMENT_ARENA.with(|current| { - let prev = current.get(); - current.set(Some(arena as *const RefCell)); - prev - }); - Self { - entered: arena as *const RefCell, - previous, - exited: false, - } - } - - /// End the scope: restores the previously-current arena and ends the - /// arena's clear-deferral scope. Returns the token for the arena clear the - /// draw now owes; producing it here makes it impossible to request a clear - /// before the scope has ended (which would be silently deferred forever). - /// - /// Panics if passed a different arena than was entered: ending the scope - /// of the wrong arena would unbalance two arenas' scope depths, allowing - /// one of them to clear while a draw still references its memory. - pub(crate) fn exit(mut self, arena: &RefCell) -> ArenaClearNeeded { - assert!( - std::ptr::eq(self.entered, arena), - "ElementArenaScope::exit called with a different arena than was entered" - ); - self.exited = true; - // Teardown (restoring the thread-local and ending the arena's - // clear-deferral scope) runs in `Drop`, which fires both here — `self` - // is dropped as `exit` returns, before the token reaches the caller — - // and when a panic unwinds the draw before `exit` is reached. - ArenaClearNeeded::new(arena) - } -} - -impl Drop for ElementArenaScope { - fn drop(&mut self) { - // Teardown lives here (rather than in `exit`) so it runs exactly once on - // every path: `exit` consumes and drops the guard on the normal path, - // and unwinding drops it on the panic path. Balancing `begin_scope` here - // keeps the arena's scope depth correct even when a draw panics; if this - // only happened in `exit`, a panic between `enter` and `exit` would leave - // the depth elevated and defer every future clear. - CURRENT_ELEMENT_ARENA.with(|current| { - current.set(self.previous); - }); - // SAFETY: `entered` came from a `&RefCell` in `enter`, and the - // arena (owned by the `App` being drawn) outlives this guard on both the - // normal and unwinding paths, since the guard is a local of the draw. - unsafe { &*self.entered }.borrow_mut().end_scope(); - if !self.exited && !std::thread::panicking() { - debug_assert!(false, "ElementArenaScope dropped without calling exit()"); - log::error!( - "ElementArenaScope dropped without calling exit(); \ - the arena clear for this draw was never requested" - ); - } - } -} - -/// Returned when the element arena has been used and so must be cleared before the next draw. -#[must_use] -pub struct ArenaClearNeeded { - /// Identity of the arena that was drawn into. Only ever compared against - /// another pointer in `clear`; never dereferenced. - arena: *const RefCell, -} - -impl ArenaClearNeeded { - /// Create a new ArenaClearNeeded token for the App whose arena was drawn - /// into. Private: the only way to obtain one is [`ElementArenaScope::exit`]. - fn new(arena: &RefCell) -> Self { - Self { - arena: arena as *const RefCell, - } - } - - /// Clear the element arena of the App the draw ran against. If an enclosing - /// draw is still in progress (this draw was nested inside it), the clear is - /// deferred to the enclosing draw's own `ArenaClearNeeded` so that its live - /// allocations aren't freed. - /// - /// Panics if passed a different App than the draw ran against, since - /// clearing another App's arena could free memory its draws still - /// reference. - pub fn clear(self, cx: &mut App) { - assert!( - std::ptr::eq(self.arena, &cx.element_arena), - "ArenaClearNeeded::clear called with a different App than the draw ran against" - ); - cx.element_arena.borrow_mut().clear(); - } -} - -pub(crate) type FocusMap = RwLock>; -pub(crate) struct FocusRef { - pub(crate) ref_count: AtomicUsize, - pub(crate) tab_index: isize, - pub(crate) tab_stop: bool, -} - -impl FocusId { - /// Obtains whether the element associated with this handle is currently focused. - pub fn is_focused(&self, window: &Window) -> bool { - window.focus == Some(*self) - } - - /// Obtains whether the element associated with this handle contains the focused - /// element or is itself focused. - pub fn contains_focused(&self, window: &Window, cx: &App) -> bool { - window - .focused(cx) - .is_some_and(|focused| self.contains(focused.id, window)) - } - - /// Obtains whether the element associated with this handle is contained within the - /// focused element or is itself focused. - pub fn within_focused(&self, window: &Window, cx: &App) -> bool { - let focused = window.focused(cx); - focused.is_some_and(|focused| focused.id.contains(*self, window)) - } - - /// Obtains whether this handle contains the given handle in the most recently rendered frame. - pub(crate) fn contains(&self, other: Self, window: &Window) -> bool { - window - .rendered_frame - .dispatch_tree - .focus_contains(*self, other) - } -} - -/// A handle which can be used to track and manipulate the focused element in a window. -pub struct FocusHandle { - pub(crate) id: FocusId, - handles: Arc, - /// The index of this element in the tab order. - pub tab_index: isize, - /// Whether this element can be focused by tab navigation. - pub tab_stop: bool, -} - -impl std::fmt::Debug for FocusHandle { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("FocusHandle({:?})", self.id)) - } -} - -impl FocusHandle { - pub(crate) fn new(handles: &Arc) -> Self { - let id = handles.write().insert(FocusRef { - ref_count: AtomicUsize::new(1), - tab_index: 0, - tab_stop: false, - }); - - Self { - id, - tab_index: 0, - tab_stop: false, - handles: handles.clone(), - } - } - - pub(crate) fn for_id(id: FocusId, handles: &Arc) -> Option { - let lock = handles.read(); - let focus = lock.get(id)?; - if atomic_incr_if_not_zero(&focus.ref_count) == 0 { - return None; - } - Some(Self { - id, - tab_index: focus.tab_index, - tab_stop: focus.tab_stop, - handles: handles.clone(), - }) - } - - /// Sets the tab index of the element associated with this handle. - pub fn tab_index(mut self, index: isize) -> Self { - self.tab_index = index; - if let Some(focus) = self.handles.write().get_mut(self.id) { - focus.tab_index = index; - } - self - } - - /// Sets whether the element associated with this handle is a tab stop. - /// - /// When `false`, the element will not be included in the tab order. - pub fn tab_stop(mut self, tab_stop: bool) -> Self { - self.tab_stop = tab_stop; - if let Some(focus) = self.handles.write().get_mut(self.id) { - focus.tab_stop = tab_stop; - } - self - } - - /// Converts this focus handle into a weak variant, which does not prevent it from being released. - pub fn downgrade(&self) -> WeakFocusHandle { - WeakFocusHandle { - id: self.id, - handles: Arc::downgrade(&self.handles), - } - } - - /// Moves the focus to the element associated with this handle. - pub fn focus(&self, window: &mut Window, cx: &mut App) { - window.focus(self, cx) - } - - /// Obtains whether the element associated with this handle is currently focused. - pub fn is_focused(&self, window: &Window) -> bool { - self.id.is_focused(window) - } - - /// Obtains whether the element associated with this handle contains the focused - /// element or is itself focused. - pub fn contains_focused(&self, window: &Window, cx: &App) -> bool { - self.id.contains_focused(window, cx) - } - - /// Obtains whether the element associated with this handle is contained within the - /// focused element or is itself focused. - pub fn within_focused(&self, window: &Window, cx: &mut App) -> bool { - self.id.within_focused(window, cx) - } - - /// Obtains whether this handle contains the given handle in the most recently rendered frame. - pub fn contains(&self, other: &Self, window: &Window) -> bool { - self.id.contains(other.id, window) - } - - /// Dispatch an action on the element that rendered this focus handle - pub fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut App) { - if let Some(node_id) = window - .rendered_frame - .dispatch_tree - .focusable_node_id(self.id) - { - window.dispatch_action_on_node(node_id, action, cx) - } - } -} - -impl Clone for FocusHandle { - fn clone(&self) -> Self { - Self::for_id(self.id, &self.handles).unwrap() - } -} - -impl PartialEq for FocusHandle { - fn eq(&self, other: &Self) -> bool { - self.id == other.id - } -} - -impl Eq for FocusHandle {} - -impl Drop for FocusHandle { - fn drop(&mut self) { - self.handles - .read() - .get(self.id) - .unwrap() - .ref_count - .fetch_sub(1, SeqCst); - } -} - -/// A weak reference to a focus handle. -#[derive(Clone, Debug)] -pub struct WeakFocusHandle { - pub(crate) id: FocusId, - pub(crate) handles: Weak, -} - -impl WeakFocusHandle { - /// Attempts to upgrade the [WeakFocusHandle] to a [FocusHandle]. - pub fn upgrade(&self) -> Option { - let handles = self.handles.upgrade()?; - FocusHandle::for_id(self.id, &handles) - } -} - -impl PartialEq for WeakFocusHandle { - fn eq(&self, other: &WeakFocusHandle) -> bool { - self.id == other.id - } -} - -impl Eq for WeakFocusHandle {} - -impl PartialEq for WeakFocusHandle { - fn eq(&self, other: &FocusHandle) -> bool { - self.id == other.id - } -} - -impl PartialEq for FocusHandle { - fn eq(&self, other: &WeakFocusHandle) -> bool { - self.id == other.id - } -} - -/// Focusable allows users of your view to easily -/// focus it (using window.focus_view(cx, view)) -pub trait Focusable: 'static { - /// Returns the focus handle associated with this view. - fn focus_handle(&self, cx: &App) -> FocusHandle; -} - -impl Focusable for Entity { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.read(cx).focus_handle(cx) - } -} - -/// ManagedView is a view (like a Modal, Popover, Menu, etc.) -/// where the lifecycle of the view is handled by another view. -pub trait ManagedView: Focusable + EventEmitter + Render {} - -impl + Render> ManagedView for M {} - -/// Emitted by implementers of [`ManagedView`] to indicate the view should be dismissed, such as when a view is presented as a modal. -pub struct DismissEvent; - -type FrameCallback = Box; - -pub(crate) type AnyMouseListener = - Box; - -#[derive(Clone)] -pub(crate) struct CursorStyleRequest { - pub(crate) hitbox_id: Option, - pub(crate) style: CursorStyle, -} - -#[derive(Default, Eq, PartialEq)] -pub(crate) struct HitTest { - pub(crate) ids: SmallVec<[HitboxId; 8]>, - pub(crate) hover_hitbox_count: usize, -} - -/// A type of window control area that corresponds to the platform window. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum WindowControlArea { - /// An area that allows dragging of the platform window. - Drag, - /// An area that allows closing of the platform window. - Close, - /// An area that allows maximizing of the platform window. - Max, - /// An area that allows minimizing of the platform window. - Min, -} - -/// An identifier for a [Hitbox] which also includes [HitboxBehavior]. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] -pub struct HitboxId(u64); - -#[cfg(feature = "test-support")] -impl HitboxId { - /// A placeholder HitboxId exclusively for integration testing API's that - /// need a hitbox but where the value of the hitbox does not matter. The - /// alternative is to make the Hitbox optional but that complicates the - /// implementation. - pub const fn placeholder() -> Self { - Self(0) - } -} - -impl HitboxId { - /// Checks if the hitbox with this ID is currently hovered. Returns `false` during keyboard - /// input modality so that keyboard navigation suppresses hover highlights. Except when handling - /// `ScrollWheelEvent`, this is typically what you want when determining whether to handle mouse - /// events or paint hover styles. - /// - /// See [`Hitbox::is_hovered`] for details. - pub fn is_hovered(self, window: &Window) -> bool { - // If this hitbox has captured the pointer, it's always considered hovered - if window.captured_hitbox == Some(self) { - return true; - } - if window.last_input_was_keyboard() { - return false; - } - self.hit_test(window) - } - - /// Checks if the hitbox with this ID is currently hovered, regardless of the last - /// input modality used. - /// - /// See [`HitboxId::is_hovered`] for more details. - pub(crate) fn is_hovered_ignoring_last_input(self, window: &Window) -> bool { - // If this hitbox has captured the pointer, it's always considered hovered - if window.captured_hitbox == Some(self) { - return true; - } - self.hit_test(window) - } - - fn hit_test(self, window: &Window) -> bool { - let hit_test = &window.mouse_hit_test; - for id in hit_test.ids.iter().take(hit_test.hover_hitbox_count) { - if self == *id { - return true; - } - } - false - } - - /// Checks if the hitbox with this ID contains the mouse and should handle scroll events. - /// Typically this should only be used when handling `ScrollWheelEvent`, and otherwise - /// `is_hovered` should be used. See the documentation of `Hitbox::is_hovered` for details about - /// this distinction. - pub fn should_handle_scroll(self, window: &Window) -> bool { - window.mouse_hit_test.ids.contains(&self) - } - - fn next(mut self) -> HitboxId { - HitboxId(self.0.wrapping_add(1)) - } -} - -/// A rectangular region that potentially blocks hitboxes inserted prior. -/// See [Window::insert_hitbox] for more details. -#[derive(Clone, Debug, Deref)] -pub struct Hitbox { - /// A unique identifier for the hitbox. - pub id: HitboxId, - /// The bounds of the hitbox. - #[deref] - pub bounds: Bounds, - /// The content mask when the hitbox was inserted. - pub content_mask: crate::ClipRegion, - /// Flags that specify hitbox behavior. - pub behavior: HitboxBehavior, -} - -impl Hitbox { - /// Checks if the hitbox is currently hovered. Returns `false` during keyboard input modality - /// so that keyboard navigation suppresses hover highlights. Except when handling - /// `ScrollWheelEvent`, this is typically what you want when determining whether to handle mouse - /// events or paint hover styles. - /// - /// This can return `false` even when the hitbox contains the mouse, if a hitbox in front of - /// this sets `HitboxBehavior::BlockMouse` (`InteractiveElement::occlude`) or - /// `HitboxBehavior::BlockMouseExceptScroll` (`InteractiveElement::block_mouse_except_scroll`), - /// or if the current input modality is keyboard (see [`Window::last_input_was_keyboard`]). - /// - /// Handling of `ScrollWheelEvent` should typically use `should_handle_scroll` instead. - /// Concretely, this is due to use-cases like overlays that cause the elements under to be - /// non-interactive while still allowing scrolling. More abstractly, this is because - /// `is_hovered` is about element interactions directly under the mouse - mouse moves, clicks, - /// hover styling, etc. In contrast, scrolling is about finding the current outer scrollable - /// container. - pub fn is_hovered(&self, window: &Window) -> bool { - self.id.is_hovered(window) - } - - /// Checks whether this hitbox would be hovered at `position`, regardless of the current input - /// modality or mouse position. - pub fn is_hovered_at(&self, position: Point, window: &Window) -> bool { - let hit_test = window.rendered_frame.hit_test(position); - hit_test - .ids - .iter() - .take(hit_test.hover_hitbox_count) - .any(|id| self.id == *id) - } - - /// Checks if the hitbox contains the mouse and should handle scroll events. Typically this - /// should only be used when handling `ScrollWheelEvent`, and otherwise `is_hovered` should be - /// used. See the documentation of `Hitbox::is_hovered` for details about this distinction. - /// - /// This can return `false` even when the hitbox contains the mouse, if a hitbox in front of - /// this sets `HitboxBehavior::BlockMouse` (`InteractiveElement::occlude`). - pub fn should_handle_scroll(&self, window: &Window) -> bool { - self.id.should_handle_scroll(window) - } -} - -/// How the hitbox affects mouse behavior. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -pub enum HitboxBehavior { - /// Normal hitbox mouse behavior, doesn't affect mouse handling for other hitboxes. - #[default] - Normal, - - /// All hitboxes behind this hitbox will be ignored and so will have `hitbox.is_hovered() == - /// false` and `hitbox.should_handle_scroll() == false`. Typically for elements this causes - /// skipping of all mouse events, hover styles, and tooltips. This flag is set by - /// [`InteractiveElement::occlude`]. - /// - /// For mouse handlers that check those hitboxes, this behaves the same as registering a - /// bubble-phase handler for every mouse event type: - /// - /// ```ignore - /// window.on_mouse_event(move |_: &EveryMouseEventTypeHere, phase, window, cx| { - /// if phase == DispatchPhase::Capture && hitbox.is_hovered(window) { - /// cx.stop_propagation(); - /// } - /// }) - /// ``` - /// - /// This has effects beyond event handling - any use of hitbox checking, such as hover - /// styles and tooltips. These other behaviors are the main point of this mechanism. An - /// alternative might be to not affect mouse event handling - but this would allow - /// inconsistent UI where clicks and moves interact with elements that are not considered to - /// be hovered. - BlockMouse, - - /// All hitboxes behind this hitbox will have `hitbox.is_hovered() == false`, even when - /// `hitbox.should_handle_scroll() == true`. Typically for elements this causes all mouse - /// interaction except scroll events to be ignored - see the documentation of - /// [`Hitbox::is_hovered`] for details. This flag is set by - /// [`InteractiveElement::block_mouse_except_scroll`]. - /// - /// For mouse handlers that check those hitboxes, this behaves the same as registering a - /// bubble-phase handler for every mouse event type **except** `ScrollWheelEvent`: - /// - /// ```ignore - /// window.on_mouse_event(move |_: &EveryMouseEventTypeExceptScroll, phase, window, cx| { - /// if phase == DispatchPhase::Bubble && hitbox.should_handle_scroll(window) { - /// cx.stop_propagation(); - /// } - /// }) - /// ``` - /// - /// See the documentation of [`Hitbox::is_hovered`] for details of why `ScrollWheelEvent` is - /// handled differently than other mouse events. If also blocking these scroll events is - /// desired, then a `cx.stop_propagation()` handler like the one above can be used. - /// - /// This has effects beyond event handling - this affects any use of `is_hovered`, such as - /// hover styles and tooltips. These other behaviors are the main point of this mechanism. - /// An alternative might be to not affect mouse event handling - but this would allow - /// inconsistent UI where clicks and moves interact with elements that are not considered to - /// be hovered. - BlockMouseExceptScroll, -} - -/// An identifier for a tooltip. -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] -pub struct TooltipId(usize); - -impl TooltipId { - /// Checks if the tooltip is currently hovered. - pub fn is_hovered(&self, window: &Window) -> bool { - window - .tooltip_bounds - .as_ref() - .is_some_and(|tooltip_bounds| { - tooltip_bounds.id == *self - && tooltip_bounds.bounds.contains(&window.mouse_position()) - }) - } -} - -pub(crate) struct TooltipBounds { - id: TooltipId, - bounds: Bounds, -} - -#[derive(Clone)] -pub(crate) struct TooltipRequest { - id: TooltipId, - tooltip: AnyTooltip, -} - -pub(crate) struct DeferredDraw { - current_view: EntityId, - priority: usize, - parent_node: DispatchNodeId, - element_id_stack: SmallVec<[ElementId; 32]>, - text_style_stack: Vec, - content_mask: Option, - rem_size: Pixels, - element: Option, - absolute_offset: Point, - prepaint_range: Range, - paint_range: Range, -} - -pub(crate) struct Frame { - pub(crate) focus: Option, - pub(crate) window_active: bool, - pub(crate) element_states: FxHashMap<(GlobalElementId, TypeId), ElementStateBox>, - accessed_element_states: Vec<(GlobalElementId, TypeId)>, - pub(crate) mouse_listeners: Vec>, - pub(crate) dispatch_tree: DispatchTree, - pub(crate) scene: Scene, - pub(crate) hitboxes: Vec, - pub(crate) window_control_hitboxes: Vec<(WindowControlArea, Hitbox)>, - pub(crate) deferred_draws: Vec, - pub(crate) input_handlers: Vec>, - pub(crate) tooltip_requests: Vec>, - pub(crate) cursor_styles: Vec, - #[cfg(any(test, feature = "test-support"))] - pub(crate) debug_bounds: FxHashMap>, - #[cfg(any(feature = "inspector", debug_assertions))] - pub(crate) next_inspector_instance_ids: FxHashMap, usize>, - #[cfg(any(feature = "inspector", debug_assertions))] - pub(crate) inspector_hitboxes: FxHashMap, - pub(crate) tab_stops: TabStopMap, -} - -#[derive(Clone, Default)] -pub(crate) struct PrepaintStateIndex { - hitboxes_index: usize, - tooltips_index: usize, - deferred_draws_index: usize, - dispatch_tree_index: usize, - accessed_element_states_index: usize, - line_layout_index: LineLayoutIndex, -} - -#[derive(Clone, Default)] -pub(crate) struct PaintIndex { - scene_index: usize, - mouse_listeners_index: usize, - input_handlers_index: usize, - cursor_styles_index: usize, - accessed_element_states_index: usize, - tab_handle_index: usize, - line_layout_index: LineLayoutIndex, -} - -impl Frame { - pub(crate) fn new(dispatch_tree: DispatchTree) -> Self { - Frame { - focus: None, - window_active: false, - element_states: FxHashMap::default(), - accessed_element_states: Vec::new(), - mouse_listeners: Vec::new(), - dispatch_tree, - scene: Scene::default(), - hitboxes: Vec::new(), - window_control_hitboxes: Vec::new(), - deferred_draws: Vec::new(), - input_handlers: Vec::new(), - tooltip_requests: Vec::new(), - cursor_styles: Vec::new(), - - #[cfg(any(test, feature = "test-support"))] - debug_bounds: FxHashMap::default(), - - #[cfg(any(feature = "inspector", debug_assertions))] - next_inspector_instance_ids: FxHashMap::default(), - - #[cfg(any(feature = "inspector", debug_assertions))] - inspector_hitboxes: FxHashMap::default(), - tab_stops: TabStopMap::default(), - } - } - - pub(crate) fn clear(&mut self) { - self.element_states.clear(); - self.accessed_element_states.clear(); - self.mouse_listeners.clear(); - self.dispatch_tree.clear(); - self.scene.clear(); - self.input_handlers.clear(); - self.tooltip_requests.clear(); - self.cursor_styles.clear(); - self.hitboxes.clear(); - self.window_control_hitboxes.clear(); - self.deferred_draws.clear(); - self.tab_stops.clear(); - self.focus = None; - - #[cfg(any(test, feature = "test-support"))] - { - self.debug_bounds.clear(); - } - - #[cfg(any(feature = "inspector", debug_assertions))] - { - self.next_inspector_instance_ids.clear(); - self.inspector_hitboxes.clear(); - } - } - - pub(crate) fn cursor_style(&self, window: &Window) -> Option { - self.cursor_styles - .iter() - .rev() - .fold_while(None, |style, request| match request.hitbox_id { - None => Done(Some(request.style)), - Some(hitbox_id) => Continue(style.or_else(|| { - hitbox_id - .is_hovered_ignoring_last_input(window) - .then_some(request.style) - })), - }) - .into_inner() - } - - pub(crate) fn hit_test(&self, position: Point) -> HitTest { - let mut set_hover_hitbox_count = false; - let mut hit_test = HitTest::default(); - for hitbox in self.hitboxes.iter().rev() { - let bounds = hitbox.bounds.intersect(&hitbox.content_mask.bounds); - if bounds.contains(&position) { - hit_test.ids.push(hitbox.id); - if !set_hover_hitbox_count - && hitbox.behavior == HitboxBehavior::BlockMouseExceptScroll - { - hit_test.hover_hitbox_count = hit_test.ids.len(); - set_hover_hitbox_count = true; - } - if hitbox.behavior == HitboxBehavior::BlockMouse { - break; - } - } - } - if !set_hover_hitbox_count { - hit_test.hover_hitbox_count = hit_test.ids.len(); - } - hit_test - } - - pub(crate) fn focus_path(&self) -> SmallVec<[FocusId; 8]> { - self.focus - .map(|focus_id| self.dispatch_tree.focus_path(focus_id)) - .unwrap_or_default() - } - - pub(crate) fn finish(&mut self, prev_frame: &mut Self) { - for element_state_key in &self.accessed_element_states { - if let Some((element_state_key, element_state)) = - prev_frame.element_states.remove_entry(element_state_key) - { - self.element_states.insert(element_state_key, element_state); - } - } - - self.scene.finish(); - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] -enum InputModality { - Mouse, - Keyboard, - Touch, -} - -/// Holds the state for a specific window. -pub struct Window { - pub(crate) handle: AnyWindowHandle, - pub(crate) invalidator: WindowInvalidator, - pub(crate) removed: bool, - pub(crate) platform_window: Box, - display_id: Option, - is_resizable: bool, - is_minimizable: bool, - sprite_atlas: Arc, - text_system: Arc, - text_rendering_mode: Rc>, - rem_size: Pixels, - /// The stack of override values for the window's rem size. - /// - /// This is used by `with_rem_size` to allow rendering an element tree with - /// a given rem size. - rem_size_override_stack: SmallVec<[Pixels; 8]>, - pub(crate) viewport_size: Size, - layout_engine: Option, - pub(crate) root: Option, - pub(crate) element_id_stack: SmallVec<[ElementId; 32]>, - pub(crate) text_style_stack: Vec, - pub(crate) rendered_entity_stack: Vec, - pub(crate) element_offset_stack: Vec>, - pub(crate) element_opacity: f32, - pub(crate) content_mask_stack: Vec, - pub(crate) requested_autoscroll: Option>, - /// The [`TextInputConfiguration`] most recently forwarded to the platform - /// window, so that only actual changes are forwarded (reconfiguring a live - /// input session can restart the IME connection). - last_text_input_configuration: Option, - focused_text_input_active: bool, - pub(crate) image_cache_stack: Vec, - pub(crate) rendered_frame: Frame, - pub(crate) next_frame: Frame, - next_hitbox_id: HitboxId, - pub(crate) next_tooltip_id: TooltipId, - pub(crate) tooltip_bounds: Option, - pub(crate) next_frame_callbacks: Rc>>, - pub(crate) dirty_views: FxHashSet, - focus_listeners: SubscriberSet<(), AnyWindowFocusListener>, - pub(crate) focus_lost_listeners: SubscriberSet<(), AnyObserver>, - focus_lost_path: SmallVec<[FocusId; 8]>, - default_prevented: bool, - mouse_position: Point, - mouse_hit_test: HitTest, - modifiers: Modifiers, - capslock: Capslock, - scale_factor: f32, - pub(crate) bounds_observers: SubscriberSet<(), AnyObserver>, - appearance: WindowAppearance, - pub(crate) appearance_observers: SubscriberSet<(), AnyObserver>, - pub(crate) button_layout_observers: SubscriberSet<(), AnyObserver>, - active: Rc>, - hovered: Rc>, - pub(crate) needs_present: Rc>, - /// Tracks recent input event timestamps to determine if input is arriving at a high rate. - /// Used to selectively enable VRR optimization only when input rate exceeds 60fps. - pub(crate) input_rate_tracker: Rc>, - #[cfg(feature = "profiler")] - window_profiler: profiler::WindowProfiler, - last_input_modality: InputModality, - touch_gestures: TouchGestureRecognizer, - touch_prediction_enabled: bool, - long_press_timer: Option>, - long_press_capture: Option, - pub(crate) refreshing: bool, - pub(crate) activation_observers: SubscriberSet<(), AnyObserver>, - pub(crate) focus: Option, - focus_enabled: bool, - /// Incremented every time focus moves. Used to invalidate a - /// pending keyboard activation state when focus changes. - pub(crate) focus_generation: u64, - pending_input: Option, - pending_modifier: ModifierState, - pub(crate) pending_input_observers: SubscriberSet<(), AnyObserver>, - prompt: Option, - pub(crate) client_inset: Option, - /// The hitbox that has captured the pointer, if any. - /// While captured, mouse events route to this hitbox regardless of hit testing. - captured_hitbox: Option, - #[cfg(any(feature = "inspector", debug_assertions))] - inspector: Option>, - #[cfg(feature = "profiler")] - debug_frame_overlay: crate::debug_overlay::DebugFrameOverlay, - pub(crate) a11y: A11y, -} - -#[derive(Clone, Debug, Default)] -struct ModifierState { - modifiers: Modifiers, - saw_other_input: bool, -} - -/// Tracks input event timestamps to determine if input is arriving at a high rate. -/// Used for selective VRR (Variable Refresh Rate) optimization. -#[derive(Clone, Debug)] -pub(crate) struct InputRateTracker { - timestamps: Vec, - window: Duration, - inputs_per_second: u32, - sustain_until: Instant, - sustain_duration: Duration, -} - -impl Default for InputRateTracker { - fn default() -> Self { - Self { - timestamps: Vec::new(), - window: Duration::from_millis(100), - inputs_per_second: 60, - sustain_until: Instant::now(), - sustain_duration: Duration::from_secs(1), - } - } -} - -impl InputRateTracker { - pub fn record_input(&mut self) { - let now = Instant::now(); - self.timestamps.push(now); - self.prune_old_timestamps(now); - - let min_events = self.inputs_per_second as u128 * self.window.as_millis() / 1000; - if self.timestamps.len() as u128 >= min_events { - self.sustain_until = now + self.sustain_duration; - } - } - - pub fn is_high_rate(&self) -> bool { - Instant::now() < self.sustain_until - } - - fn prune_old_timestamps(&mut self, now: Instant) { - self.timestamps - .retain(|&t| now.duration_since(t) <= self.window); - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum DrawPhase { - None, - Prepaint, - Paint, - Focus, -} - -pub(crate) const PENDING_INPUT_TIMEOUT: Duration = Duration::from_secs(1); - -/// Pending input for a potential multi-stroke key binding. -pub struct PendingInputStatus<'a> { - keystrokes: &'a [Keystroke], - timeout: Option, -} - -impl<'a> PendingInputStatus<'a> { - /// Returns the keystrokes entered so far. - pub fn keystrokes(&self) -> &'a [Keystroke] { - self.keystrokes - } - - /// Returns the timeout state for flushing this input, if it needs a timeout. - pub fn timeout(&self) -> Option { - self.timeout - } -} - -/// The timeout state for pending input. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct PendingInputTimeoutStatus { - duration: Duration, - remaining: Duration, - started_at: Option, - paused: bool, -} - -impl PendingInputTimeoutStatus { - /// Returns the full timeout duration. - pub fn duration(&self) -> Duration { - self.duration - } - - /// Returns the duration remaining before pending input is flushed. - pub fn remaining(&self, cx: &App) -> Duration { - self.started_at - .map(|started_at| { - self.remaining - .saturating_sub(cx.background_executor().now() - started_at) - }) - .unwrap_or(self.remaining) - } - - /// Returns whether the timeout is paused. - pub fn is_paused(&self) -> bool { - self.paused - } -} - -#[derive(Debug)] -struct PendingInputTimeout { - duration: Duration, - remaining: Duration, - state: PendingInputTimeoutState, -} - -#[derive(Debug)] -enum PendingInputTimeoutState { - Running { started_at: Instant, task: Task<()> }, - Paused { pause: PendingInputTimeoutPause }, -} - -#[derive(Debug)] -struct PendingInputTimeoutPause { - owner_id: EntityId, - _release_subscription: Subscription, -} - -impl PendingInputTimeout { - fn is_paused(&self) -> bool { - matches!(&self.state, PendingInputTimeoutState::Paused { .. }) - } - - fn pause(&mut self, pause: PendingInputTimeoutPause, now: Instant) -> bool { - match std::mem::replace(&mut self.state, PendingInputTimeoutState::Paused { pause }) { - PendingInputTimeoutState::Running { started_at, task } => { - self.remaining = self.remaining.saturating_sub(now - started_at); - drop(task); - true - } - previous_state @ PendingInputTimeoutState::Paused { .. } => { - self.state = previous_state; - false - } - } - } - - fn pause_owner_id(&self) -> Option { - match &self.state { - PendingInputTimeoutState::Running { .. } => None, - PendingInputTimeoutState::Paused { pause } => Some(pause.owner_id), - } - } - - fn resume(&mut self, owner_id: EntityId, started_at: Instant, task: Task<()>) -> bool { - match std::mem::replace( - &mut self.state, - PendingInputTimeoutState::Running { started_at, task }, - ) { - PendingInputTimeoutState::Paused { pause } if pause.owner_id == owner_id => true, - previous_state => { - self.state = previous_state; - false - } - } - } - - fn reset_duration(&mut self, duration: Duration) { - self.duration = duration; - self.remaining = duration; - } - - fn status(&self) -> PendingInputTimeoutStatus { - let (started_at, paused) = match &self.state { - PendingInputTimeoutState::Running { started_at, .. } => (Some(*started_at), false), - PendingInputTimeoutState::Paused { .. } => (None, true), - }; - PendingInputTimeoutStatus { - duration: self.duration, - remaining: self.remaining, - started_at, - paused, - } - } -} - -#[derive(Default, Debug)] -struct PendingInput { - keystrokes: SmallVec<[Keystroke; 1]>, - focus: Option, - timeout: Option, -} - -pub(crate) struct ElementStateBox { - pub(crate) inner: Box, - #[cfg(debug_assertions)] - pub(crate) type_name: &'static str, -} - -fn default_bounds(display_id: Option, cx: &mut App) -> WindowBounds { - // TODO, BUG: if you open a window with the currently active window - // on the stack, this will erroneously fallback to `None` - // - // TODO these should be the initial window bounds not considering maximized/fullscreen - let active_window_bounds = cx - .active_window() - .and_then(|w| w.update(cx, |_, window, _| window.window_bounds()).ok()); - - const CASCADE_OFFSET: f32 = 25.0; - - let display = display_id - .map(|id| cx.find_display(id)) - .unwrap_or_else(|| cx.primary_display()); - - let default_placement = || Bounds::new(point(px(0.), px(0.)), DEFAULT_WINDOW_SIZE); - - // Use visible_bounds to exclude taskbar/dock areas - let display_bounds = display - .as_ref() - .map(|d| d.visible_bounds()) - .unwrap_or_else(default_placement); - - let ( - Bounds { - origin: base_origin, - size: base_size, - }, - window_bounds_ctor, - ): (_, fn(Bounds) -> WindowBounds) = match active_window_bounds { - Some(bounds) => match bounds { - WindowBounds::Windowed(bounds) => (bounds, WindowBounds::Windowed), - WindowBounds::Maximized(bounds) => (bounds, WindowBounds::Maximized), - WindowBounds::Fullscreen(bounds) => (bounds, WindowBounds::Fullscreen), - }, - None => ( - display - .as_ref() - .map(|d| d.default_bounds()) - .unwrap_or_else(default_placement), - WindowBounds::Windowed, - ), - }; - - let cascade_offset = point(px(CASCADE_OFFSET), px(CASCADE_OFFSET)); - let proposed_origin = base_origin + cascade_offset; - let proposed_bounds = Bounds::new(proposed_origin, base_size); - - let display_right = display_bounds.origin.x + display_bounds.size.width; - let display_bottom = display_bounds.origin.y + display_bounds.size.height; - let window_right = proposed_bounds.origin.x + proposed_bounds.size.width; - let window_bottom = proposed_bounds.origin.y + proposed_bounds.size.height; - - let fits_horizontally = window_right <= display_right; - let fits_vertically = window_bottom <= display_bottom; - - let final_origin = match (fits_horizontally, fits_vertically) { - (true, true) => proposed_origin, - (false, true) => point(display_bounds.origin.x, base_origin.y), - (true, false) => point(base_origin.x, display_bounds.origin.y), - (false, false) => display_bounds.origin, - }; - window_bounds_ctor(Bounds::new(final_origin, base_size)) -} - -impl Window { - pub(crate) fn new( - handle: AnyWindowHandle, - options: WindowOptions, - cx: &mut App, - ) -> Result { - let WindowOptions { - window_bounds, - titlebar, - focus, - show, - kind, - is_movable, - app_owns_titlebar_drag, - inactive_frame_interval, - is_resizable, - is_minimizable, - display_id, - window_background, - app_id, - window_min_size, - window_decorations, - #[cfg_attr( - not(any(target_os = "linux", target_os = "freebsd")), - allow(unused_variables) - )] - icon, - #[cfg_attr(not(target_os = "macos"), allow(unused_variables))] - tabbing_identifier, - } = options; - - let initial_window_title = titlebar - .as_ref() - .and_then(|titlebar| titlebar.title.clone()); - - let window_bounds = window_bounds.unwrap_or_else(|| default_bounds(display_id, cx)); - let mut platform_window = cx.platform.open_window( - handle, - WindowParams { - bounds: window_bounds.get_bounds(), - titlebar, - kind, - is_movable, - app_owns_titlebar_drag, - is_resizable, - is_minimizable, - focus, - show, - display_id, - window_min_size, - app_id: app_id.clone(), - icon, - #[cfg(target_os = "macos")] - tabbing_identifier, - }, - )?; - - let tab_bar_visible = platform_window.tab_bar_visible(); - SystemWindowTabController::init_visible(cx, tab_bar_visible); - if let Some(tabs) = platform_window.tabbed_windows() { - SystemWindowTabController::add_tab(cx, handle.window_id(), tabs); - } - - let display_id = platform_window.display().map(|display| display.id()); - let sprite_atlas = platform_window.sprite_atlas(); - let mouse_position = platform_window.mouse_position(); - let modifiers = platform_window.modifiers(); - let capslock = platform_window.capslock(); - let content_size = platform_window.content_size(); - let scale_factor = platform_window.scale_factor(); - let appearance = platform_window.appearance(); - let text_system = Arc::new(WindowTextSystem::new(cx.text_system().clone())); - let invalidator = WindowInvalidator::new(handle.window_id()); - let active = Rc::new(Cell::new(platform_window.is_active())); - let hovered = Rc::new(Cell::new(platform_window.is_hovered())); - let needs_present = Rc::new(Cell::new(false)); - let next_frame_callbacks: Rc>> = Default::default(); - let input_rate_tracker = Rc::new(RefCell::new(InputRateTracker::default())); - let last_frame_time = Rc::new(Cell::new(None)); - - platform_window - .request_decorations(window_decorations.unwrap_or(WindowDecorations::Server)); - platform_window.set_background_appearance(window_background); - - match window_bounds { - WindowBounds::Fullscreen(_) => platform_window.toggle_fullscreen(), - WindowBounds::Maximized(_) => platform_window.zoom(), - WindowBounds::Windowed(_) => {} - } - - let accessibility_force_disabled = cx.accessibility_force_disabled; - let a11y_active_flag = Arc::new(AtomicBool::new(false)); - - #[cfg(not(target_family = "wasm"))] - if !accessibility_force_disabled { - let mut initial_root_node = accesskit::Node::new(accesskit::Role::Window); - if let Some(title) = &initial_window_title { - initial_root_node.set_label(title.to_string()); - } - let initial_tree = accesskit::TreeUpdate { - nodes: vec![(ROOT_NODE_ID, initial_root_node)], - tree: Some(accesskit::Tree::new(ROOT_NODE_ID)), - tree_id: accesskit::TreeId::ROOT, - focus: ROOT_NODE_ID, - }; - let (activation_sender, activation_receiver) = async_channel::unbounded::<()>(); - let (deactivation_sender, deactivation_receiver) = async_channel::unbounded::<()>(); - let (action_sender, action_receiver) = - async_channel::unbounded::(); - - platform_window.a11y_init(crate::A11yCallbacks { - activation: { - let active_flag = a11y_active_flag.clone(); - Box::new(move || { - log::info!("Accessibility activated"); - active_flag.store(true, SeqCst); - activation_sender.send_blocking(()).log_err(); - Some(initial_tree.clone()) - }) - }, - action: Box::new(move |request| { - action_sender.send_blocking(request).log_err(); - }), - deactivation: { - let active_flag = a11y_active_flag.clone(); - Box::new(move || { - log::info!("Accessibility deactivated"); - active_flag.store(false, SeqCst); - deactivation_sender.send_blocking(()).log_err(); - }) - }, - }); - - // A11y can be activated at any time, and so we cannot compute a - // correct `TreeUpdate` on-demand. When this happens, we return a - // default empty `TreeUpdate`. - // - // So we force a new frame, which will then send a correct `TreeUpdate`. - let mut async_cx = cx.to_async(); - cx.foreground_executor() - .spawn(async move { - while activation_receiver.recv().await.is_ok() { - handle - .update(&mut async_cx, |_, window, _| window.refresh()) - .log_err(); - } - }) - .detach(); - - let mut async_cx = cx.to_async(); - cx.foreground_executor() - .spawn(async move { - while deactivation_receiver.recv().await.is_ok() { - handle - .update(&mut async_cx, |_, window, _| window.refresh()) - .log_err(); - } - }) - .detach(); - - let mut async_cx = cx.to_async(); - cx.foreground_executor() - .spawn(async move { - while let Ok(request) = action_receiver.recv().await { - handle - .update(&mut async_cx, |_, window, cx| { - window.handle_a11y_action(request, cx); - }) - .log_err(); - } - }) - .detach(); - } - - platform_window.on_close(Box::new({ - let window_id = handle.window_id(); - let mut cx = cx.to_async(); - move || { - let _ = handle.update(&mut cx, |_, window, _| window.remove_window()); - let _ = cx.update(|cx| { - SystemWindowTabController::remove_tab(cx, window_id); - }); - } - })); - platform_window.on_request_frame(Box::new({ - let mut cx = cx.to_async(); - let invalidator = invalidator.clone(); - let active = active.clone(); - let needs_present = needs_present.clone(); - let next_frame_callbacks = next_frame_callbacks.clone(); - let input_rate_tracker = input_rate_tracker.clone(); - let mut deferred_force_render = false; - move |request_frame_options| { - #[cfg(feature = "profiler")] - let _foreground_turn = profiler::journal::foreground_turn(); - // This must be checked before anything else: if this request - // arrived re-entrantly while a draw is on this thread's stack - // (e.g. via a nested message pump in the Windows window - // procedure), drawing would nest draws, and even touching the - // App would panic on its already-mutable borrow. Skip instead; - // the platform leaves the window invalidated (or re-invalidates - // it), so a fresh request arrives once the in-progress draw - // unwinds. Remember force_render so the deferred frame still - // bypasses the view cache. - // - // Returning here skips `complete_frame`, which on Wayland would - // stall the window's frame callbacks (no `surface.commit()`) — - // but calling it would hit the App borrow panic above, and this - // branch is unreachable there in practice: only Windows pumps - // platform events (and thus requests frames) mid-draw. - if draw_in_progress() { - log::debug!("deferring re-entrant window draw request"); - deferred_force_render |= request_frame_options.force_render; - return; - } - // Take the deferred flag first: `||` short-circuits, and leaving - // the flag set when this request already forces a render would - // force a second, redundant render on the next frame. - let force_render = - mem::take(&mut deferred_force_render) || request_frame_options.force_render; - - let thermal_state = handle - .update(&mut cx, |_, _, cx| cx.thermal_state()) - .log_err(); - - // Throttle frame rate based on conditions: - // - Thermal pressure (Serious/Critical): cap to ~60fps - // - Inactive window (not focused): cap to ~30fps to save energy - let min_frame_interval = if request_frame_options.require_presentation - || (!request_frame_options.force_render - && next_frame_callbacks.borrow().is_empty()) - { - None - } else if !active.get() && !input_rate_tracker.borrow_mut().is_high_rate() { - inactive_frame_interval - } else if let Some(ThermalState::Critical | ThermalState::Serious) = thermal_state { - Some(Duration::from_micros(16667)) - } else { - None - }; - - let now = Instant::now(); - if let Some(min_interval) = min_frame_interval { - if let Some(last_frame) = last_frame_time.get() - && now.duration_since(last_frame) < min_interval - { - // Don't lose a pending forced render to throttling. - deferred_force_render |= force_render; - // Deferred by throttling: ask demand-driven platforms to retry. - handle - .update(&mut cx, |_, window, _| { - window.platform_window.schedule_frame(); - }) - .log_err(); - // The demand that entered this branch (a deferred forced - // render or pending next-frame callbacks) is still - // unserved; platforms that stop requesting frames for - // idle windows need a wakeup to deliver the retry. - invalidator.wake_platform(); - return; - } - } - last_frame_time.set(Some(now)); - - let pending_next_frame_callbacks = next_frame_callbacks.take(); - if !pending_next_frame_callbacks.is_empty() { - handle - .update(&mut cx, |_, window, cx| { - for callback in pending_next_frame_callbacks { - callback(window, cx); - } - }) - .log_err(); - } - - // Keep presenting if input was recently arriving at a high rate (>= 60fps). - // Once high-rate input is detected, we sustain presentation for 1 second - // to prevent display underclocking during active input. - let needs_present = request_frame_options.require_presentation - || needs_present.get() - || input_rate_tracker.borrow_mut().is_high_rate(); - - if invalidator.is_dirty() || force_render { - measure("frame duration", || { - handle - .update(&mut cx, |_, window, cx| { - if force_render { - // Bypass cached view reuse so we don't replay stale - // atlas tile references after a GPU device recovery. - window.refresh(); - } - let arena_clear_needed = window.draw(cx); - window.present(); - arena_clear_needed.clear(cx); - }) - .log_err(); - }) - } else if needs_present { - handle - .update(&mut cx, |_, window, _| window.present()) - .log_err(); - } - - handle - .update(&mut cx, |_, window, _| { - if window.invalidator.is_dirty() - || !window.next_frame_callbacks.borrow().is_empty() - { - window.platform_window.schedule_frame(); - } - }) - .log_err(); - - // Platforms that stop requesting frames for idle windows only - // deliver another request after a wakeup. If demand remains - // after this frame (the window was re-invalidated mid-draw, or - // animations scheduled next-frame callbacks), re-arm the frame - // source explicitly. - if invalidator.is_dirty() || !next_frame_callbacks.borrow().is_empty() { - invalidator.wake_platform(); - } - } - })); - invalidator.set_platform_waker(platform_window.frame_waker()); - platform_window.on_resize(Box::new({ - let mut cx = cx.to_async(); - move |_, _| { - handle - .update(&mut cx, |_, window, cx| window.bounds_changed(cx)) - .log_err(); - } - })); - platform_window.on_moved(Box::new({ - let mut cx = cx.to_async(); - move || { - handle - .update(&mut cx, |_, window, cx| window.bounds_changed(cx)) - .log_err(); - } - })); - platform_window.on_appearance_changed(Box::new({ - let cx = cx.to_async(); - let foreground_executor = cx.foreground_executor().clone(); - move || { - let mut cx = cx.clone(); - // Defer the update because changing the AppKit appearance may - // synchronously invoke this callback while App is already borrowed. - foreground_executor - .spawn(async move { - handle - .update(&mut cx, |_, window, cx| window.appearance_changed(cx)) - .log_err(); - }) - .detach(); - } - })); - platform_window.on_button_layout_changed(Box::new({ - let mut cx = cx.to_async(); - move || { - handle - .update(&mut cx, |_, window, cx| window.button_layout_changed(cx)) - .log_err(); - } - })); - platform_window.on_active_status_change(Box::new({ - let mut cx = cx.to_async(); - move |active| { - handle - .update(&mut cx, |_, window, cx| { - window.active.set(active); - window.modifiers = window.platform_window.modifiers(); - window.capslock = window.platform_window.capslock(); - window - .activation_observers - .clone() - .retain(&(), |callback| callback(window, cx)); - - window.bounds_changed(cx); - window.refresh(); - - SystemWindowTabController::update_last_active(cx, window.handle.id); - }) - .log_err(); - } - })); - platform_window.on_hover_status_change(Box::new({ - let mut cx = cx.to_async(); - move |active| { - handle - .update(&mut cx, |_, window, _| { - window.hovered.set(active); - window.refresh(); - }) - .log_err(); - } - })); - platform_window.on_input({ - let mut cx = cx.to_async(); - Box::new(move |event| { - handle - .update(&mut cx, |_, window, cx| window.dispatch_event(event, cx)) - .log_err() - .unwrap_or(DispatchEventResult::default()) - }) - }); - platform_window.on_hit_test_window_control({ - let mut cx = cx.to_async(); - Box::new(move || { - handle - .update(&mut cx, |_, window, _cx| { - for (area, hitbox) in &window.rendered_frame.window_control_hitboxes { - if window.mouse_hit_test.ids.contains(&hitbox.id) { - return Some(*area); - } - } - None - }) - .log_err() - .unwrap_or(None) - }) - }); - platform_window.on_move_tab_to_new_window({ - let mut cx = cx.to_async(); - Box::new(move || { - handle - .update(&mut cx, |_, _window, cx| { - SystemWindowTabController::move_tab_to_new_window(cx, handle.window_id()); - }) - .log_err(); - }) - }); - platform_window.on_merge_all_windows({ - let mut cx = cx.to_async(); - Box::new(move || { - handle - .update(&mut cx, |_, _window, cx| { - SystemWindowTabController::merge_all_windows(cx, handle.window_id()); - }) - .log_err(); - }) - }); - platform_window.on_select_next_tab({ - let mut cx = cx.to_async(); - Box::new(move || { - handle - .update(&mut cx, |_, _window, cx| { - SystemWindowTabController::select_next_tab(cx, handle.window_id()); - }) - .log_err(); - }) - }); - platform_window.on_select_previous_tab({ - let mut cx = cx.to_async(); - Box::new(move || { - handle - .update(&mut cx, |_, _window, cx| { - SystemWindowTabController::select_previous_tab(cx, handle.window_id()) - }) - .log_err(); - }) - }); - platform_window.on_toggle_tab_bar({ - let mut cx = cx.to_async(); - Box::new(move || { - handle - .update(&mut cx, |_, window, cx| { - let tab_bar_visible = window.platform_window.tab_bar_visible(); - SystemWindowTabController::set_visible(cx, tab_bar_visible); - }) - .log_err(); - }) - }); - - if let Some(app_id) = app_id { - platform_window.set_app_id(&app_id); - } - - platform_window.map_window().unwrap(); - - Ok(Window { - handle, - invalidator, - removed: false, - platform_window, - display_id, - is_resizable, - is_minimizable, - sprite_atlas, - text_system, - text_rendering_mode: cx.text_rendering_mode.clone(), - rem_size: px(16.), - rem_size_override_stack: SmallVec::new(), - viewport_size: content_size, - layout_engine: Some(TaffyLayoutEngine::new()), - root: None, - element_id_stack: SmallVec::default(), - text_style_stack: Vec::new(), - rendered_entity_stack: Vec::new(), - element_offset_stack: Vec::new(), - content_mask_stack: Vec::new(), - element_opacity: 1.0, - requested_autoscroll: None, - last_text_input_configuration: None, - focused_text_input_active: false, - rendered_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), - next_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), - next_frame_callbacks, - next_hitbox_id: HitboxId(0), - next_tooltip_id: TooltipId::default(), - tooltip_bounds: None, - dirty_views: FxHashSet::default(), - focus_listeners: SubscriberSet::new(), - focus_lost_listeners: SubscriberSet::new(), - focus_lost_path: SmallVec::new(), - default_prevented: true, - mouse_position, - mouse_hit_test: HitTest::default(), - modifiers, - capslock, - scale_factor, - bounds_observers: SubscriberSet::new(), - appearance, - appearance_observers: SubscriberSet::new(), - button_layout_observers: SubscriberSet::new(), - active, - hovered, - needs_present, - input_rate_tracker, - #[cfg(feature = "profiler")] - window_profiler: profiler::WindowProfiler::new(handle.window_id())?, - last_input_modality: InputModality::Mouse, - touch_gestures: TouchGestureRecognizer::new( - cx.platform - .gestures() - .map_or_else(GestureTuning::default, |gestures| gestures.tuning()), - ), - touch_prediction_enabled: true, - long_press_timer: None, - long_press_capture: None, - refreshing: false, - activation_observers: SubscriberSet::new(), - focus: None, - focus_enabled: true, - focus_generation: 0, - pending_input: None, - pending_modifier: ModifierState::default(), - pending_input_observers: SubscriberSet::new(), - prompt: None, - client_inset: None, - image_cache_stack: Vec::new(), - captured_hitbox: None, - #[cfg(any(feature = "inspector", debug_assertions))] - inspector: None, - #[cfg(feature = "profiler")] - debug_frame_overlay: crate::debug_overlay::DebugFrameOverlay::new(), - a11y: A11y::new( - a11y_active_flag, - accessibility_force_disabled, - initial_window_title, - ), - }) - } - - pub(crate) fn new_focus_listener( - &self, - value: AnyWindowFocusListener, - ) -> (Subscription, impl FnOnce() + use<>) { - self.focus_listeners.insert((), value) - } -} - -#[derive(Clone, Debug, Default, PartialEq, Eq)] -#[expect(missing_docs)] -pub struct DispatchEventResult { - pub propagate: bool, - pub default_prevented: bool, -} - -/// Indicates which region of the window is visible. Content falling outside of -/// this mask will not be rendered. A mask carries both its rectangular cull -/// bounds and optional rounded corners; the bounds keep scene culling cheap, -/// while the renderer applies the corner shape to every primitive. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -#[repr(C)] -pub struct ContentMask { - /// The bounds - pub bounds: Bounds

, - /// The corner radii of the visible region. - pub corner_radii: Corners

, _keymap: &Keymap) { - *self.inner.state.menus.borrow_mut() = menus.into_iter().map(|menu| menu.owned()).collect(); - } - - fn get_menus(&self) -> Option> { - Some(self.inner.state.menus.borrow().clone()) - } - - fn set_dock_menu(&self, menus: Vec, _keymap: &Keymap) { - self.set_dock_menus(menus); - } - - fn on_app_menu_action(&self, callback: Box) { - self.inner - .state - .callbacks - .app_menu_action - .set(Some(callback)); - } - - fn on_will_open_app_menu(&self, callback: Box) { - self.inner - .state - .callbacks - .will_open_app_menu - .set(Some(callback)); - } - - fn on_validate_app_menu_command(&self, callback: Box bool>) { - self.inner - .state - .callbacks - .validate_app_menu_command - .set(Some(callback)); - } - - fn app_path(&self) -> Result { - Ok(std::env::current_exe()?) - } - - // todo(windows) - fn path_for_auxiliary_executable(&self, _name: &str) -> Result { - anyhow::bail!("not yet implemented"); - } - - fn set_cursor_style(&self, style: CursorStyle) { - let hcursor = load_cursor(style); - if self.inner.state.current_cursor.get().map(|c| c.0) != hcursor.map(|c| c.0) { - self.post_message( - WM_GPUI_CURSOR_STYLE_CHANGED, - WPARAM(0), - LPARAM(hcursor.map_or(0, |c| c.0 as isize)), - ); - self.inner.state.current_cursor.set(hcursor); - } - } - - fn hide_cursor_until_mouse_moves(&self) { - if !self - .inner - .state - .cursor_visible - .swap(false, Ordering::Relaxed) - { - return; - } - - for handle in self.raw_window_handles.read().iter() { - let Some(window) = window_from_hwnd(handle.as_raw()) else { - continue; - }; - if window.state.hovered.get() { - unsafe { SetCursor(None) }; - break; - } - } - } - - fn is_cursor_visible(&self) -> bool { - self.inner.state.cursor_visible.load(Ordering::Relaxed) - } - - fn should_auto_hide_scrollbars(&self) -> bool { - should_auto_hide_scrollbars().log_err().unwrap_or(false) - } - - fn write_to_clipboard(&self, item: ClipboardItem) { - write_to_clipboard(item); - } - - fn read_from_clipboard(&self) -> Option { - read_from_clipboard() - } - - fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task> { - // CredWriteW rejects larger blobs with the opaque RPC error - // 0x800706F7 "The stub received bad data", so fail with a clear - // message instead. - if password.len() > CRED_MAX_CREDENTIAL_BLOB_SIZE as usize { - return Task::ready(Err(anyhow!( - "credential for {url} is {} bytes, which exceeds the Windows Credential Manager limit of {CRED_MAX_CREDENTIAL_BLOB_SIZE} bytes", - password.len() - ))); - } - let password = password.to_vec(); - let mut username = username.encode_utf16().chain(Some(0)).collect_vec(); - let mut target_name = windows_credentials_target_name(url) - .encode_utf16() - .chain(Some(0)) - .collect_vec(); - self.foreground_executor().spawn(async move { - let credentials = CREDENTIALW { - LastWritten: unsafe { GetSystemTimeAsFileTime() }, - Flags: CRED_FLAGS(0), - Type: CRED_TYPE_GENERIC, - TargetName: PWSTR::from_raw(target_name.as_mut_ptr()), - CredentialBlobSize: password.len() as u32, - CredentialBlob: password.as_ptr() as *mut _, - Persist: CRED_PERSIST_LOCAL_MACHINE, - UserName: PWSTR::from_raw(username.as_mut_ptr()), - ..CREDENTIALW::default() - }; - unsafe { - CredWriteW(&credentials, 0).map_err(|err| { - anyhow!( - "Failed to write credentials to Windows Credential Manager: {}", - err, - ) - })?; - } - Ok(()) - }) - } - - fn read_credentials(&self, url: &str) -> Task)>>> { - let target_name = windows_credentials_target_name(url) - .encode_utf16() - .chain(Some(0)) - .collect_vec(); - self.foreground_executor().spawn(async move { - let mut credentials: *mut CREDENTIALW = std::ptr::null_mut(); - let result = unsafe { - CredReadW( - PCWSTR::from_raw(target_name.as_ptr()), - CRED_TYPE_GENERIC, - None, - &mut credentials, - ) - }; - - if let Err(err) = result { - // ERROR_NOT_FOUND means the credential doesn't exist. - // Return Ok(None) to match macOS and Linux behavior. - if err.code() == ERROR_NOT_FOUND.to_hresult() { - return Ok(None); - } - return Err(err.into()); - } - - if credentials.is_null() { - Ok(None) - } else { - let username: String = unsafe { (*credentials).UserName.to_string()? }; - let credential_blob = unsafe { - std::slice::from_raw_parts( - (*credentials).CredentialBlob, - (*credentials).CredentialBlobSize as usize, - ) - }; - let password = credential_blob.to_vec(); - unsafe { CredFree(credentials as *const _ as _) }; - Ok(Some((username, password))) - } - }) - } - - fn delete_credentials(&self, url: &str) -> Task> { - let target_name = windows_credentials_target_name(url) - .encode_utf16() - .chain(Some(0)) - .collect_vec(); - self.foreground_executor().spawn(async move { - unsafe { - CredDeleteW( - PCWSTR::from_raw(target_name.as_ptr()), - CRED_TYPE_GENERIC, - None, - )? - }; - Ok(()) - }) - } - - fn register_url_scheme(&self, _: &str) -> Task> { - Task::ready(Err(anyhow!("register_url_scheme unimplemented"))) - } - - fn perform_dock_menu_action(&self, action: usize) { - unsafe { - PostMessageW( - Some(self.handle), - WM_GPUI_DOCK_MENU_ACTION, - WPARAM(self.inner.validation_number), - LPARAM(action as isize), - ) - .log_err(); - } - } - - fn update_jump_list( - &self, - menus: Vec, - entries: Vec>, - ) -> Task>> { - self.update_jump_list(menus, entries) - } -} - -impl WindowsPlatformInner { - fn new(context: &mut PlatformWindowCreateContext) -> Result> { - let state = WindowsPlatformState::new(context.directx_devices.take()); - Ok(Rc::new(Self { - state, - raw_window_handles: context.raw_window_handles.clone(), - dispatcher: context - .dispatcher - .as_ref() - .context("missing dispatcher")? - .clone(), - validation_number: context.validation_number, - main_receiver: context - .main_receiver - .take() - .context("missing main receiver")?, - })) - } - - /// Calls `project` to project to the corresponding callback field, removes it from callbacks, calls `f` with the callback and then puts the callback back. - fn with_callback( - &self, - project: impl Fn(&PlatformCallbacks) -> &Cell>, - f: impl FnOnce(&mut T), - ) { - let callback = project(&self.state.callbacks).take(); - if let Some(mut callback) = callback { - f(&mut callback); - project(&self.state.callbacks).set(Some(callback)); - } - } - - fn handle_msg( - self: &Rc, - handle: HWND, - msg: u32, - wparam: WPARAM, - lparam: LPARAM, - ) -> LRESULT { - let handled = match msg { - WM_GPUI_CLOSE_ONE_WINDOW - | WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD - | WM_GPUI_DOCK_MENU_ACTION - | WM_GPUI_KEYBOARD_LAYOUT_CHANGED - | WM_GPUI_GPU_DEVICE_LOST - | WM_GPUI_END_SESSION => self.handle_gpui_events(msg, wparam, lparam), - WM_POWERBROADCAST => self.handle_power_broadcast(wparam), - _ => None, - }; - if let Some(result) = handled { - LRESULT(result) - } else { - unsafe { DefWindowProcW(handle, msg, wparam, lparam) } - } - } - - fn handle_gpui_events(&self, message: u32, wparam: WPARAM, lparam: LPARAM) -> Option { - if wparam.0 != self.validation_number { - log::error!("Wrong validation number while processing message: {message}"); - return None; - } - match message { - WM_GPUI_CLOSE_ONE_WINDOW => { - self.close_one_window(HWND(lparam.0 as _)); - Some(0) - } - WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD => self.run_foreground_task(), - WM_GPUI_DOCK_MENU_ACTION => self.handle_dock_action_event(lparam.0 as _), - WM_GPUI_KEYBOARD_LAYOUT_CHANGED => self.handle_keyboard_layout_change(), - WM_GPUI_GPU_DEVICE_LOST => self.handle_device_lost(lparam), - WM_GPUI_END_SESSION => self.handle_end_session(), - _ => unreachable!(), - } - } - - fn handle_end_session(&self) -> Option { - let mut shutdown_completed = false; - self.with_callback( - |callbacks| &callbacks.quit, - |callback| shutdown_completed = callback(), - ); - log::logger().flush(); - if shutdown_completed { - std::process::exit(0); - } - - // Shutdown couldn't run synchronously, since the AppCell is already borrowed. - // Windows may terminate the application as soon as we return from this handler, but if we post a WM_QUIT message now, - // we may get to gracefully shut down the app before we're terminated by the OS. - unsafe { PostQuitMessage(0) }; - Some(0) - } - - fn close_one_window(&self, target_window: HWND) -> bool { - let Some(all_windows) = self.raw_window_handles.upgrade() else { - log::error!("Failed to upgrade raw window handles"); - return false; - }; - let mut lock = all_windows.write(); - let index = lock - .iter() - .position(|handle| handle.as_raw() == target_window) - .unwrap(); - lock.remove(index); - - lock.is_empty() - } - - #[inline] - fn run_foreground_task(&self) -> Option { - const MAIN_TASK_TIMEOUT: u128 = 10; - - let start = std::time::Instant::now(); - 'tasks: loop { - 'timeout_loop: loop { - if start.elapsed().as_millis() >= MAIN_TASK_TIMEOUT { - log::debug!("foreground task timeout reached"); - // we spent our budget on gpui tasks, we likely have a lot of work queued so drain system events first to stay responsive - // then quit out of foreground work to allow us to process other gpui events first before returning back to foreground task work - // if we don't we might not for example process window quit events - let mut msg = MSG::default(); - let process_message = |msg: &_| { - if translate_accelerator(msg).is_none() { - _ = unsafe { TranslateMessage(msg) }; - unsafe { DispatchMessageW(msg) }; - } - }; - let peek_msg = |msg: &mut _, msg_kind| unsafe { - PeekMessageW(msg, None, 0, 0, PM_REMOVE | msg_kind).as_bool() - }; - // We need to process a paint message here as otherwise we will re-enter `run_foreground_task` before painting if we have work remaining. - // The reason for this is that windows prefers custom application message processing over system messages. - if peek_msg(&mut msg, PM_QS_PAINT) { - process_message(&msg); - } - while peek_msg(&mut msg, PM_QS_INPUT) { - process_message(&msg); - } - // Allow the main loop to process other gpui events before going back into `run_foreground_task` - unsafe { - if let Err(_) = PostMessageW( - Some(self.dispatcher.platform_window_handle.as_raw()), - WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD, - WPARAM(self.validation_number), - LPARAM(0), - ) { - self.dispatcher.wake_posted.store(false, Ordering::Release); - }; - } - break 'tasks; - } - let mut main_receiver = self.main_receiver.clone(); - match main_receiver.try_pop() { - Ok(Some(runnable)) => WindowsDispatcher::execute_runnable(runnable), - _ => break 'timeout_loop, - } - } - - // Someone could enqueue a Runnable here. The flag is still true, so they will not PostMessage. - // We need to check for those Runnables after we clear the flag. - self.dispatcher.wake_posted.store(false, Ordering::Release); - let mut main_receiver = self.main_receiver.clone(); - match main_receiver.try_pop() { - Ok(Some(runnable)) => { - self.dispatcher.wake_posted.store(true, Ordering::Release); - - WindowsDispatcher::execute_runnable(runnable); - } - _ => break 'tasks, - } - } - - Some(0) - } - - fn handle_dock_action_event(&self, action_idx: usize) -> Option { - let Some(action) = self - .state - .jump_list - .borrow() - .dock_menus - .get(action_idx) - .map(|dock_menu| dock_menu.action.boxed_clone()) - else { - log::error!("Dock menu for index {action_idx} not found"); - return Some(1); - }; - self.with_callback( - |callbacks| &callbacks.app_menu_action, - |callback| callback(&*action), - ); - Some(0) - } - - fn handle_keyboard_layout_change(&self) -> Option { - self.with_callback( - |callbacks| &callbacks.keyboard_layout_change, - |callback| callback(), - ); - Some(0) - } - - fn handle_power_broadcast(&self, wparam: WPARAM) -> Option { - if wparam.0 as u32 == PBT_APMRESUMEAUTOMATIC { - self.with_callback(|callbacks| &callbacks.system_wake, |callback| callback()); - } - Some(1) - } - - fn handle_device_lost(&self, lparam: LPARAM) -> Option { - let directx_devices = lparam.0 as *const DirectXDevices; - let directx_devices = unsafe { &*directx_devices }; - self.state.directx_devices.borrow_mut().take(); - *self.state.directx_devices.borrow_mut() = Some(directx_devices.clone()); - - Some(0) - } -} - -impl Drop for WindowsPlatform { - fn drop(&mut self) { - unsafe { - if let Some(notification) = self.suspend_resume_notification.borrow_mut().take() { - // SAFETY: notification was returned by RegisterSuspendResumeNotification. - UnregisterSuspendResumeNotification(notification).log_err(); - } - DestroyWindow(self.handle) - .context("Destroying platform window") - .log_err(); - OleUninitialize(); - } - } -} - -pub(crate) struct WindowCreationInfo { - pub(crate) icon: HICON, - pub(crate) executor: ForegroundExecutor, - pub(crate) current_cursor: Option, - pub(crate) cursor_visible: Arc, - pub(crate) drop_target_helper: IDropTargetHelper, - pub(crate) validation_number: usize, - pub(crate) main_receiver: PriorityQueueReceiver, - pub(crate) platform_window_handle: HWND, - pub(crate) disable_direct_composition: bool, - pub(crate) directx_devices: DirectXDevices, - /// Flag to instruct the `VSyncProvider` thread to invalidate the directx devices - /// as resizing them has failed, causing us to have lost at least the render target. - pub(crate) invalidate_devices: Arc, - /// Shared with [`WindowsPlatformState::draw_coordinator`] and every other window. - pub(crate) draw_coordinator: Rc, -} - -struct PlatformWindowCreateContext { - inner: Option>>, - raw_window_handles: std::sync::Weak>>, - validation_number: usize, - main_sender: Option>, - main_receiver: Option>, - directx_devices: Option, - dispatcher: Option>, -} - -fn has_package_identity() -> bool { - let mut package_full_name_length = 0; - let result = unsafe { - windows::Win32::Storage::Packaging::Appx::GetCurrentPackageFullName( - &mut package_full_name_length, - None, - ) - }; - if result == ERROR_INSUFFICIENT_BUFFER { - true - } else if result == APPMODEL_ERROR_NO_PACKAGE { - false - } else { - log::warn!("failed to determine whether the process has package identity: {result:?}"); - false - } -} - -fn open_target(target: impl AsRef) -> Result<()> { - let target = target.as_ref(); - let ret = unsafe { - ShellExecuteW( - None, - windows::core::w!("open"), - &HSTRING::from(target), - None, - None, - SW_SHOWDEFAULT, - ) - }; - if ret.0 as isize <= 32 { - Err(anyhow::anyhow!( - "Unable to open target: {}", - std::io::Error::last_os_error() - )) - } else { - Ok(()) - } -} - -fn open_target_in_explorer(target: &Path) -> Result<()> { - let dir = target.parent().context("No parent folder found")?; - let desktop = unsafe { SHGetDesktopFolder()? }; - - let mut dir_item = std::ptr::null_mut(); - unsafe { - desktop.ParseDisplayName( - HWND::default(), - None, - &HSTRING::from(dir), - None, - &mut dir_item, - std::ptr::null_mut(), - )?; - } - - let mut file_item = std::ptr::null_mut(); - unsafe { - desktop.ParseDisplayName( - HWND::default(), - None, - &HSTRING::from(target), - None, - &mut file_item, - std::ptr::null_mut(), - )?; - } - - let highlight = [file_item as *const _]; - unsafe { SHOpenFolderAndSelectItems(dir_item as _, Some(&highlight), 0) }.or_else(|err| { - if err.code().0 == ERROR_FILE_NOT_FOUND.0 as i32 { - // On some systems, the above call mysteriously fails with "file not - // found" even though the file is there. In these cases, ShellExecute() - // seems to work as a fallback (although it won't select the file). - open_target(dir).context("Opening target parent folder") - } else { - Err(anyhow::anyhow!("Can not open target path: {}", err)) - } - }) -} - -fn file_open_dialog( - options: PathPromptOptions, - window: Option, -) -> Result>> { - let folder_dialog: IFileOpenDialog = - unsafe { CoCreateInstance(&FileOpenDialog, None, CLSCTX_ALL)? }; - - let mut dialog_options = FOS_FILEMUSTEXIST; - if options.multiple { - dialog_options |= FOS_ALLOWMULTISELECT; - } - if options.directories { - dialog_options |= FOS_PICKFOLDERS; - } - - unsafe { - folder_dialog.SetOptions(dialog_options)?; - - if let Some(prompt) = options.prompt { - let prompt: &str = &prompt; - folder_dialog.SetOkButtonLabel(&HSTRING::from(prompt))?; - } - - if folder_dialog.Show(window).is_err() { - // User cancelled - return Ok(None); - } - } - - let results = unsafe { folder_dialog.GetResults()? }; - let file_count = unsafe { results.GetCount()? }; - if file_count == 0 { - return Ok(None); - } - - let mut paths = Vec::with_capacity(file_count as usize); - for i in 0..file_count { - let item = unsafe { results.GetItemAt(i)? }; - let path = unsafe { item.GetDisplayName(SIGDN_FILESYSPATH)?.to_string()? }; - paths.push(PathBuf::from(path)); - } - - Ok(Some(paths)) -} - -fn file_save_dialog( - directory: PathBuf, - suggested_name: Option, - window: Option, -) -> Result> { - let dialog: IFileSaveDialog = unsafe { CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)? }; - if !directory.to_string_lossy().is_empty() - && let Some(full_path) = directory - .canonicalize() - .context("failed to canonicalize directory") - .log_err() - { - let full_path = dunce::simplified(&full_path); - let full_path_string = full_path.display().to_string(); - let path_item: IShellItem = - unsafe { SHCreateItemFromParsingName(&HSTRING::from(full_path_string), None)? }; - unsafe { - dialog - .SetFolder(&path_item) - .context("failed to set dialog folder") - .log_err() - }; - } - - if let Some(suggested_name) = suggested_name { - unsafe { - dialog - .SetFileName(&HSTRING::from(suggested_name)) - .context("failed to set file name") - .log_err() - }; - } - - unsafe { - dialog.SetFileTypes(&[Common::COMDLG_FILTERSPEC { - pszName: windows::core::w!("All files"), - pszSpec: windows::core::w!("*.*"), - }])?; - if dialog.Show(window).is_err() { - // User cancelled - return Ok(None); - } - } - let shell_item = unsafe { dialog.GetResult()? }; - let file_path_string = unsafe { - let pwstr = shell_item.GetDisplayName(SIGDN_FILESYSPATH)?; - let string = pwstr.to_string()?; - CoTaskMemFree(Some(pwstr.0 as _)); - string - }; - Ok(Some(PathBuf::from(file_path_string))) -} - -fn load_icon() -> Result { - let module = unsafe { GetModuleHandleW(None).context("unable to get module handle")? }; - let handle = unsafe { - LoadImageW( - Some(module.into()), - windows::core::PCWSTR(1 as _), - IMAGE_ICON, - 0, - 0, - LR_DEFAULTSIZE | LR_SHARED, - ) - .context("unable to load icon file")? - }; - Ok(HICON(handle.0)) -} - -#[inline] -fn should_auto_hide_scrollbars() -> Result { - let ui_settings = UISettings::new()?; - Ok(ui_settings.AutoHideScrollBars()?) -} - -fn check_device_lost(device: &ID3D11Device) -> bool { - let device_state = unsafe { device.GetDeviceRemovedReason() }; - match device_state { - Ok(_) => false, - Err(err) => { - log::error!("DirectX device lost detected: {:?}", err); - true - } - } -} - -fn handle_gpu_device_lost( - directx_devices: &mut DirectXDevices, - platform_window: HWND, - validation_number: usize, - all_windows: &std::sync::Weak>>, - text_system: &std::sync::Weak, -) -> Result<()> { - // Here we wait a bit to ensure the system has time to recover from the device lost state. - // If we don't wait, the final drawing result will be blank. - std::thread::sleep(std::time::Duration::from_millis(350)); - - *directx_devices = try_to_recover_from_device_lost(|| { - DirectXDevices::new().context("Failed to recreate new DirectX devices after device lost") - })?; - log::info!("DirectX devices successfully recreated."); - - let lparam = LPARAM(directx_devices as *const _ as _); - unsafe { - SendMessageW( - platform_window, - WM_GPUI_GPU_DEVICE_LOST, - Some(WPARAM(validation_number)), - Some(lparam), - ); - } - - if let Some(text_system) = text_system.upgrade() { - text_system.handle_gpu_lost(&directx_devices)?; - } - if let Some(all_windows) = all_windows.upgrade() { - for window in all_windows.read().iter() { - unsafe { - SendMessageW( - window.as_raw(), - WM_GPUI_GPU_DEVICE_LOST, - Some(WPARAM(validation_number)), - Some(lparam), - ); - } - } - std::thread::sleep(std::time::Duration::from_millis(200)); - for window in all_windows.read().iter() { - unsafe { - SendMessageW( - window.as_raw(), - WM_GPUI_FORCE_UPDATE_WINDOW, - Some(WPARAM(validation_number)), - None, - ); - } - } - } - Ok(()) -} - -const PLATFORM_WINDOW_CLASS_NAME: PCWSTR = w!("Zed::PlatformWindow"); - -fn register_platform_window_class() { - let wc = WNDCLASSW { - lpfnWndProc: Some(window_procedure), - lpszClassName: PCWSTR(PLATFORM_WINDOW_CLASS_NAME.as_ptr()), - ..Default::default() - }; - unsafe { RegisterClassW(&wc) }; -} - -unsafe extern "system" fn window_procedure( - hwnd: HWND, - msg: u32, - wparam: WPARAM, - lparam: LPARAM, -) -> LRESULT { - if msg == WM_NCCREATE { - let params = unsafe { &*(lparam.0 as *const CREATESTRUCTW) }; - let creation_context = params.lpCreateParams as *mut PlatformWindowCreateContext; - let creation_context = unsafe { &mut *creation_context }; - - let Some(main_sender) = creation_context.main_sender.take() else { - creation_context.inner = Some(Err(anyhow!("missing main sender"))); - return LRESULT(0); - }; - creation_context.dispatcher = Some(Arc::new(WindowsDispatcher::new( - main_sender, - hwnd, - creation_context.validation_number, - ))); - - return match WindowsPlatformInner::new(creation_context) { - Ok(inner) => { - let weak = Box::new(Rc::downgrade(&inner)); - unsafe { set_window_long(hwnd, GWLP_USERDATA, Box::into_raw(weak) as isize) }; - creation_context.inner = Some(Ok(inner)); - unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) } - } - Err(error) => { - creation_context.inner = Some(Err(error)); - LRESULT(0) - } - }; - } - - let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak; - if ptr.is_null() { - return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }; - } - let inner = unsafe { &*ptr }; - let result = if let Some(inner) = inner.upgrade() { - if cfg!(debug_assertions) { - let inner = std::panic::AssertUnwindSafe(inner); - match std::panic::catch_unwind(|| { inner }.handle_msg(hwnd, msg, wparam, lparam)) { - Ok(result) => result, - Err(_) => std::process::abort(), - } - } else { - inner.handle_msg(hwnd, msg, wparam, lparam) - } - } else { - unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) } - }; - - if msg == WM_NCDESTROY { - unsafe { set_window_long(hwnd, GWLP_USERDATA, 0) }; - unsafe { drop(Box::from_raw(ptr)) }; - } - - result -} - -#[cfg(test)] -mod tests { - use std::ffi::{OsStr, OsString}; - - use crate::{read_from_clipboard, write_to_clipboard}; - use gpui::ClipboardItem; - - use super::encode_restart_arguments; - - #[test] - fn test_encode_restart_arguments() { - assert_eq!(encode_restart_arguments(&[]), OsStr::new("")); - assert_eq!( - encode_restart_arguments(&[ - OsString::from("--user-data-dir"), - OsString::from(r"C:\Zed Data"), - ]), - OsStr::new(r#""--user-data-dir" "C:\Zed Data""#) - ); - assert_eq!( - encode_restart_arguments(&[OsString::from(r"C:\")]), - OsStr::new(r#""C:\\""#) - ); - } - - #[test] - fn test_clipboard() { - let item = ClipboardItem::new_string("你好,我是张小白".to_string()); - write_to_clipboard(item.clone()); - assert_eq!(read_from_clipboard(), Some(item)); - - let item = ClipboardItem::new_string("12345".to_string()); - write_to_clipboard(item.clone()); - assert_eq!(read_from_clipboard(), Some(item)); - - let item = ClipboardItem::new_string_with_json_metadata("abcdef".to_string(), vec![3, 4]); - write_to_clipboard(item.clone()); - assert_eq!(read_from_clipboard(), Some(item)); - } -} diff --git a/crates/gpui_pre_windows/src/shaders.hlsl b/crates/gpui_pre_windows/src/shaders.hlsl deleted file mode 100644 index 4e9089d..0000000 --- a/crates/gpui_pre_windows/src/shaders.hlsl +++ /dev/null @@ -1,1379 +0,0 @@ -#include "alpha_correction.hlsl" - -cbuffer GlobalParams: register(b0) { - float4 gamma_ratios; - float2 global_viewport_size; - float grayscale_enhanced_contrast; - float subpixel_enhanced_contrast; - uint is_bgr; - uint3 global_pad; -}; - -cbuffer BatchParams: register(b1) { - uint batch_start_index; - uint3 batch_pad; -}; - -Texture2D t_sprite: register(t0); -SamplerState s_sprite: register(s0); - -struct SubpixelSpriteFragmentOutput { - float4 foreground : SV_Target0; - float4 alpha : SV_Target1; -}; - -struct Bounds { - float2 origin; - float2 size; -}; - -struct Corners { - float top_left; - float top_right; - float bottom_right; - float bottom_left; -}; - -struct ContentMask { - Bounds bounds; - Corners corner_radii; - uint clip_index; - uint clip_padding; -}; - -struct RoundedClip { - Bounds bounds; - Corners radii_x; - Corners radii_y; - uint parent; - uint padding; -}; -StructuredBuffer rounded_clips: register(t2); - -struct Edges { - float top; - float right; - float bottom; - float left; -}; - -struct Hsla { - float h; - float s; - float l; - float a; -}; - -struct LinearColorStop { - Hsla color; - float percentage; -}; - -struct Background { - // 0u is Solid - // 1u is LinearGradient - // 2u is PatternSlash - uint tag; - // 0u is sRGB linear color - // 1u is Oklab color - uint color_space; - Hsla solid; - float gradient_angle_or_pattern_height; - LinearColorStop colors[2]; - uint pad; -}; - -struct GradientColor { - float4 solid; - float4 color0; - float4 color1; -}; - -struct AtlasTextureId { - uint index; - uint kind; -}; - -struct AtlasBounds { - int2 origin; - int2 size; -}; - -struct AtlasTile { - AtlasTextureId texture_id; - uint tile_id; - uint padding; - AtlasBounds bounds; -}; - -struct TransformationMatrix { - float2x2 rotation_scale; - float2 translation; -}; - -static const float M_PI_F = 3.141592653f; -static const float3 GRAYSCALE_FACTORS = float3(0.2126f, 0.7152f, 0.0722f); - -float4 to_device_position_impl(float2 position) { - float2 device_position = position / global_viewport_size * float2(2.0, -2.0) + float2(-1.0, 1.0); - return float4(device_position, 0., 1.); -} - -float4 to_device_position(float2 unit_vertex, Bounds bounds) { - float2 position = unit_vertex * bounds.size + bounds.origin; - return to_device_position_impl(position); -} - -float4 distance_from_clip_rect_impl(float2 position, ContentMask mask) { - float2 tl = position - mask.bounds.origin; - float2 br = mask.bounds.origin + mask.bounds.size - position; - return float4(tl.x, br.x, tl.y, br.y); -} - -// Path sprites are rasterized into an intermediate texture with their mask -// already applied; this overload keeps their existing rectangular clip. -float4 distance_from_clip_rect_impl(float2 position, Bounds clip_bounds) { - float2 tl = position - clip_bounds.origin; - float2 br = clip_bounds.origin + clip_bounds.size - position; - return float4(tl.x, br.x, tl.y, br.y); -} - -float4 distance_from_clip_rect(float2 unit_vertex, Bounds bounds, ContentMask mask) { - float2 position = unit_vertex * bounds.size + bounds.origin; - return distance_from_clip_rect_impl(position, mask); -} - -float4 distance_from_clip_rect_transformed(float2 unit_vertex, Bounds bounds, ContentMask mask, TransformationMatrix transformation) { - float2 position = unit_vertex * bounds.size + bounds.origin; - float2 transformed = mul(position, transformation.rotation_scale) + transformation.translation; - return distance_from_clip_rect_impl(transformed, mask); -} - -float clip_corner_distance(float2 point, float2 radii) { - if (any(radii <= float2(0.0, 0.0)) || any(point >= radii)) return -1e20; - float2 p = point - radii; - float k0 = length(p / radii); - float k1 = length(p / (radii * radii)); - if (k1 == 0.0) return -min(radii.x, radii.y); - return k0 * (k0 - 1.0) / k1; -} - -float rounded_clip_distance(float2 position, RoundedClip clip) { - float2 tl = position - clip.bounds.origin; - float2 br = clip.bounds.size - tl; - float distance = max(max(-tl.x, -tl.y), max(-br.x, -br.y)); - distance = max(distance, clip_corner_distance(tl, float2(clip.radii_x.top_left, clip.radii_y.top_left))); - distance = max(distance, clip_corner_distance(float2(br.x, tl.y), float2(clip.radii_x.top_right, clip.radii_y.top_right))); - distance = max(distance, clip_corner_distance(br, float2(clip.radii_x.bottom_right, clip.radii_y.bottom_right))); - distance = max(distance, clip_corner_distance(float2(tl.x, br.y), float2(clip.radii_x.bottom_left, clip.radii_y.bottom_left))); - return distance; -} - -float content_mask_coverage(float2 position, ContentMask mask) { - float distance = -1e20; - if (mask.clip_index == 0) { - RoundedClip clip; - clip.bounds = mask.bounds; - clip.radii_x = mask.corner_radii; - clip.radii_y = mask.corner_radii; - distance = rounded_clip_distance(position, clip); - } else { - uint index = mask.clip_index; - do { - RoundedClip clip = rounded_clips[index - 1]; - distance = max(distance, rounded_clip_distance(position, clip)); - index = clip.parent; - } while (index != 0); - } - return saturate(0.5 - distance); -} - -// Convert linear RGB to sRGB -float3 linear_to_srgb(float3 color) { - return pow(color, float3(2.2, 2.2, 2.2)); -} - -// Convert sRGB to linear RGB -float3 srgb_to_linear(float3 color) { - return pow(color, float3(1.0 / 2.2, 1.0 / 2.2, 1.0 / 2.2)); -} - -/// Hsla to linear RGBA conversion. -float4 hsla_to_rgba(Hsla hsla) { - float h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range - float s = hsla.s; - float l = hsla.l; - float a = hsla.a; - - float c = (1.0 - abs(2.0 * l - 1.0)) * s; - float x = c * (1.0 - abs(fmod(h, 2.0) - 1.0)); - float m = l - c / 2.0; - - float r = 0.0; - float g = 0.0; - float b = 0.0; - - if (h >= 0.0 && h < 1.0) { - r = c; - g = x; - b = 0.0; - } else if (h >= 1.0 && h < 2.0) { - r = x; - g = c; - b = 0.0; - } else if (h >= 2.0 && h < 3.0) { - r = 0.0; - g = c; - b = x; - } else if (h >= 3.0 && h < 4.0) { - r = 0.0; - g = x; - b = c; - } else if (h >= 4.0 && h < 5.0) { - r = x; - g = 0.0; - b = c; - } else { - r = c; - g = 0.0; - b = x; - } - - float4 rgba; - rgba.x = (r + m); - rgba.y = (g + m); - rgba.z = (b + m); - rgba.w = a; - return rgba; -} - -// Converts a sRGB color to the Oklab color space. -// Reference: https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab -float4 srgb_to_oklab(float4 color) { - // Convert non-linear sRGB to linear sRGB - color = float4(srgb_to_linear(color.rgb), color.a); - - float l = 0.4122214708 * color.r + 0.5363325363 * color.g + 0.0514459929 * color.b; - float m = 0.2119034982 * color.r + 0.6806995451 * color.g + 0.1073969566 * color.b; - float s = 0.0883024619 * color.r + 0.2817188376 * color.g + 0.6299787005 * color.b; - - float l_ = pow(l, 1.0/3.0); - float m_ = pow(m, 1.0/3.0); - float s_ = pow(s, 1.0/3.0); - - return float4( - 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_, - 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_, - 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_, - color.a - ); -} - -// Converts an Oklab color to the sRGB color space. -float4 oklab_to_srgb(float4 color) { - float l_ = color.r + 0.3963377774 * color.g + 0.2158037573 * color.b; - float m_ = color.r - 0.1055613458 * color.g - 0.0638541728 * color.b; - float s_ = color.r - 0.0894841775 * color.g - 1.2914855480 * color.b; - - float l = l_ * l_ * l_; - float m = m_ * m_ * m_; - float s = s_ * s_ * s_; - - float3 linear_rgb = float3( - 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, - -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, - -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s - ); - - // Convert linear sRGB to non-linear sRGB - return float4(linear_to_srgb(linear_rgb), color.a); -} - -// This approximates the error function, needed for the gaussian integral -float2 erf(float2 x) { - float2 s = sign(x); - float2 a = abs(x); - x = 1. + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a; - x *= x; - return s - s / (x * x); -} - -float blur_along_x(float x, float y, float sigma, float corner, float2 half_size) { - float delta = min(half_size.y - corner - abs(y), 0.); - float curved = half_size.x - corner + sqrt(max(0., corner * corner - delta * delta)); - float2 integral = 0.5 + 0.5 * erf((x + float2(-curved, curved)) * (sqrt(0.5) / sigma)); - return integral.y - integral.x; -} - -// A standard gaussian function, used for weighting samples -float gaussian(float x, float sigma) { - return exp(-(x * x) / (2. * sigma * sigma)) / (sqrt(2. * M_PI_F) * sigma); -} - -float4 over(float4 below, float4 above) { - float4 result; - float alpha = above.a + below.a * (1.0 - above.a); - result.rgb = (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha; - result.a = alpha; - return result; -} - -float2 to_tile_position(float2 unit_vertex, AtlasTile tile) { - float2 atlas_size; - t_sprite.GetDimensions(atlas_size.x, atlas_size.y); - return (float2(tile.bounds.origin) + unit_vertex * float2(tile.bounds.size)) / atlas_size; -} - -// Selects corner radius based on quadrant. -float pick_corner_radius(float2 center_to_point, Corners corner_radii) { - if (center_to_point.x < 0.) { - if (center_to_point.y < 0.) { - return corner_radii.top_left; - } else { - return corner_radii.bottom_left; - } - } else { - if (center_to_point.y < 0.) { - return corner_radii.top_right; - } else { - return corner_radii.bottom_right; - } - } -} - -float4 to_device_position_transformed(float2 unit_vertex, Bounds bounds, - TransformationMatrix transformation) { - float2 position = unit_vertex * bounds.size + bounds.origin; - float2 transformed = mul(position, transformation.rotation_scale) + transformation.translation; - float2 device_position = transformed / global_viewport_size * float2(2.0, -2.0) + float2(-1.0, 1.0); - return float4(device_position, 0.0, 1.0); -} - -// Implementation of quad signed distance field -float quad_sdf_impl(float2 corner_center_to_point, float corner_radius) { - if (corner_radius == 0.0) { - // Fast path for unrounded corners - return max(corner_center_to_point.x, corner_center_to_point.y); - } else { - // Signed distance of the point from a quad that is inset by corner_radius - // It is negative inside this quad, and positive outside - float signed_distance_to_inset_quad = - // 0 inside the inset quad, and positive outside - length(max(float2(0.0, 0.0), corner_center_to_point)) + - // 0 outside the inset quad, and negative inside - min(0.0, max(corner_center_to_point.x, corner_center_to_point.y)); - - return signed_distance_to_inset_quad - corner_radius; - } -} - -float quad_sdf(float2 pt, Bounds bounds, Corners corner_radii) { - float2 half_size = bounds.size / 2.; - float2 center = bounds.origin + half_size; - float2 center_to_point = pt - center; - float corner_radius = pick_corner_radius(center_to_point, corner_radii); - float2 corner_to_point = abs(center_to_point) - half_size; - float2 corner_center_to_point = corner_to_point + corner_radius; - return quad_sdf_impl(corner_center_to_point, corner_radius); -} - -GradientColor prepare_gradient_color(uint tag, uint color_space, Hsla solid, LinearColorStop colors[2]) { - GradientColor output; - if (tag == 0 || tag == 2 || tag == 3) { - output.solid = hsla_to_rgba(solid); - } else if (tag == 1) { - output.color0 = hsla_to_rgba(colors[0].color); - output.color1 = hsla_to_rgba(colors[1].color); - - // Prepare color space in vertex for avoid conversion - // in fragment shader for performance reasons - if (color_space == 1) { - // Oklab - output.color0 = srgb_to_oklab(output.color0); - output.color1 = srgb_to_oklab(output.color1); - } - } - - return output; -} - -float2x2 rotate2d(float angle) { - float s = sin(angle); - float c = cos(angle); - return float2x2(c, -s, s, c); -} - -float4 gradient_color(Background background, - float2 position, - Bounds bounds, - float4 solid_color, float4 color0, float4 color1) { - float4 color; - - switch (background.tag) { - case 0: - color = solid_color; - break; - case 1: { - // -90 degrees to match the CSS gradient angle. - float gradient_angle = background.gradient_angle_or_pattern_height; - float radians = (fmod(gradient_angle, 360.0) - 90.0) * (M_PI_F / 180.0); - float2 direction = float2(cos(radians), sin(radians)); - - // Expand the short side to be the same as the long side - if (bounds.size.x > bounds.size.y) { - direction.y *= bounds.size.y / bounds.size.x; - } else { - direction.x *= bounds.size.x / bounds.size.y; - } - - // Get the t value for the linear gradient with the color stop percentages. - float2 half_size = bounds.size * 0.5; - float2 center = bounds.origin + half_size; - float2 center_to_point = position - center; - float t = dot(center_to_point, direction) / length(direction); - // Check the direct to determine the use x or y - if (abs(direction.x) > abs(direction.y)) { - t = (t + half_size.x) / bounds.size.x; - } else { - t = (t + half_size.y) / bounds.size.y; - } - - // Adjust t based on the stop percentages - t = (t - background.colors[0].percentage) - / (background.colors[1].percentage - - background.colors[0].percentage); - t = clamp(t, 0.0, 1.0); - - switch (background.color_space) { - case 0: - color = lerp(color0, color1, t); - break; - case 1: { - float4 oklab_color = lerp(color0, color1, t); - color = oklab_to_srgb(oklab_color); - break; - } - } - - // Dither to reduce banding in gradients (especially dark/alpha). - // Triangular-distributed noise breaks up 8-bit quantization steps. - // ±2/255 for RGB (enough for dark-on-dark compositing), - // ±3/255 for alpha (needs more because alpha × dark color = tiny steps). - { - float2 seed = position * 0.6180339887; // golden ratio spread - float r1 = frac(sin(dot(seed, float2(12.9898, 78.233))) * 43758.5453); - float r2 = frac(sin(dot(seed, float2(39.3460, 11.135))) * 24634.6345); - float tri = r1 + r2 - 1.0; // triangular PDF, range [-1, +1] - color.rgb += tri * 2.0 / 255.0; - color.a += tri * 3.0 / 255.0; - } - - break; - } - case 2: { - float gradient_angle_or_pattern_height = background.gradient_angle_or_pattern_height; - float pattern_width = (gradient_angle_or_pattern_height / 65535.0f) / 255.0f; - float pattern_interval = fmod(gradient_angle_or_pattern_height, 65535.0f) / 255.0f; - float pattern_height = pattern_width + pattern_interval; - float stripe_angle = M_PI_F / 4.0; - float pattern_period = pattern_height * sin(stripe_angle); - float2x2 rotation = rotate2d(stripe_angle); - float2 relative_position = position - bounds.origin; - float2 rotated_point = mul(relative_position, rotation); - float pattern = fmod(rotated_point.x, pattern_period); - float distance = min(pattern, pattern_period - pattern) - pattern_period * (pattern_width / pattern_height) / 2.0f; - color = solid_color; - color.a *= saturate(0.5 - distance); - break; - } - case 3: { - // checkerboard - float size = background.gradient_angle_or_pattern_height; - float2 relative_position = position - bounds.origin; - - float x_index = floor(relative_position.x / size); - float y_index = floor(relative_position.y / size); - float should_be_colored = (x_index + y_index) % 2.0; - - color = solid_color; - color.a *= saturate(should_be_colored); - break; - } - } - - return color; -} - -// Returns the dash velocity of a corner given the dash velocity of the two -// sides, by returning the slower velocity (larger dashes). -// -// Since 0 is used for dash velocity when the border width is 0 (instead of -// +inf), this returns the other dash velocity in that case. -// -// An alternative to this might be to appropriately interpolate the dash -// velocity around the corner, but that seems overcomplicated. -float corner_dash_velocity(float dv1, float dv2) { - if (dv1 == 0.0) { - return dv2; - } else if (dv2 == 0.0) { - return dv1; - } else { - return min(dv1, dv2); - } -} - -// Returns alpha used to render antialiased dashes. -// `t` is within the dash when `fmod(t, period) < length`. -float dash_alpha( - float t, float period, float length, float dash_velocity, - float antialias_threshold -) { - float half_period = period / 2.0; - float half_length = length / 2.0; - // Value in [-half_period, half_period] - // The dash is in [-half_length, half_length] - float centered = fmod(t + half_period - half_length, period) - half_period; - // Signed distance for the dash, negative values are inside the dash - float signed_distance = abs(centered) - half_length; - // Antialiased alpha based on the signed distance - return saturate(antialias_threshold - signed_distance / dash_velocity); -} - -// This approximates distance to the nearest point to a quarter ellipse in a way -// that is sufficient for anti-aliasing when the ellipse is not very eccentric. -// The components of `point` are expected to be positive. -// -// Negative on the outside and positive on the inside. -float quarter_ellipse_sdf(float2 pt, float2 radii) { - // Scale the space to treat the ellipse like a unit circle - float2 circle_vec = pt / radii; - float unit_circle_sdf = length(circle_vec) - 1.0; - // Approximate up-scaling of the length by using the average of the radii. - // - // TODO: A better solution would be to use the gradient of the implicit - // function for an ellipse to approximate a scaling factor. - return unit_circle_sdf * (radii.x + radii.y) * -0.5; -} - -/* -** -** Quads -** -*/ - -struct Quad { - uint order; - uint border_style; - Bounds bounds; - ContentMask content_mask; - Background background; - Hsla border_color; - Corners corner_radii; - Edges border_widths; -}; - -struct QuadVertexOutput { - nointerpolation uint quad_id: TEXCOORD0; - float4 position: SV_Position; - nointerpolation float4 border_color: COLOR0; - nointerpolation float4 background_solid: COLOR1; - nointerpolation float4 background_color0: COLOR2; - nointerpolation float4 background_color1: COLOR3; - nointerpolation float4 clip_mask_bounds: COLOR4; - nointerpolation float4 clip_mask_radii: COLOR5; - float4 clip_distance: SV_ClipDistance; -}; - -struct QuadFragmentInput { - nointerpolation uint quad_id: TEXCOORD0; - float4 position: SV_Position; - nointerpolation float4 border_color: COLOR0; - nointerpolation float4 background_solid: COLOR1; - nointerpolation float4 background_color0: COLOR2; - nointerpolation float4 background_color1: COLOR3; - nointerpolation float4 clip_mask_bounds: COLOR4; - nointerpolation float4 clip_mask_radii: COLOR5; -}; - -StructuredBuffer quads: register(t1); - -QuadVertexOutput quad_vertex(uint vertex_id: SV_VertexID, uint instance_id: SV_InstanceID) { - float2 unit_vertex = float2(float(vertex_id & 1u), 0.5 * float(vertex_id & 2u)); - uint quad_id = batch_start_index + instance_id; - Quad quad = quads[quad_id]; - float4 device_position = to_device_position(unit_vertex, quad.bounds); - - GradientColor gradient = prepare_gradient_color( - quad.background.tag, - quad.background.color_space, - quad.background.solid, - quad.background.colors - ); - float4 clip_distance = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask); - float4 border_color = hsla_to_rgba(quad.border_color); - - QuadVertexOutput output; - output.position = device_position; - output.border_color = border_color; - output.quad_id = quad_id; - output.background_solid = gradient.solid; - output.background_color0 = gradient.color0; - output.background_color1 = gradient.color1; - output.clip_mask_bounds = float4( - quad.content_mask.bounds.origin, quad.content_mask.bounds.size); - output.clip_mask_radii = float4( - quad.content_mask.corner_radii.top_left, - quad.content_mask.corner_radii.top_right, - quad.content_mask.corner_radii.bottom_right, - quad.content_mask.corner_radii.bottom_left); - output.clip_distance = clip_distance; - return output; -} - -float4 quad_fragment(QuadFragmentInput input): SV_Target { - Quad quad = quads[input.quad_id]; - float coverage = content_mask_coverage(input.position.xy, quad.content_mask); - if (coverage <= 0.0) { - return float4(0.0, 0.0, 0.0, 0.0); - } - float4 background_color = gradient_color(quad.background, input.position.xy, quad.bounds, - input.background_solid, input.background_color0, input.background_color1); - - bool unrounded = quad.corner_radii.top_left == 0.0 && - quad.corner_radii.top_right == 0.0 && - quad.corner_radii.bottom_left == 0.0 && - quad.corner_radii.bottom_right == 0.0; - - // Fast path when the quad is not rounded and doesn't have any border - if (quad.border_widths.top == 0.0 && - quad.border_widths.left == 0.0 && - quad.border_widths.right == 0.0 && - quad.border_widths.bottom == 0.0 && - unrounded) { - return background_color * float4(1.0, 1.0, 1.0, coverage); - } - - float2 size = quad.bounds.size; - float2 half_size = size / 2.; - float2 the_point = input.position.xy - quad.bounds.origin; - float2 center_to_point = the_point - half_size; - - // Signed distance field threshold for inclusion of pixels. 0.5 is the - // minimum distance between the center of the pixel and the edge. - const float antialias_threshold = 0.5; - - // Radius of the nearest corner - float corner_radius = pick_corner_radius(center_to_point, quad.corner_radii); - - float2 border = float2( - center_to_point.x < 0.0 ? quad.border_widths.left : quad.border_widths.right, - center_to_point.y < 0.0 ? quad.border_widths.top : quad.border_widths.bottom - ); - - // 0-width borders are reduced so that `inner_sdf >= antialias_threshold`. - // The purpose of this is to not draw antialiasing pixels in this case. - float2 reduced_border = float2( - border.x == 0.0 ? -antialias_threshold : border.x, - border.y == 0.0 ? -antialias_threshold : border.y - ); - - // Vector from the corner of the quad bounds to the point, after mirroring - // the point into the bottom right quadrant. Both components are <= 0. - float2 corner_to_point = abs(center_to_point) - half_size; - - // Vector from the point to the center of the rounded corner's circle, also - // mirrored into bottom right quadrant. - float2 corner_center_to_point = corner_to_point + corner_radius; - - // Whether the nearest point on the border is rounded - bool is_near_rounded_corner = - corner_center_to_point.x >= 0.0 && - corner_center_to_point.y >= 0.0; - - // Vector from straight border inner corner to point. - // - // 0-width borders are turned into width -1 so that inner_sdf is > 1.0 near - // the border. Without this, antialiasing pixels would be drawn. - float2 straight_border_inner_corner_to_point = corner_to_point + reduced_border; - - // Whether the point is beyond the inner edge of the straight border - bool is_beyond_inner_straight_border = - straight_border_inner_corner_to_point.x > 0.0 || - straight_border_inner_corner_to_point.y > 0.0; - - // Whether the point is far enough inside the quad, such that the pixels are - // not affected by the straight border. - bool is_within_inner_straight_border = - straight_border_inner_corner_to_point.x < -antialias_threshold && - straight_border_inner_corner_to_point.y < -antialias_threshold; - - // Fast path for points that must be part of the background - if (is_within_inner_straight_border && !is_near_rounded_corner) { - return background_color * float4(1.0, 1.0, 1.0, coverage); - } - - // Signed distance of the point to the outside edge of the quad's border - float outer_sdf = quad_sdf_impl(corner_center_to_point, corner_radius); - - // Approximate signed distance of the point to the inside edge of the quad's - // border. It is negative outside this edge (within the border), and - // positive inside. - // - // This is not always an accurate signed distance: - // * The rounded portions with varying border width use an approximation of - // nearest-point-on-ellipse. - // * When it is quickly known to be outside the edge, -1.0 is used. - float inner_sdf = 0.0; - if (corner_center_to_point.x <= 0.0 || corner_center_to_point.y <= 0.0) { - // Fast paths for straight borders - inner_sdf = -max(straight_border_inner_corner_to_point.x, - straight_border_inner_corner_to_point.y); - } else if (is_beyond_inner_straight_border) { - // Fast path for points that must be outside the inner edge - inner_sdf = -1.0; - } else if (reduced_border.x == reduced_border.y) { - // Fast path for circular inner edge. - inner_sdf = -(outer_sdf + reduced_border.x); - } else { - float2 ellipse_radii = max(float2(0.0, 0.0), float2(corner_radius, corner_radius) - reduced_border); - inner_sdf = quarter_ellipse_sdf(corner_center_to_point, ellipse_radii); - } - - // Negative when inside the border - float border_sdf = max(inner_sdf, outer_sdf); - - float4 color = background_color; - if (border_sdf < antialias_threshold) { - float4 border_color = input.border_color; - // Dashed border logic when border_style == 1 - if (quad.border_style == 1) { - // Position along the perimeter in "dash space", where each dash - // period has length 1 - float t = 0.0; - - // Total number of dash periods, so that the dash spacing can be - // adjusted to evenly divide it - float max_t = 0.0; - - // Border width is proportional to dash size. This is the behavior - // used by browsers, but also avoids dashes from different segments - // overlapping when dash size is smaller than the border width. - // - // Dash pattern: (2 * border width) dash, (1 * border width) gap - const float dash_length_per_width = 2.0; - const float dash_gap_per_width = 1.0; - const float dash_period_per_width = dash_length_per_width + dash_gap_per_width; - - // Since the dash size is determined by border width, the density of - // dashes varies. Multiplying a pixel distance by this returns a - // position in dash space - it has units (dash period / pixels). So - // a dash velocity of (1 / 10) is 1 dash every 10 pixels. - float dash_velocity = 0.0; - - // Dividing this by the border width gives the dash velocity - const float dv_numerator = 1.0 / dash_period_per_width; - - if (unrounded) { - // When corners aren't rounded, the dashes are separately laid - // out on each straight line, rather than around the whole - // perimeter. This way each line starts and ends with a dash. - bool is_horizontal = corner_center_to_point.x < corner_center_to_point.y; - // Choosing the right border width for dashed borders. - // TODO: A better solution exists taking a look at the whole file. - // this does not fix single dashed borders at the corners - float2 dashed_border = float2( - max(quad.border_widths.bottom, quad.border_widths.top), - max(quad.border_widths.right, quad.border_widths.left) - ); - float border_width = is_horizontal ? dashed_border.x : dashed_border.y; - dash_velocity = dv_numerator / border_width; - t = is_horizontal ? the_point.x : the_point.y; - t *= dash_velocity; - max_t = is_horizontal ? size.x : size.y; - max_t *= dash_velocity; - } else { - // When corners are rounded, the dashes are laid out clockwise - // around the whole perimeter. - - float r_tr = quad.corner_radii.top_right; - float r_br = quad.corner_radii.bottom_right; - float r_bl = quad.corner_radii.bottom_left; - float r_tl = quad.corner_radii.top_left; - - float w_t = quad.border_widths.top; - float w_r = quad.border_widths.right; - float w_b = quad.border_widths.bottom; - float w_l = quad.border_widths.left; - - // Straight side dash velocities - float dv_t = w_t <= 0.0 ? 0.0 : dv_numerator / w_t; - float dv_r = w_r <= 0.0 ? 0.0 : dv_numerator / w_r; - float dv_b = w_b <= 0.0 ? 0.0 : dv_numerator / w_b; - float dv_l = w_l <= 0.0 ? 0.0 : dv_numerator / w_l; - - // Straight side lengths in dash space - float s_t = (size.x - r_tl - r_tr) * dv_t; - float s_r = (size.y - r_tr - r_br) * dv_r; - float s_b = (size.x - r_br - r_bl) * dv_b; - float s_l = (size.y - r_bl - r_tl) * dv_l; - - float corner_dash_velocity_tr = corner_dash_velocity(dv_t, dv_r); - float corner_dash_velocity_br = corner_dash_velocity(dv_b, dv_r); - float corner_dash_velocity_bl = corner_dash_velocity(dv_b, dv_l); - float corner_dash_velocity_tl = corner_dash_velocity(dv_t, dv_l); - - // Corner lengths in dash space - float c_tr = r_tr * (M_PI_F / 2.0) * corner_dash_velocity_tr; - float c_br = r_br * (M_PI_F / 2.0) * corner_dash_velocity_br; - float c_bl = r_bl * (M_PI_F / 2.0) * corner_dash_velocity_bl; - float c_tl = r_tl * (M_PI_F / 2.0) * corner_dash_velocity_tl; - - // Cumulative dash space upto each segment - float upto_tr = s_t; - float upto_r = upto_tr + c_tr; - float upto_br = upto_r + s_r; - float upto_b = upto_br + c_br; - float upto_bl = upto_b + s_b; - float upto_l = upto_bl + c_bl; - float upto_tl = upto_l + s_l; - max_t = upto_tl + c_tl; - - if (is_near_rounded_corner) { - float radians = atan2(corner_center_to_point.y, corner_center_to_point.x); - float corner_t = radians * corner_radius; - - if (center_to_point.x >= 0.0) { - if (center_to_point.y < 0.0) { - dash_velocity = corner_dash_velocity_tr; - // Subtracted because radians is pi/2 to 0 when - // going clockwise around the top right corner, - // since the y axis has been flipped - t = upto_r - corner_t * dash_velocity; - } else { - dash_velocity = corner_dash_velocity_br; - // Added because radians is 0 to pi/2 when going - // clockwise around the bottom-right corner - t = upto_br + corner_t * dash_velocity; - } - } else { - if (center_to_point.y >= 0.0) { - dash_velocity = corner_dash_velocity_bl; - // Subtracted because radians is pi/1 to 0 when - // going clockwise around the bottom-left corner, - // since the x axis has been flipped - t = upto_l - corner_t * dash_velocity; - } else { - dash_velocity = corner_dash_velocity_tl; - // Added because radians is 0 to pi/2 when going - // clockwise around the top-left corner, since both - // axis were flipped - t = upto_tl + corner_t * dash_velocity; - } - } - } else { - // Straight borders - bool is_horizontal = corner_center_to_point.x < corner_center_to_point.y; - if (is_horizontal) { - if (center_to_point.y < 0.0) { - dash_velocity = dv_t; - t = (the_point.x - r_tl) * dash_velocity; - } else { - dash_velocity = dv_b; - t = upto_bl - (the_point.x - r_bl) * dash_velocity; - } - } else { - if (center_to_point.x < 0.0) { - dash_velocity = dv_l; - t = upto_tl - (the_point.y - r_tl) * dash_velocity; - } else { - dash_velocity = dv_r; - t = upto_r + (the_point.y - r_tr) * dash_velocity; - } - } - } - } - float dash_length = dash_length_per_width / dash_period_per_width; - float desired_dash_gap = dash_gap_per_width / dash_period_per_width; - - // Straight borders should start and end with a dash, so max_t is - // reduced to cause this. - max_t -= unrounded ? dash_length : 0.0; - if (max_t >= 1.0) { - // Adjust dash gap to evenly divide max_t - float dash_count = floor(max_t); - float dash_period = max_t / dash_count; - border_color.a *= dash_alpha(t, dash_period, dash_length, dash_velocity, antialias_threshold); - } else if (unrounded) { - // When there isn't enough space for the full gap between the - // two start / end dashes of a straight border, reduce gap to - // make them fit. - float dash_gap = max_t - dash_length; - if (dash_gap > 0.0) { - float dash_period = dash_length + dash_gap; - border_color.a *= dash_alpha(t, dash_period, dash_length, dash_velocity, antialias_threshold); - } - } - } - - // Blend the border on top of the background and then linearly interpolate - // between the two as we slide inside the background. - float4 blended_border = over(background_color, border_color); - color = lerp(background_color, blended_border, - saturate(antialias_threshold - inner_sdf)); - } - - return color * float4(1.0, 1.0, 1.0, min(coverage, saturate(antialias_threshold - outer_sdf))); -} - -/* -** -** Shadows -** -*/ - -struct Shadow { - uint order; - float blur_radius; - Bounds bounds; - Corners corner_radii; - ContentMask content_mask; - Hsla color; - Bounds element_bounds; - Corners element_corner_radii; - uint inset; - uint pad; // align to 8 bytes -}; - -struct ShadowVertexOutput { - nointerpolation uint shadow_id: TEXCOORD0; - float4 position: SV_Position; - nointerpolation float4 color: COLOR; - float4 clip_distance: SV_ClipDistance; -}; - -struct ShadowFragmentInput { - nointerpolation uint shadow_id: TEXCOORD0; - float4 position: SV_Position; - nointerpolation float4 color: COLOR; -}; - -StructuredBuffer shadows: register(t1); - -ShadowVertexOutput shadow_vertex(uint vertex_id: SV_VertexID, uint instance_id: SV_InstanceID) { - float2 unit_vertex = float2(float(vertex_id & 1u), 0.5 * float(vertex_id & 2u)); - uint shadow_id = batch_start_index + instance_id; - Shadow shadow = shadows[shadow_id]; - - Bounds bounds; - if (shadow.inset != 0u) { - bounds = shadow.element_bounds; - } else { - // Leave room for the gaussian tail outside the shadow rect. - float margin = 3.0 * shadow.blur_radius; - bounds = shadow.bounds; - bounds.origin -= margin; - bounds.size += 2.0 * margin; - } - - float4 device_position = to_device_position(unit_vertex, bounds); - float4 clip_distance = distance_from_clip_rect(unit_vertex, bounds, shadow.content_mask); - float4 color = hsla_to_rgba(shadow.color); - - ShadowVertexOutput output; - output.position = device_position; - output.color = color; - output.shadow_id = shadow_id; - output.clip_distance = clip_distance; - - return output; -} - -float4 shadow_fragment(ShadowFragmentInput input): SV_TARGET { - Shadow shadow = shadows[input.shadow_id]; - float coverage = content_mask_coverage(input.position.xy, shadow.content_mask); - if (coverage <= 0.0) { - return float4(0.0, 0.0, 0.0, 0.0); - } - - float2 half_size = shadow.bounds.size / 2.; - float2 center = shadow.bounds.origin + half_size; - float2 point0 = input.position.xy - center; - float corner_radius = pick_corner_radius(point0, shadow.corner_radii); - - float alpha; - if (shadow.blur_radius == 0.) { - float distance = quad_sdf(input.position.xy, shadow.bounds, shadow.corner_radii); - alpha = saturate(0.5 - distance); - } else { - // The signal is only non-zero in a limited range, so don't waste samples - float low = point0.y - half_size.y; - float high = point0.y + half_size.y; - float start = clamp(-3. * shadow.blur_radius, low, high); - float end = clamp(3. * shadow.blur_radius, low, high); - - // Accumulate samples (we can get away with surprisingly few samples) - float step = (end - start) / 4.; - float y = start + step * 0.5; - alpha = 0.; - for (int i = 0; i < 4; i++) { - alpha += blur_along_x(point0.x, point0.y - y, shadow.blur_radius, - corner_radius, half_size) * - gaussian(y, shadow.blur_radius) * step; - y += step; - } - } - - if (shadow.inset != 0u) { - // The inset shadow is the complement of the (blurred) hole rect, clipped to the element. - // `saturate(0.5 - d)` gives a 1-pixel antialiased edge: d <= -0.5 -> 1, d >= 0.5 -> 0. - alpha = 1.0 - alpha; - float element_distance = quad_sdf(input.position.xy, shadow.element_bounds, - shadow.element_corner_radii); - alpha *= saturate(0.5 - element_distance); - } - - return input.color * float4(1., 1., 1., alpha * coverage); -} - -/* -** -** Path Rasterization -** -*/ - -struct PathRasterizationSprite { - float2 xy_position; - float2 st_position; - Background color; - Bounds bounds; - ContentMask content_mask; -}; - -StructuredBuffer path_rasterization_sprites: register(t1); - -struct PathVertexOutput { - float4 position: SV_Position; - float2 st_position: TEXCOORD0; - nointerpolation uint vertex_id: TEXCOORD1; - float4 clip_distance: SV_ClipDistance; -}; - -struct PathFragmentInput { - float4 position: SV_Position; - float2 st_position: TEXCOORD0; - nointerpolation uint vertex_id: TEXCOORD1; -}; - -PathVertexOutput path_rasterization_vertex(uint vertex_id: SV_VertexID) { - PathRasterizationSprite sprite = path_rasterization_sprites[vertex_id]; - - PathVertexOutput output; - output.position = to_device_position_impl(sprite.xy_position); - output.st_position = sprite.st_position; - output.vertex_id = vertex_id; - output.clip_distance = distance_from_clip_rect_impl(sprite.xy_position, sprite.bounds); - - return output; -} - -float4 path_rasterization_fragment(PathFragmentInput input): SV_Target { - float2 dx = ddx(input.st_position); - float2 dy = ddy(input.st_position); - PathRasterizationSprite sprite = path_rasterization_sprites[input.vertex_id]; - float coverage = content_mask_coverage(input.position.xy, sprite.content_mask); - if (coverage <= 0.0) { - return float4(0.0, 0.0, 0.0, 0.0); - } - - Background background = sprite.color; - Bounds bounds = sprite.bounds; - - float alpha; - if (length(float2(dx.x, dy.x))) { - alpha = 1.0; - } else { - float2 gradient = 2.0 * input.st_position.xx * float2(dx.x, dy.x) - float2(dx.y, dy.y); - float f = input.st_position.x * input.st_position.x - input.st_position.y; - float distance = f / length(gradient); - alpha = saturate(0.5 - distance); - } - - GradientColor gradient = prepare_gradient_color( - background.tag, background.color_space, background.solid, background.colors); - - float4 color = gradient_color(background, input.position.xy, bounds, - gradient.solid, gradient.color0, gradient.color1); - return float4(color.rgb * color.a * alpha, alpha * color.a) * coverage; -} - -/* -** -** Path Sprites -** -*/ - -struct PathSprite { - Bounds bounds; -}; - -struct PathSpriteVertexOutput { - float4 position: SV_Position; - float2 texture_coords: TEXCOORD0; -}; - -StructuredBuffer path_sprites: register(t1); - -PathSpriteVertexOutput path_sprite_vertex(uint vertex_id: SV_VertexID, uint sprite_id: SV_InstanceID) { - float2 unit_vertex = float2(float(vertex_id & 1u), 0.5 * float(vertex_id & 2u)); - PathSprite sprite = path_sprites[sprite_id]; - - // Don't apply content mask because it was already accounted for when rasterizing the path - float4 device_position = to_device_position(unit_vertex, sprite.bounds); - - float2 screen_position = sprite.bounds.origin + unit_vertex * sprite.bounds.size; - float2 texture_coords = screen_position / global_viewport_size; - - PathSpriteVertexOutput output; - output.position = device_position; - output.texture_coords = texture_coords; - return output; -} - -float4 path_sprite_fragment(PathSpriteVertexOutput input): SV_Target { - return t_sprite.Sample(s_sprite, input.texture_coords); -} - -/* -** -** Underlines -** -*/ - -struct Underline { - uint order; - uint pad; - Bounds bounds; - ContentMask content_mask; - Hsla color; - float thickness; - uint wavy; -}; - -struct UnderlineVertexOutput { - nointerpolation uint underline_id: TEXCOORD0; - float4 position: SV_Position; - nointerpolation float4 color: COLOR; - float4 clip_distance: SV_ClipDistance; -}; - -struct UnderlineFragmentInput { - nointerpolation uint underline_id: TEXCOORD0; - float4 position: SV_Position; - nointerpolation float4 color: COLOR; -}; - -StructuredBuffer underlines: register(t1); - -UnderlineVertexOutput underline_vertex(uint vertex_id: SV_VertexID, uint instance_id: SV_InstanceID) { - float2 unit_vertex = float2(float(vertex_id & 1u), 0.5 * float(vertex_id & 2u)); - uint underline_id = batch_start_index + instance_id; - Underline underline = underlines[underline_id]; - float4 device_position = to_device_position(unit_vertex, underline.bounds); - float4 clip_distance = distance_from_clip_rect(unit_vertex, underline.bounds, - underline.content_mask); - float4 color = hsla_to_rgba(underline.color); - - UnderlineVertexOutput output; - output.position = device_position; - output.color = color; - output.underline_id = underline_id; - output.clip_distance = clip_distance; - return output; -} - -float4 underline_fragment(UnderlineFragmentInput input): SV_Target { - const float WAVE_FREQUENCY = 2.0; - const float WAVE_HEIGHT_RATIO = 0.8; - - Underline underline = underlines[input.underline_id]; - float coverage = content_mask_coverage(input.position.xy, underline.content_mask); - if (coverage <= 0.0) { - return float4(0.0, 0.0, 0.0, 0.0); - } - if (underline.wavy) { - float half_thickness = underline.thickness * 0.5; - float2 origin = underline.bounds.origin; - - float2 st = ((input.position.xy - origin) / underline.bounds.size.y) - float2(0., 0.5); - float frequency = (M_PI_F * WAVE_FREQUENCY * underline.thickness) / underline.bounds.size.y; - float amplitude = (underline.thickness * WAVE_HEIGHT_RATIO) / underline.bounds.size.y; - - float sine = sin(st.x * frequency) * amplitude; - float dSine = cos(st.x * frequency) * amplitude * frequency; - float distance = (st.y - sine) / sqrt(1. + dSine * dSine); - float distance_in_pixels = distance * underline.bounds.size.y; - float distance_from_top_border = distance_in_pixels - half_thickness; - float distance_from_bottom_border = distance_in_pixels + half_thickness; - float alpha = saturate( - 0.5 - max(-distance_from_bottom_border, distance_from_top_border)); - return input.color * float4(1., 1., 1., alpha * coverage); - } else { - return input.color * float4(1., 1., 1., coverage); - } -} - -/* -** -** Monochrome sprites -** -*/ - -struct MonochromeSprite { - uint order; - uint pad; - Bounds bounds; - ContentMask content_mask; - Hsla color; - AtlasTile tile; - TransformationMatrix transformation; -}; - -struct MonochromeSpriteVertexOutput { - nointerpolation uint sprite_id: TEXCOORD0; - float4 position: SV_Position; - float2 tile_position: POSITION0; - nointerpolation float4 color: COLOR; - float4 clip_distance: SV_ClipDistance; -}; - -struct MonochromeSpriteFragmentInput { - nointerpolation uint sprite_id: TEXCOORD0; - float4 position: SV_Position; - float2 tile_position: POSITION0; - nointerpolation float4 color: COLOR; - float4 clip_distance: SV_ClipDistance; -}; - -StructuredBuffer mono_sprites: register(t1); - -MonochromeSpriteVertexOutput monochrome_sprite_vertex(uint vertex_id: SV_VertexID, uint instance_id: SV_InstanceID) { - float2 unit_vertex = float2(float(vertex_id & 1u), 0.5 * float(vertex_id & 2u)); - uint sprite_id = batch_start_index + instance_id; - MonochromeSprite sprite = mono_sprites[sprite_id]; - float4 device_position = - to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation); - float4 clip_distance = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation); - float2 tile_position = to_tile_position(unit_vertex, sprite.tile); - float4 color = hsla_to_rgba(sprite.color); - - MonochromeSpriteVertexOutput output; - output.position = device_position; - output.sprite_id = sprite_id; - output.tile_position = tile_position; - output.color = color; - output.clip_distance = clip_distance; - return output; -} - -float4 monochrome_sprite_fragment(MonochromeSpriteFragmentInput input): SV_Target { - MonochromeSprite sprite = mono_sprites[input.sprite_id]; - float coverage = content_mask_coverage(input.position.xy, sprite.content_mask); - if (coverage <= 0.0) { - return float4(0.0, 0.0, 0.0, 0.0); - } - float sample = t_sprite.Sample(s_sprite, input.tile_position).r; - float alpha_corrected = apply_contrast_and_gamma_correction(sample, input.color.rgb, grayscale_enhanced_contrast, gamma_ratios); - return float4(input.color.rgb, input.color.a * alpha_corrected * coverage); -} - -MonochromeSpriteVertexOutput subpixel_sprite_vertex(uint vertex_id: SV_VertexID, uint instance_id: SV_InstanceID) { - return monochrome_sprite_vertex(vertex_id, instance_id); -} - -SubpixelSpriteFragmentOutput subpixel_sprite_fragment(MonochromeSpriteFragmentInput input) { - MonochromeSprite sprite = mono_sprites[input.sprite_id]; - float coverage = content_mask_coverage(input.position.xy, sprite.content_mask); - if (coverage <= 0.0) { - SubpixelSpriteFragmentOutput empty; - empty.foreground = float4(0.0, 0.0, 0.0, 0.0); - empty.alpha = float4(0.0, 0.0, 0.0, 0.0); - return empty; - } - float3 sample = t_sprite.Sample(s_sprite, input.tile_position).rgb; - if (is_bgr) { - sample = sample.bgr; - } - float3 alpha_corrected = apply_contrast_and_gamma_correction3(sample, input.color.rgb, subpixel_enhanced_contrast, gamma_ratios); - - SubpixelSpriteFragmentOutput output; - output.foreground = float4(input.color.rgb, 1.0f); - output.alpha = float4(input.color.a * alpha_corrected * coverage, coverage); - return output; -} - -/* -** -** Polychrome sprites -** -*/ - -struct PolychromeSprite { - uint order; - uint pad; - uint grayscale; - float opacity; - Bounds bounds; - ContentMask content_mask; - Corners corner_radii; - AtlasTile tile; -}; - -struct PolychromeSpriteVertexOutput { - nointerpolation uint sprite_id: TEXCOORD0; - float4 position: SV_Position; - float2 tile_position: POSITION; - float4 clip_distance: SV_ClipDistance; -}; - -struct PolychromeSpriteFragmentInput { - nointerpolation uint sprite_id: TEXCOORD0; - float4 position: SV_Position; - float2 tile_position: POSITION; -}; - -StructuredBuffer poly_sprites: register(t1); - -PolychromeSpriteVertexOutput polychrome_sprite_vertex(uint vertex_id: SV_VertexID, uint instance_id: SV_InstanceID) { - float2 unit_vertex = float2(float(vertex_id & 1u), 0.5 * float(vertex_id & 2u)); - uint sprite_id = batch_start_index + instance_id; - PolychromeSprite sprite = poly_sprites[sprite_id]; - float4 device_position = to_device_position(unit_vertex, sprite.bounds); - float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds, - sprite.content_mask); - float2 tile_position = to_tile_position(unit_vertex, sprite.tile); - - PolychromeSpriteVertexOutput output; - output.position = device_position; - output.tile_position = tile_position; - output.sprite_id = sprite_id; - output.clip_distance = clip_distance; - return output; -} - -float4 polychrome_sprite_fragment(PolychromeSpriteFragmentInput input): SV_Target { - PolychromeSprite sprite = poly_sprites[input.sprite_id]; - float coverage = content_mask_coverage(input.position.xy, sprite.content_mask); - if (coverage <= 0.0) { - return float4(0.0, 0.0, 0.0, 0.0); - } - float4 sample = t_sprite.Sample(s_sprite, input.tile_position); - float distance = quad_sdf(input.position.xy, sprite.bounds, sprite.corner_radii); - - float4 color = sample; - if (sprite.grayscale != 0u) { - float3 grayscale = dot(color.rgb, GRAYSCALE_FACTORS); - color = float4(grayscale, sample.a); - } - color.a *= sprite.opacity * min(coverage, saturate(0.5 - distance)); - return color; -} diff --git a/crates/gpui_pre_windows/src/system_notifications.rs b/crates/gpui_pre_windows/src/system_notifications.rs deleted file mode 100644 index b0075f0..0000000 --- a/crates/gpui_pre_windows/src/system_notifications.rs +++ /dev/null @@ -1,198 +0,0 @@ -//! System notifications as Windows toast notifications. - -use std::cell::RefCell; -use std::collections::{HashMap, hash_map::DefaultHasher}; -use std::hash::{Hash as _, Hasher as _}; -use std::rc::Rc; - -use futures::StreamExt as _; -use futures::channel::mpsc; -use gpui::{ - ForegroundExecutor, SharedString, SystemNotification, SystemNotificationResponse, Task, -}; -use windows::Data::Xml::Dom::XmlDocument; -use windows::Foundation::TypedEventHandler; -use windows::UI::Notifications::{ - ToastActivatedEventArgs, ToastNotification, ToastNotificationManager, ToastNotifier, -}; -use windows::core::{IInspectable, Interface as _, h}; - -type ResponseCallback = Rc>>>; - -pub(crate) struct SystemNotificationState { - notifier: Option, - active_toasts: HashMap, - response_sender: mpsc::UnboundedSender, - response_receiver: Option>, - callback: ResponseCallback, - _response_task: Option>, -} - -impl SystemNotificationState { - pub(crate) fn new() -> Self { - let (response_sender, response_receiver) = mpsc::unbounded(); - Self { - notifier: None, - active_toasts: HashMap::new(), - response_sender, - response_receiver: Some(response_receiver), - callback: Rc::new(RefCell::new(None)), - _response_task: None, - } - } - - pub(crate) fn show( - &mut self, - has_package_identity: bool, - app_identity: Option<(&str, &str)>, - notification: SystemNotification, - ) -> windows::core::Result<()> { - let Some(notifier) = self.notifier(has_package_identity, app_identity)? else { - return Ok(()); - }; - - let document = toast_document(¬ification)?; - let toast = ToastNotification::CreateToastNotification(&document)?; - // Windows caps toast tags at 64 characters (post-Creators Update), - // so hash the arbitrary GPUI tag down to a fixed-width value. - let tag = { - let mut hasher = DefaultHasher::new(); - notification.tag.hash(&mut hasher); - format!("{:016x}", hasher.finish()) - }; - toast.SetTag(&tag.into())?; - - let sender = self.response_sender.clone(); - let response_tag = notification.tag.clone(); - toast.Activated(&TypedEventHandler::::new( - move |_sender, arguments| { - let action_id = arguments - .as_ref() - .and_then(|arguments| arguments.cast::().ok()) - .and_then(|arguments| arguments.Arguments().ok()) - .filter(|arguments| !arguments.is_empty()) - .map(|arguments| SharedString::from(arguments.to_string())); - sender - .unbounded_send(SystemNotificationResponse { - tag: response_tag.clone(), - action_id, - }) - .ok(); - Ok(()) - }, - ))?; - - if let Some(previous) = self.active_toasts.remove(¬ification.tag) { - notifier.Hide(&previous)?; - } - notifier.Show(&toast)?; - self.active_toasts.insert(notification.tag, toast); - Ok(()) - } - - pub(crate) fn dismiss(&mut self, tag: &str) { - let Some(toast) = self.active_toasts.remove(tag) else { - return; - }; - let Some(notifier) = &self.notifier else { - return; - }; - if let Err(error) = notifier.Hide(&toast) { - log::warn!("failed to dismiss system notification: {error}"); - } - } - - pub(crate) fn on_response( - &mut self, - executor: &ForegroundExecutor, - callback: Box, - ) { - *self.callback.borrow_mut() = Some(callback); - - if let Some(mut receiver) = self.response_receiver.take() { - let callback = self.callback.clone(); - self._response_task = Some(executor.spawn(async move { - while let Some(response) = receiver.next().await { - // Take the callback out for the call: it may re-enter the - // platform (e.g. to dismiss the notification it was told - // about) or replace itself. - let taken = callback.borrow_mut().take(); - if let Some(mut taken) = taken { - taken(response); - callback.borrow_mut().get_or_insert(taken); - } - } - })); - } - } - - fn notifier( - &mut self, - has_package_identity: bool, - app_identity: Option<(&str, &str)>, - ) -> windows::core::Result> { - if let Some(notifier) = &self.notifier { - return Ok(Some(notifier.clone())); - } - - let notifier = if has_package_identity { - ToastNotificationManager::CreateToastNotifier()? - } else { - let Some((app_identifier, app_name)) = app_identity else { - log::warn!( - "cannot show a system notification without an app identity; \ - call `App::set_app_identity` during startup" - ); - return Ok(None); - }; - register_app_user_model_id(app_identifier, app_name); - ToastNotificationManager::CreateToastNotifierWithId(&app_identifier.into())? - }; - - self.notifier = Some(notifier.clone()); - Ok(Some(notifier)) - } -} - -fn toast_document(notification: &SystemNotification) -> windows::core::Result { - let document = XmlDocument::new()?; - let toast = document.CreateElement(h!("toast"))?; - document.AppendChild(&toast)?; - - let visual = document.CreateElement(h!("visual"))?; - toast.AppendChild(&visual)?; - let binding = document.CreateElement(h!("binding"))?; - binding.SetAttribute(h!("template"), h!("ToastGeneric"))?; - visual.AppendChild(&binding)?; - for text in [¬ification.title, ¬ification.body] { - let element = document.CreateElement(h!("text"))?; - element.SetInnerText(&text.as_ref().into())?; - binding.AppendChild(&element)?; - } - - if !notification.actions.is_empty() { - let actions = document.CreateElement(h!("actions"))?; - toast.AppendChild(&actions)?; - for action in ¬ification.actions { - let action_element = document.CreateElement(h!("action"))?; - action_element.SetAttribute(h!("content"), &action.label.as_ref().into())?; - action_element.SetAttribute(h!("arguments"), &action.id.as_ref().into())?; - actions.AppendChild(&action_element)?; - } - } - - let audio = document.CreateElement(h!("audio"))?; - audio.SetAttribute(h!("silent"), h!("true"))?; - toast.AppendChild(&audio)?; - Ok(document) -} - -/// Registers the app's AUMID so toasts display correctly for an unpackaged app without a Start Menu shortcut. -fn register_app_user_model_id(app_identifier: &str, app_name: &str) { - let result = windows_registry::CURRENT_USER - .create(format!(r"Software\Classes\AppUserModelId\{app_identifier}")) - .and_then(|key| key.set_string("DisplayName", app_name)); - if let Err(error) = result { - log::warn!("failed to register AppUserModelID; notifications may not display: {error}"); - } -} diff --git a/crates/gpui_pre_windows/src/system_settings.rs b/crates/gpui_pre_windows/src/system_settings.rs deleted file mode 100644 index 8cce544..0000000 --- a/crates/gpui_pre_windows/src/system_settings.rs +++ /dev/null @@ -1,87 +0,0 @@ -use std::{ - cell::Cell, - ffi::{c_uint, c_void}, -}; - -use gpui_util::ResultExt; -use windows::Win32::UI::WindowsAndMessaging::{ - SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES, SYSTEM_PARAMETERS_INFO_ACTION, - SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, SystemParametersInfoW, -}; - -/// Windows settings pulled from SystemParametersInfo -/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-systemparametersinfow -#[derive(Default, Debug, Clone)] -pub(crate) struct WindowsSystemSettings { - pub(crate) mouse_wheel_settings: MouseWheelSettings, -} - -#[derive(Default, Debug, Clone)] -pub(crate) struct MouseWheelSettings { - /// SEE: SPI_GETWHEELSCROLLCHARS - pub(crate) wheel_scroll_chars: Cell, - /// SEE: SPI_GETWHEELSCROLLLINES - pub(crate) wheel_scroll_lines: Cell, -} - -impl WindowsSystemSettings { - pub(crate) fn new() -> Self { - let mut settings = Self::default(); - settings.init(); - settings - } - - fn init(&mut self) { - self.mouse_wheel_settings.update(); - } - - pub(crate) fn update(&self, wparam: usize) { - match SYSTEM_PARAMETERS_INFO_ACTION(wparam as u32) { - SPI_GETWHEELSCROLLLINES | SPI_GETWHEELSCROLLCHARS => self.update_mouse_wheel_settings(), - _ => {} - } - } - - fn update_mouse_wheel_settings(&self) { - self.mouse_wheel_settings.update(); - } -} - -impl MouseWheelSettings { - fn update(&self) { - self.update_wheel_scroll_chars(); - self.update_wheel_scroll_lines(); - } - - fn update_wheel_scroll_chars(&self) { - let mut value = c_uint::default(); - let result = unsafe { - SystemParametersInfoW( - SPI_GETWHEELSCROLLCHARS, - 0, - Some((&mut value) as *mut c_uint as *mut c_void), - SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS::default(), - ) - }; - - if result.log_err() != None && self.wheel_scroll_chars.get() != value { - self.wheel_scroll_chars.set(value); - } - } - - fn update_wheel_scroll_lines(&self) { - let mut value = c_uint::default(); - let result = unsafe { - SystemParametersInfoW( - SPI_GETWHEELSCROLLLINES, - 0, - Some((&mut value) as *mut c_uint as *mut c_void), - SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS::default(), - ) - }; - - if result.log_err() != None && self.wheel_scroll_lines.get() != value { - self.wheel_scroll_lines.set(value); - } - } -} diff --git a/crates/gpui_pre_windows/src/util.rs b/crates/gpui_pre_windows/src/util.rs deleted file mode 100644 index b3badd8..0000000 --- a/crates/gpui_pre_windows/src/util.rs +++ /dev/null @@ -1,191 +0,0 @@ -use std::sync::OnceLock; - -use anyhow::Context; -use gpui_util::ResultExt; -use windows::{ - UI::{ - Color, - ViewManagement::{UIColorType, UISettings}, - }, - Win32::{ - Foundation::*, Graphics::Dwm::*, System::LibraryLoader::LoadLibraryA, - UI::WindowsAndMessaging::*, - }, - core::{BOOL, PCSTR}, -}; - -use crate::*; -use gpui::*; - -pub(crate) trait HiLoWord { - fn hiword(&self) -> u16; - fn loword(&self) -> u16; - fn signed_hiword(&self) -> i16; - fn signed_loword(&self) -> i16; -} - -impl HiLoWord for WPARAM { - fn hiword(&self) -> u16 { - ((self.0 >> 16) & 0xFFFF) as u16 - } - - fn loword(&self) -> u16 { - (self.0 & 0xFFFF) as u16 - } - - fn signed_hiword(&self) -> i16 { - ((self.0 >> 16) & 0xFFFF) as i16 - } - - fn signed_loword(&self) -> i16 { - (self.0 & 0xFFFF) as i16 - } -} - -impl HiLoWord for LPARAM { - fn hiword(&self) -> u16 { - ((self.0 >> 16) & 0xFFFF) as u16 - } - - fn loword(&self) -> u16 { - (self.0 & 0xFFFF) as u16 - } - - fn signed_hiword(&self) -> i16 { - ((self.0 >> 16) & 0xFFFF) as i16 - } - - fn signed_loword(&self) -> i16 { - (self.0 & 0xFFFF) as i16 - } -} - -pub(crate) unsafe fn get_window_long(hwnd: HWND, nindex: WINDOW_LONG_PTR_INDEX) -> isize { - #[cfg(target_pointer_width = "64")] - unsafe { - GetWindowLongPtrW(hwnd, nindex) - } - #[cfg(target_pointer_width = "32")] - unsafe { - GetWindowLongW(hwnd, nindex) as isize - } -} - -pub(crate) unsafe fn set_window_long( - hwnd: HWND, - nindex: WINDOW_LONG_PTR_INDEX, - dwnewlong: isize, -) -> isize { - #[cfg(target_pointer_width = "64")] - unsafe { - SetWindowLongPtrW(hwnd, nindex, dwnewlong) - } - #[cfg(target_pointer_width = "32")] - unsafe { - SetWindowLongW(hwnd, nindex, dwnewlong as i32) as isize - } -} - -pub(crate) fn windows_credentials_target_name(url: &str) -> String { - format!("zed:url={}", url) -} - -pub(crate) fn load_cursor(style: CursorStyle) -> Option { - static ARROW: OnceLock = OnceLock::new(); - static IBEAM: OnceLock = OnceLock::new(); - static CROSS: OnceLock = OnceLock::new(); - static HAND: OnceLock = OnceLock::new(); - static SIZEWE: OnceLock = OnceLock::new(); - static SIZENS: OnceLock = OnceLock::new(); - static SIZENWSE: OnceLock = OnceLock::new(); - static SIZENESW: OnceLock = OnceLock::new(); - static NO: OnceLock = OnceLock::new(); - let (lock, name) = match style { - CursorStyle::IBeam | CursorStyle::IBeamCursorForVerticalLayout => (&IBEAM, IDC_IBEAM), - CursorStyle::Crosshair => (&CROSS, IDC_CROSS), - CursorStyle::PointingHand | CursorStyle::DragLink => (&HAND, IDC_HAND), - CursorStyle::ResizeLeft - | CursorStyle::ResizeRight - | CursorStyle::ResizeLeftRight - | CursorStyle::ResizeColumn => (&SIZEWE, IDC_SIZEWE), - CursorStyle::ResizeUp - | CursorStyle::ResizeDown - | CursorStyle::ResizeUpDown - | CursorStyle::ResizeRow => (&SIZENS, IDC_SIZENS), - CursorStyle::ResizeUpLeftDownRight => (&SIZENWSE, IDC_SIZENWSE), - CursorStyle::ResizeUpRightDownLeft => (&SIZENESW, IDC_SIZENESW), - CursorStyle::OperationNotAllowed => (&NO, IDC_NO), - _ => (&ARROW, IDC_ARROW), - }; - Some( - *(*lock.get_or_init(|| { - HCURSOR( - unsafe { LoadImageW(None, name, IMAGE_CURSOR, 0, 0, LR_DEFAULTSIZE | LR_SHARED) } - .log_err() - .unwrap_or_default() - .0, - ) - .into() - })), - ) -} - -/// This function is used to configure the dark mode for the window built-in title bar. -pub(crate) fn configure_dwm_dark_mode(hwnd: HWND, appearance: WindowAppearance) { - let dark_mode_enabled: BOOL = match appearance { - WindowAppearance::Dark | WindowAppearance::VibrantDark => true.into(), - WindowAppearance::Light | WindowAppearance::VibrantLight => false.into(), - }; - unsafe { - DwmSetWindowAttribute( - hwnd, - DWMWA_USE_IMMERSIVE_DARK_MODE, - &dark_mode_enabled as *const _ as _, - std::mem::size_of::() as u32, - ) - .log_err(); - } -} - -#[inline] -pub(crate) fn logical_point(x: f32, y: f32, scale_factor: f32) -> Point { - Point { - x: px(x / scale_factor), - y: px(y / scale_factor), - } -} - -// https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/apply-windows-themes -#[inline] -pub(crate) fn system_appearance() -> Result { - let ui_settings = UISettings::new()?; - let foreground_color = ui_settings.GetColorValue(UIColorType::Foreground)?; - // If the foreground is light, then is_color_light will evaluate to true, - // meaning Dark mode is enabled. - if is_color_light(&foreground_color) { - Ok(WindowAppearance::Dark) - } else { - Ok(WindowAppearance::Light) - } -} - -#[inline(always)] -fn is_color_light(color: &Color) -> bool { - ((5 * color.G as u32) + (2 * color.R as u32) + color.B as u32) > (8 * 128) -} - -pub(crate) fn with_dll_library(dll_name: PCSTR, f: F) -> Result -where - F: FnOnce(HMODULE) -> Result, -{ - let library = unsafe { - LoadLibraryA(dll_name).with_context(|| format!("Loading dll: {}", dll_name.display()))? - }; - let result = f(library); - unsafe { - FreeLibrary(library) - .with_context(|| format!("Freeing dll: {}", dll_name.display())) - .log_err(); - } - result -} diff --git a/crates/gpui_pre_windows/src/vsync.rs b/crates/gpui_pre_windows/src/vsync.rs deleted file mode 100644 index 51fbdf6..0000000 --- a/crates/gpui_pre_windows/src/vsync.rs +++ /dev/null @@ -1,81 +0,0 @@ -use std::{ - sync::LazyLock, - time::{Duration, Instant}, -}; - -use anyhow::{Context, Result}; -use gpui_util::ResultExt; -use windows::Win32::{ - Foundation::HWND, - Graphics::Dwm::{DWM_TIMING_INFO, DwmFlush, DwmGetCompositionTimingInfo}, - System::Performance::QueryPerformanceFrequency, -}; - -static QPC_TICKS_PER_SECOND: LazyLock = LazyLock::new(|| { - let mut frequency = 0; - // On systems that run Windows XP or later, the function will always succeed and - // will thus never return zero. - unsafe { QueryPerformanceFrequency(&mut frequency).unwrap() }; - frequency as u64 -}); - -const VSYNC_INTERVAL_THRESHOLD: Duration = Duration::from_millis(1); -const DEFAULT_VSYNC_INTERVAL: Duration = Duration::from_micros(16_666); // ~60Hz - -pub(crate) struct VSyncProvider { - interval: Duration, - f: Box bool>, -} - -impl VSyncProvider { - pub(crate) fn new() -> Self { - let interval = get_dwm_interval() - .context("Failed to get DWM interval") - .log_err() - .unwrap_or(DEFAULT_VSYNC_INTERVAL); - let f = Box::new(|| unsafe { DwmFlush().is_ok() }); - Self { interval, f } - } - - pub(crate) fn wait_for_vsync(&self) { - let vsync_start = Instant::now(); - let wait_succeeded = (self.f)(); - let elapsed = vsync_start.elapsed(); - // DwmFlush and DCompositionWaitForCompositorClock returns very early - // instead of waiting until vblank when the monitor goes to sleep or is - // unplugged (nothing to present due to desktop occlusion). We use 1ms as - // a threshold for the duration of the wait functions and fallback to - // Sleep() if it returns before that. This could happen during normal - // operation for the first call after the vsync thread becomes non-idle, - // but it shouldn't happen often. - if !wait_succeeded || elapsed < VSYNC_INTERVAL_THRESHOLD { - log::trace!("VSyncProvider::wait_for_vsync() took less time than expected"); - std::thread::sleep(self.interval); - } - } -} - -fn get_dwm_interval() -> Result { - let mut timing_info = DWM_TIMING_INFO { - cbSize: std::mem::size_of::() as u32, - ..Default::default() - }; - unsafe { DwmGetCompositionTimingInfo(HWND::default(), &mut timing_info) }?; - let interval = retrieve_duration(timing_info.qpcRefreshPeriod, *QPC_TICKS_PER_SECOND); - // Check for interval values that are impossibly low. A 29 microsecond - // interval was seen (from a qpcRefreshPeriod of 60). - if interval < VSYNC_INTERVAL_THRESHOLD { - Ok(retrieve_duration( - timing_info.rateRefresh.uiDenominator as u64, - timing_info.rateRefresh.uiNumerator as u64, - )) - } else { - Ok(interval) - } -} - -#[inline] -fn retrieve_duration(counts: u64, ticks_per_second: u64) -> Duration { - let ticks_per_microsecond = ticks_per_second / 1_000_000; - Duration::from_micros(counts / ticks_per_microsecond) -} diff --git a/crates/gpui_pre_windows/src/window.rs b/crates/gpui_pre_windows/src/window.rs deleted file mode 100644 index 37e9d9a..0000000 --- a/crates/gpui_pre_windows/src/window.rs +++ /dev/null @@ -1,1687 +0,0 @@ -#![deny(unsafe_op_in_unsafe_fn)] - -use std::{ - cell::{Cell, RefCell}, - num::NonZeroIsize, - path::PathBuf, - rc::{Rc, Weak}, - str::FromStr, - sync::{Arc, Once, atomic::AtomicBool}, - time::{Duration, Instant}, -}; - -use anyhow::{Context as _, Result}; -use futures::channel::oneshot::{self, Receiver}; -use gpui_util::ResultExt; -use raw_window_handle as rwh; -use smallvec::SmallVec; -use windows::{ - Win32::{ - Foundation::*, - Graphics::Dwm::*, - Graphics::Gdi::*, - System::{ - Com::*, Diagnostics::Debug::MessageBeep, LibraryLoader::*, Ole::*, SystemServices::*, - }, - UI::{Controls::*, HiDpi::*, Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*}, - }, - core::*, -}; - -use crate::direct_manipulation::DirectManipulationHandler; -use crate::*; -use gpui::*; - -pub(crate) struct WindowsWindow(pub Rc); - -impl std::ops::Deref for WindowsWindow { - type Target = WindowsWindowInner; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -pub struct WindowsWindowState { - pub origin: Cell>, - pub logical_size: Cell>, - pub min_size: Option>, - pub fullscreen_restore_bounds: Cell>, - pub border_offset: WindowBorderOffset, - pub appearance: Cell, - pub background_appearance: Cell, - pub scale_factor: Cell, - pub restore_from_minimized: Cell>>, - - pub callbacks: Callbacks, - pub input_handler: Cell>, - pub ime_enabled: Cell, - pub pending_surrogate: Cell>, - pub last_reported_modifiers: Cell>, - pub last_reported_capslock: Cell>, - pub hovered: Cell, - pub direct_manipulation: DirectManipulationHandler, - - pub renderer: RefCell, - /// Set when the next `draw_window` call must be treated as a forced - /// render. Used after a GPU device-lost recovery, where the next frame - /// must both re-enable drawing (via `mark_drawable`) and bypass the GPUI - /// view cache (which would otherwise replay stale atlas tile references - /// from the previous frame and panic in `DirectXAtlasState::texture`), - /// and when a forced render was requested while another draw was in - /// progress and had to be deferred. - pub force_render_pending: Cell, - - pub click_state: ClickState, - pub current_cursor: Cell>, - /// Shared with [`WindowsPlatformState::cursor_visible`]. - pub cursor_visible: Arc, - pub nc_button_pressed: Cell>, - - pub display: Cell, - /// Flag to instruct the `VSyncProvider` thread to invalidate the directx devices - /// as resizing them has failed, causing us to have lost at least the render target. - pub invalidate_devices: Arc, - /// Shared with [`WindowsPlatformState::draw_coordinator`] and every other window. - pub(crate) draw_coordinator: Rc, - fullscreen: Cell>, - initial_placement: Cell>, - hwnd: HWND, - pub(crate) a11y: RefCell>, -} - -pub(crate) struct WindowsWindowInner { - hwnd: HWND, - drop_target_helper: IDropTargetHelper, - pub(crate) state: WindowsWindowState, - system_settings: WindowsSystemSettings, - pub(crate) handle: AnyWindowHandle, - pub(crate) hide_title_bar: bool, - pub(crate) is_movable: bool, - pub(crate) is_resizable: bool, - pub(crate) is_minimizable: bool, - pub(crate) executor: ForegroundExecutor, - pub(crate) validation_number: usize, - pub(crate) main_receiver: PriorityQueueReceiver, - pub(crate) platform_window_handle: HWND, - pub(crate) parent_hwnd: Option, -} - -impl WindowsWindowState { - fn new( - hwnd: HWND, - directx_devices: &DirectXDevices, - window_params: &CREATESTRUCTW, - current_cursor: Option, - cursor_visible: Arc, - display: WindowsDisplay, - min_size: Option>, - appearance: WindowAppearance, - disable_direct_composition: bool, - invalidate_devices: Arc, - draw_coordinator: Rc, - ) -> Result { - let scale_factor = { - let monitor_dpi = unsafe { GetDpiForWindow(hwnd) } as f32; - monitor_dpi / USER_DEFAULT_SCREEN_DPI as f32 - }; - let origin = logical_point(window_params.x as f32, window_params.y as f32, scale_factor); - let logical_size = { - let physical_size = size( - DevicePixels(window_params.cx), - DevicePixels(window_params.cy), - ); - physical_size.to_pixels(scale_factor) - }; - let fullscreen_restore_bounds = Bounds { - origin, - size: logical_size, - }; - let border_offset = WindowBorderOffset::default(); - let restore_from_minimized = None; - let renderer = DirectXRenderer::new(hwnd, directx_devices, disable_direct_composition) - .context("Creating DirectX renderer")?; - let callbacks = Callbacks::default(); - let input_handler = None; - let pending_surrogate = None; - let last_reported_modifiers = None; - let last_reported_capslock = None; - let hovered = false; - let click_state = ClickState::new(); - let nc_button_pressed = None; - let fullscreen = None; - let initial_placement = None; - - let direct_manipulation = DirectManipulationHandler::new(hwnd, scale_factor) - .context("initializing Direct Manipulation")?; - - Ok(Self { - origin: Cell::new(origin), - logical_size: Cell::new(logical_size), - fullscreen_restore_bounds: Cell::new(fullscreen_restore_bounds), - border_offset, - appearance: Cell::new(appearance), - background_appearance: Cell::new(WindowBackgroundAppearance::Opaque), - scale_factor: Cell::new(scale_factor), - restore_from_minimized: Cell::new(restore_from_minimized), - min_size, - callbacks, - input_handler: Cell::new(input_handler), - ime_enabled: Cell::new(true), - pending_surrogate: Cell::new(pending_surrogate), - last_reported_modifiers: Cell::new(last_reported_modifiers), - last_reported_capslock: Cell::new(last_reported_capslock), - hovered: Cell::new(hovered), - renderer: RefCell::new(renderer), - force_render_pending: Cell::new(false), - click_state, - current_cursor: Cell::new(current_cursor), - cursor_visible, - nc_button_pressed: Cell::new(nc_button_pressed), - display: Cell::new(display), - fullscreen: Cell::new(fullscreen), - initial_placement: Cell::new(initial_placement), - hwnd, - invalidate_devices, - draw_coordinator, - direct_manipulation, - a11y: RefCell::new(None), - }) - } - - #[inline] - pub(crate) fn is_fullscreen(&self) -> bool { - self.fullscreen.get().is_some() - } - - pub(crate) fn is_maximized(&self) -> bool { - !self.is_fullscreen() && unsafe { IsZoomed(self.hwnd) }.as_bool() - } - - fn bounds(&self) -> Bounds { - Bounds { - origin: self.origin.get(), - size: self.logical_size.get(), - } - } - - // Calculate the bounds used for saving and whether the window is maximized. - fn calculate_window_bounds(&self) -> (Bounds, bool) { - let placement = unsafe { - let mut placement = WINDOWPLACEMENT { - length: std::mem::size_of::() as u32, - ..Default::default() - }; - GetWindowPlacement(self.hwnd, &mut placement) - .context("failed to get window placement") - .log_err(); - placement - }; - ( - calculate_client_rect( - placement.rcNormalPosition, - &self.border_offset, - self.scale_factor.get(), - ), - placement.showCmd == SW_SHOWMAXIMIZED.0 as u32, - ) - } - - fn window_bounds(&self) -> WindowBounds { - let (bounds, maximized) = self.calculate_window_bounds(); - - if self.is_fullscreen() { - WindowBounds::Fullscreen(self.fullscreen_restore_bounds.get()) - } else if maximized { - WindowBounds::Maximized(bounds) - } else { - WindowBounds::Windowed(bounds) - } - } - - /// get the logical size of the app's drawable area. - /// - /// Currently, GPUI uses the logical size of the app to handle mouse interactions (such as - /// whether the mouse collides with other elements of GPUI). - fn content_size(&self) -> Size { - self.logical_size.get() - } -} - -impl WindowsWindowInner { - fn new(context: &mut WindowCreateContext, hwnd: HWND, cs: &CREATESTRUCTW) -> Result> { - let state = WindowsWindowState::new( - hwnd, - &context.directx_devices, - cs, - context.current_cursor, - context.cursor_visible.clone(), - context.display, - context.min_size, - context.appearance, - context.disable_direct_composition, - context.invalidate_devices.clone(), - context.draw_coordinator.clone(), - )?; - - Ok(Rc::new(Self { - hwnd, - drop_target_helper: context.drop_target_helper.clone(), - state, - handle: context.handle, - hide_title_bar: context.hide_title_bar, - is_movable: context.is_movable, - is_resizable: context.is_resizable, - is_minimizable: context.is_minimizable, - executor: context.executor.clone(), - validation_number: context.validation_number, - main_receiver: context.main_receiver.clone(), - platform_window_handle: context.platform_window_handle, - system_settings: WindowsSystemSettings::new(), - parent_hwnd: context.parent_hwnd, - })) - } - - fn toggle_fullscreen(self: &Rc) { - let this = self.clone(); - self.executor - .spawn(async move { - let StyleAndBounds { - style, - x, - y, - cx, - cy, - } = match this.state.fullscreen.take() { - Some(state) => state, - None => { - let (window_bounds, _) = this.state.calculate_window_bounds(); - this.state.fullscreen_restore_bounds.set(window_bounds); - - let style = - WINDOW_STYLE(unsafe { get_window_long(this.hwnd, GWL_STYLE) } as _); - let mut rc = RECT::default(); - unsafe { GetWindowRect(this.hwnd, &mut rc) } - .context("failed to get window rect") - .log_err(); - let _ = this.state.fullscreen.set(Some(StyleAndBounds { - style, - x: rc.left, - y: rc.top, - cx: rc.right - rc.left, - cy: rc.bottom - rc.top, - })); - let style = style - & !(WS_THICKFRAME - | WS_SYSMENU - | WS_MAXIMIZEBOX - | WS_MINIMIZEBOX - | WS_CAPTION); - let physical_bounds = this.state.display.get().physical_bounds(); - StyleAndBounds { - style, - x: physical_bounds.left().0, - y: physical_bounds.top().0, - cx: physical_bounds.size.width.0, - cy: physical_bounds.size.height.0, - } - } - }; - set_non_rude_hwnd(this.hwnd, !this.state.is_fullscreen()); - unsafe { set_window_long(this.hwnd, GWL_STYLE, style.0 as isize) }; - unsafe { - SetWindowPos( - this.hwnd, - None, - x, - y, - cx, - cy, - SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOZORDER, - ) - } - .log_err(); - }) - .detach(); - } - - fn set_window_placement(self: &Rc) -> Result<()> { - let Some(open_status) = self.state.initial_placement.take() else { - return Ok(()); - }; - match open_status.state { - WindowOpenState::Maximized => unsafe { - SetWindowPlacement(self.hwnd, &open_status.placement) - .context("failed to set window placement")?; - ShowWindowAsync(self.hwnd, SW_MAXIMIZE).ok()?; - }, - WindowOpenState::Fullscreen => { - unsafe { - SetWindowPlacement(self.hwnd, &open_status.placement) - .context("failed to set window placement")? - }; - self.toggle_fullscreen(); - } - WindowOpenState::Windowed => unsafe { - SetWindowPlacement(self.hwnd, &open_status.placement) - .context("failed to set window placement")?; - }, - } - Ok(()) - } - - pub(crate) fn system_settings(&self) -> &WindowsSystemSettings { - &self.system_settings - } -} - -#[derive(Default)] -pub(crate) struct Callbacks { - pub(crate) request_frame: Cell>>, - pub(crate) input: Cell DispatchEventResult>>>, - pub(crate) active_status_change: Cell>>, - pub(crate) hovered_status_change: Cell>>, - pub(crate) resize: Cell, f32)>>>, - pub(crate) moved: Cell>>, - pub(crate) should_close: Cell bool>>>, - pub(crate) close: Cell>>, - pub(crate) hit_test_window_control: Cell Option>>>, - pub(crate) appearance_changed: Cell>>, -} - -struct WindowCreateContext { - inner: Option>>, - handle: AnyWindowHandle, - hide_title_bar: bool, - display: WindowsDisplay, - is_movable: bool, - is_resizable: bool, - is_minimizable: bool, - min_size: Option>, - executor: ForegroundExecutor, - current_cursor: Option, - cursor_visible: Arc, - drop_target_helper: IDropTargetHelper, - validation_number: usize, - main_receiver: PriorityQueueReceiver, - platform_window_handle: HWND, - appearance: WindowAppearance, - disable_direct_composition: bool, - directx_devices: DirectXDevices, - invalidate_devices: Arc, - draw_coordinator: Rc, - parent_hwnd: Option, -} - -impl WindowsWindow { - pub(crate) fn new( - handle: AnyWindowHandle, - params: WindowParams, - creation_info: WindowCreationInfo, - ) -> Result { - // Native popups are not implemented on Windows yet. Rejecting lets callers fall back to - // gpui's in-window popovers. - if let WindowKind::AnchoredPopup(_) = params.kind { - return Err(popup::PopupNotSupportedError.into()); - } - - let WindowCreationInfo { - icon, - executor, - current_cursor, - cursor_visible, - drop_target_helper, - validation_number, - main_receiver, - platform_window_handle, - disable_direct_composition, - directx_devices, - invalidate_devices, - draw_coordinator, - } = creation_info; - register_window_class(icon); - let parent_hwnd = if params.kind == WindowKind::Dialog { - let parent_window = unsafe { GetActiveWindow() }; - if parent_window.is_invalid() { - None - } else { - // Disable the parent window to make this dialog modal - unsafe { - EnableWindow(parent_window, false).as_bool(); - }; - Some(parent_window) - } - } else { - None - }; - let hide_title_bar = params - .titlebar - .as_ref() - .map(|titlebar| titlebar.appears_transparent) - .unwrap_or(true); - let window_name = HSTRING::from( - params - .titlebar - .as_ref() - .and_then(|titlebar| titlebar.title.as_ref()) - .map(|title| title.as_ref()) - .unwrap_or(""), - ); - - let (mut dwexstyle, dwstyle) = if params.kind == WindowKind::PopUp { - (WS_EX_TOOLWINDOW | WS_EX_TOPMOST, WINDOW_STYLE(0x0)) - } else { - let mut dwstyle = WS_SYSMENU; - - if params.is_resizable { - dwstyle |= WS_THICKFRAME | WS_MAXIMIZEBOX; - } - - if params.is_minimizable { - dwstyle |= WS_MINIMIZEBOX; - } - let dwexstyle = if params.kind == WindowKind::Dialog { - dwstyle |= WS_POPUP | WS_CAPTION; - WS_EX_DLGMODALFRAME - } else { - WS_EX_APPWINDOW - }; - - (dwexstyle, dwstyle) - }; - if !disable_direct_composition { - dwexstyle |= WS_EX_NOREDIRECTIONBITMAP; - } - - let hinstance = get_module_handle(); - let display = if let Some(display_id) = params.display_id { - WindowsDisplay::new(display_id) - } else { - None - } - .or_else(WindowsDisplay::primary_monitor) - .context("failed to find any monitor")?; - let appearance = system_appearance().unwrap_or_default(); - let mut context = WindowCreateContext { - inner: None, - handle, - hide_title_bar, - display, - is_movable: params.is_movable, - is_resizable: params.is_resizable, - is_minimizable: params.is_minimizable, - min_size: params.window_min_size, - executor, - current_cursor, - cursor_visible, - drop_target_helper, - validation_number, - main_receiver, - platform_window_handle, - appearance, - disable_direct_composition, - directx_devices, - invalidate_devices, - draw_coordinator, - parent_hwnd, - }; - let creation_result = unsafe { - CreateWindowExW( - dwexstyle, - WINDOW_CLASS_NAME, - &window_name, - dwstyle, - CW_USEDEFAULT, - CW_USEDEFAULT, - CW_USEDEFAULT, - CW_USEDEFAULT, - parent_hwnd, - None, - Some(hinstance.into()), - Some(&context as *const _ as *const _), - ) - }; - - // Failure to create a `WindowsWindowState` can cause window creation to fail, - // so check the inner result first. - let this = context.inner.take().transpose()?; - let hwnd = creation_result?; - let this = this.unwrap(); - - register_drag_drop(&this)?; - set_non_rude_hwnd(hwnd, true); - configure_dwm_dark_mode(hwnd, appearance); - this.state.border_offset.update(hwnd)?; - let placement = - retrieve_window_placement(hwnd, display, params.bounds, &this.state.border_offset)?; - if params.show { - let mut placement = placement; - if !params.focus { - placement.showCmd = SW_SHOWNOACTIVATE.0 as u32; - } - unsafe { SetWindowPlacement(hwnd, &placement)? }; - } else { - this.state.initial_placement.set(Some(WindowOpenStatus { - placement, - state: WindowOpenState::Windowed, - })); - } - - Ok(Self(this)) - } -} - -impl rwh::HasWindowHandle for WindowsWindow { - fn window_handle(&self) -> std::result::Result, rwh::HandleError> { - let raw = rwh::Win32WindowHandle::new(unsafe { - NonZeroIsize::new_unchecked(self.0.hwnd.0 as isize) - }) - .into(); - Ok(unsafe { rwh::WindowHandle::borrow_raw(raw) }) - } -} - -impl rwh::HasDisplayHandle for WindowsWindow { - fn display_handle(&self) -> std::result::Result, rwh::HandleError> { - Ok(rwh::DisplayHandle::windows()) - } -} - -impl Drop for WindowsWindow { - fn drop(&mut self) { - // clone this `Rc` to prevent early release of the pointer - let this = self.0.clone(); - self.0 - .executor - .spawn(async move { - let handle = this.hwnd; - unsafe { - RevokeDragDrop(handle).log_err(); - DestroyWindow(handle).log_err(); - } - }) - .detach(); - } -} - -impl PlatformWindow for WindowsWindow { - fn bounds(&self) -> Bounds { - self.state.bounds() - } - - fn is_maximized(&self) -> bool { - self.state.is_maximized() - } - - fn window_bounds(&self) -> WindowBounds { - self.state.window_bounds() - } - - /// get the logical size of the app's drawable area. - /// - /// Currently, GPUI uses the logical size of the app to handle mouse interactions (such as - /// whether the mouse collides with other elements of GPUI). - fn content_size(&self) -> Size { - self.state.content_size() - } - - fn resize(&mut self, size: Size) { - let hwnd = self.0.hwnd; - let bounds = gpui::bounds(self.bounds().origin, size).to_device_pixels(self.scale_factor()); - let rect = calculate_window_rect(bounds, &self.state.border_offset); - - self.0 - .executor - .spawn(async move { - unsafe { - SetWindowPos( - hwnd, - None, - bounds.origin.x.0, - bounds.origin.y.0, - rect.right - rect.left, - rect.bottom - rect.top, - SWP_NOMOVE, - ) - .context("unable to set window content size") - .log_err(); - } - }) - .detach(); - } - - fn scale_factor(&self) -> f32 { - self.state.scale_factor.get() - } - - fn appearance(&self) -> WindowAppearance { - self.state.appearance.get() - } - - fn display(&self) -> Option> { - Some(Rc::new(self.state.display.get())) - } - - fn mouse_position(&self) -> Point { - let scale_factor = self.scale_factor(); - let point = unsafe { - let mut point: POINT = std::mem::zeroed(); - GetCursorPos(&mut point) - .context("unable to get cursor position") - .log_err(); - ScreenToClient(self.0.hwnd, &mut point).ok().log_err(); - point - }; - logical_point(point.x as f32, point.y as f32, scale_factor) - } - - fn modifiers(&self) -> Modifiers { - current_modifiers() - } - - fn capslock(&self) -> Capslock { - current_capslock() - } - - fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { - self.state.input_handler.set(Some(input_handler)); - } - - fn take_input_handler(&mut self) -> Option { - self.state.input_handler.take() - } - - fn prompt( - &self, - level: PromptLevel, - msg: &str, - detail: Option<&str>, - answers: &[PromptButton], - ) -> Option> { - let (done_tx, done_rx) = oneshot::channel(); - let msg = msg.to_string(); - let detail_string = detail.map(|detail| detail.to_string()); - let handle = self.0.hwnd; - let answers = answers.to_vec(); - self.0 - .executor - .spawn(async move { - unsafe { - let mut config = TASKDIALOGCONFIG::default(); - config.cbSize = std::mem::size_of::() as _; - config.hwndParent = handle; - let title; - let main_icon; - match level { - PromptLevel::Info => { - title = windows::core::w!("Info"); - main_icon = TD_INFORMATION_ICON; - } - PromptLevel::Warning => { - title = windows::core::w!("Warning"); - main_icon = TD_WARNING_ICON; - } - PromptLevel::Critical => { - title = windows::core::w!("Critical"); - main_icon = TD_ERROR_ICON; - } - }; - config.pszWindowTitle = title; - config.Anonymous1.pszMainIcon = main_icon; - let instruction = HSTRING::from(msg); - config.pszMainInstruction = PCWSTR::from_raw(instruction.as_ptr()); - let hints_encoded; - if let Some(ref hints) = detail_string { - hints_encoded = HSTRING::from(hints); - config.pszContent = PCWSTR::from_raw(hints_encoded.as_ptr()); - }; - let mut button_id_map = Vec::with_capacity(answers.len()); - let mut buttons = Vec::new(); - let mut btn_encoded = Vec::new(); - for (index, btn) in answers.iter().enumerate() { - let encoded = HSTRING::from(btn.label().as_ref()); - let button_id = match btn { - PromptButton::Ok(_) => IDOK.0, - PromptButton::Cancel(_) => IDCANCEL.0, - // the first few low integer values are reserved for known buttons - // so for simplicity we just go backwards from -1 - PromptButton::Other(_) => -(index as i32) - 1, - }; - button_id_map.push(button_id); - buttons.push(TASKDIALOG_BUTTON { - nButtonID: button_id, - pszButtonText: PCWSTR::from_raw(encoded.as_ptr()), - }); - btn_encoded.push(encoded); - } - config.cButtons = buttons.len() as _; - config.pButtons = buttons.as_ptr(); - - config.pfCallback = None; - let mut res = std::mem::zeroed(); - let _ = TaskDialogIndirect(&config, Some(&mut res), None, None) - .context("unable to create task dialog") - .log_err(); - - if let Some(clicked) = - button_id_map.iter().position(|&button_id| button_id == res) - { - let _ = done_tx.send(clicked); - } - } - }) - .detach(); - - Some(done_rx) - } - - fn activate(&self) { - let hwnd = self.0.hwnd; - let this = self.0.clone(); - self.0 - .executor - .spawn(async move { - this.set_window_placement().log_err(); - - unsafe { - // If the window is minimized, restore it. - if IsIconic(hwnd).as_bool() { - ShowWindowAsync(hwnd, SW_RESTORE).ok().log_err(); - } - - SetActiveWindow(hwnd).ok(); - SetFocus(Some(hwnd)).ok(); - } - - // premium ragebait by windows, this is needed because the window - // must have received an input event to be able to set itself to foreground - // so let's just simulate user input as that seems to be the most reliable way - // some more info: https://gist.github.com/Aetopia/1581b40f00cc0cadc93a0e8ccb65dc8c - // bonus: this bug also doesn't manifest if you have vs attached to the process - let inputs = [ - INPUT { - r#type: INPUT_KEYBOARD, - Anonymous: INPUT_0 { - ki: KEYBDINPUT { - wVk: VK_MENU, - dwFlags: KEYBD_EVENT_FLAGS(0), - ..Default::default() - }, - }, - }, - INPUT { - r#type: INPUT_KEYBOARD, - Anonymous: INPUT_0 { - ki: KEYBDINPUT { - wVk: VK_MENU, - dwFlags: KEYEVENTF_KEYUP, - ..Default::default() - }, - }, - }, - ]; - unsafe { SendInput(&inputs, std::mem::size_of::() as i32) }; - - // todo(windows) - // crate `windows 0.56` reports true as Err - unsafe { SetForegroundWindow(hwnd).as_bool() }; - }) - .detach(); - } - - fn request_attention(&self) { - if self.is_active() { - return; - } - - let hwnd = self.0.hwnd; - self.0 - .executor - .spawn(async move { - let info = FLASHWINFO { - cbSize: std::mem::size_of::() as u32, - hwnd, - dwFlags: FLASHW_ALL, - uCount: 1, - dwTimeout: 0, - }; - unsafe { FlashWindowEx(&info).ok().log_err() }; - }) - .detach(); - } - - fn is_active(&self) -> bool { - self.0.hwnd == unsafe { GetActiveWindow() } - } - - fn is_hovered(&self) -> bool { - self.state.hovered.get() - } - - fn background_appearance(&self) -> WindowBackgroundAppearance { - self.state.background_appearance.get() - } - - fn is_subpixel_rendering_supported(&self) -> bool { - true - } - - fn set_title(&mut self, title: &str) { - unsafe { SetWindowTextW(self.0.hwnd, &HSTRING::from(title)) } - .inspect_err(|e| log::error!("Set title failed: {e}")) - .ok(); - } - - fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { - self.state.background_appearance.set(background_appearance); - let hwnd = self.0.hwnd; - - // using Dwm APIs for Mica and MicaAlt backdrops. - // others follow the set_window_composition_attribute approach - match background_appearance { - WindowBackgroundAppearance::Opaque => { - set_window_composition_attribute(hwnd, None, 0); - } - WindowBackgroundAppearance::Transparent => { - set_window_composition_attribute(hwnd, None, 2); - } - WindowBackgroundAppearance::Blurred => { - set_window_composition_attribute(hwnd, Some((0, 0, 0, 0)), 4); - } - WindowBackgroundAppearance::MicaBackdrop => { - // DWMSBT_MAINWINDOW => MicaBase - dwm_set_window_composition_attribute(hwnd, 2); - } - WindowBackgroundAppearance::MicaAltBackdrop => { - // DWMSBT_TABBEDWINDOW => MicaAlt - dwm_set_window_composition_attribute(hwnd, 4); - } - } - } - - fn minimize(&self) { - unsafe { ShowWindowAsync(self.0.hwnd, SW_MINIMIZE).ok().log_err() }; - } - - fn zoom(&self) { - unsafe { - if IsWindowVisible(self.0.hwnd).as_bool() { - ShowWindowAsync(self.0.hwnd, SW_MAXIMIZE).ok().log_err(); - } else if let Some(mut status) = self.state.initial_placement.take() { - status.state = WindowOpenState::Maximized; - self.state.initial_placement.set(Some(status)); - } - } - } - - fn toggle_fullscreen(&self) { - if unsafe { IsWindowVisible(self.0.hwnd).as_bool() } { - self.0.toggle_fullscreen(); - } else if let Some(mut status) = self.state.initial_placement.take() { - status.state = WindowOpenState::Fullscreen; - self.state.initial_placement.set(Some(status)); - } - } - - fn is_fullscreen(&self) -> bool { - self.state.is_fullscreen() - } - - fn on_request_frame(&self, callback: Box) { - self.state.callbacks.request_frame.set(Some(callback)); - } - - fn on_input(&self, callback: Box DispatchEventResult>) { - self.state.callbacks.input.set(Some(callback)); - } - - fn on_active_status_change(&self, callback: Box) { - self.0 - .state - .callbacks - .active_status_change - .set(Some(callback)); - } - - fn on_hover_status_change(&self, callback: Box) { - self.0 - .state - .callbacks - .hovered_status_change - .set(Some(callback)); - } - - fn on_resize(&self, callback: Box, f32)>) { - self.state.callbacks.resize.set(Some(callback)); - } - - fn on_moved(&self, callback: Box) { - self.state.callbacks.moved.set(Some(callback)); - } - - fn on_should_close(&self, callback: Box bool>) { - self.state.callbacks.should_close.set(Some(callback)); - } - - fn on_close(&self, callback: Box) { - self.state.callbacks.close.set(Some(callback)); - } - - fn on_hit_test_window_control(&self, callback: Box Option>) { - self.0 - .state - .callbacks - .hit_test_window_control - .set(Some(callback)); - } - - fn on_appearance_changed(&self, callback: Box) { - self.0 - .state - .callbacks - .appearance_changed - .set(Some(callback)); - } - - fn draw(&self, scene: &Scene) { - self.state - .renderer - .borrow_mut() - .draw(scene, self.state.background_appearance.get()) - .log_err(); - } - - #[cfg(any(test, feature = "test-support"))] - fn render_to_image(&self, scene: &Scene) -> anyhow::Result { - self.state - .renderer - .borrow_mut() - .render_to_image(scene, self.state.background_appearance.get()) - } - - fn sprite_atlas(&self) -> Arc { - self.state.renderer.borrow().sprite_atlas() - } - - fn get_raw_handle(&self) -> HWND { - self.0.hwnd - } - - fn gpu_specs(&self) -> Option { - self.state.renderer.borrow().gpu_specs().log_err() - } - - fn update_ime_position(&self, bounds: Bounds) { - let scale_factor = self.state.scale_factor.get(); - let caret_position = POINT { - x: (bounds.origin.x.as_f32() * scale_factor) as i32, - y: (bounds.origin.y.as_f32() * scale_factor) as i32 - + ((bounds.size.height.as_f32() * scale_factor) as i32 / 2), - }; - - self.0.update_ime_position(self.0.hwnd, caret_position); - } - - fn play_system_bell(&self) { - // MB_OK: The sound specified as the Windows Default Beep sound. - let _ = unsafe { MessageBeep(MB_OK) }; - } - - fn a11y_init(&self, callbacks: gpui::A11yCallbacks) { - let action_handler = A11yActionHandler(callbacks.action); - let is_focused = unsafe { GetForegroundWindow() } == self.0.hwnd; - - let adapter = accesskit_windows::Adapter::new( - accesskit_windows::HWND(self.0.hwnd.0), - is_focused, - action_handler, - ); - - let activation_handler = A11yActivationHandler { - callback: callbacks.activation, - }; - - *self.state.a11y.borrow_mut() = Some(A11yState { - adapter, - activation_handler, - }); - } - - fn a11y_tree_update(&self, tree_update: accesskit::TreeUpdate) { - let events = { - let mut a11y = self.state.a11y.borrow_mut(); - a11y.as_mut() - .and_then(|a11y| a11y.adapter.update_if_active(|| tree_update)) - }; - // The borrow must be dropped before raising events, because - // `events.raise()` calls `UiaRaiseAutomationPropertyChangedEvent` - // which may send a nested `WM_GETOBJECT` back into this window - // procedure, re-entering `handle_wm_getobject` which also borrows - // `self.state.a11y`. - if let Some(events) = events { - events.raise(); - } - } - - fn a11y_update_window_bounds(&self) { - // Windows UIA handles window bounds tracking automatically. - } -} - -pub(crate) struct A11yState { - pub(crate) adapter: accesskit_windows::Adapter, - pub(crate) activation_handler: A11yActivationHandler, -} - -pub(crate) struct A11yActivationHandler { - callback: Box Option + Send + 'static>, -} - -impl accesskit::ActivationHandler for A11yActivationHandler { - fn request_initial_tree(&mut self) -> Option { - (self.callback)() - } -} - -struct A11yActionHandler(Box); - -impl accesskit::ActionHandler for A11yActionHandler { - fn do_action(&mut self, request: accesskit::ActionRequest) { - (self.0)(request); - } -} - -#[implement(IDropTarget)] -struct WindowsDragDropHandler(pub Rc); - -impl WindowsDragDropHandler { - fn handle_drag_drop(&self, input: PlatformInput) { - if let Some(mut func) = self.0.state.callbacks.input.take() { - func(input); - self.0.state.callbacks.input.set(Some(func)); - } - } -} - -#[allow(non_snake_case)] -impl IDropTarget_Impl for WindowsDragDropHandler_Impl { - fn DragEnter( - &self, - pdataobj: windows::core::Ref, - _grfkeystate: MODIFIERKEYS_FLAGS, - pt: &POINTL, - pdweffect: *mut DROPEFFECT, - ) -> windows::core::Result<()> { - unsafe { - let idata_obj = pdataobj.ok()?; - let config = FORMATETC { - cfFormat: CF_HDROP.0, - ptd: std::ptr::null_mut() as _, - dwAspect: DVASPECT_CONTENT.0, - lindex: -1, - tymed: TYMED_HGLOBAL.0 as _, - }; - let cursor_position = POINT { x: pt.x, y: pt.y }; - if idata_obj.QueryGetData(&config as _) == S_OK { - *pdweffect = DROPEFFECT_COPY; - let Some(mut idata) = idata_obj.GetData(&config as _).log_err() else { - return Ok(()); - }; - if idata.u.hGlobal.is_invalid() { - return Ok(()); - } - let hdrop = HDROP(idata.u.hGlobal.0); - let mut paths = SmallVec::<[PathBuf; 2]>::new(); - with_file_names(hdrop, |file_name| { - if let Some(path) = PathBuf::from_str(&file_name).log_err() { - paths.push(path); - } - }); - ReleaseStgMedium(&mut idata); - let mut cursor_position = cursor_position; - ScreenToClient(self.0.hwnd, &mut cursor_position) - .ok() - .log_err(); - let scale_factor = self.0.state.scale_factor.get(); - let input = PlatformInput::FileDrop(FileDropEvent::Entered { - position: logical_point( - cursor_position.x as f32, - cursor_position.y as f32, - scale_factor, - ), - paths: ExternalPaths(paths), - }); - self.handle_drag_drop(input); - } else { - *pdweffect = DROPEFFECT_NONE; - } - self.0 - .drop_target_helper - .DragEnter(self.0.hwnd, idata_obj, &cursor_position, *pdweffect) - .log_err(); - } - Ok(()) - } - - fn DragOver( - &self, - _grfkeystate: MODIFIERKEYS_FLAGS, - pt: &POINTL, - pdweffect: *mut DROPEFFECT, - ) -> windows::core::Result<()> { - let mut cursor_position = POINT { x: pt.x, y: pt.y }; - unsafe { - *pdweffect = DROPEFFECT_COPY; - self.0 - .drop_target_helper - .DragOver(&cursor_position, *pdweffect) - .log_err(); - ScreenToClient(self.0.hwnd, &mut cursor_position) - .ok() - .log_err(); - } - let scale_factor = self.0.state.scale_factor.get(); - let input = PlatformInput::FileDrop(FileDropEvent::Pending { - position: logical_point( - cursor_position.x as f32, - cursor_position.y as f32, - scale_factor, - ), - }); - self.handle_drag_drop(input); - - Ok(()) - } - - fn DragLeave(&self) -> windows::core::Result<()> { - unsafe { - self.0.drop_target_helper.DragLeave().log_err(); - } - let input = PlatformInput::FileDrop(FileDropEvent::Exited); - self.handle_drag_drop(input); - - Ok(()) - } - - fn Drop( - &self, - pdataobj: windows::core::Ref, - _grfkeystate: MODIFIERKEYS_FLAGS, - pt: &POINTL, - pdweffect: *mut DROPEFFECT, - ) -> windows::core::Result<()> { - let idata_obj = pdataobj.ok()?; - let mut cursor_position = POINT { x: pt.x, y: pt.y }; - unsafe { - *pdweffect = DROPEFFECT_COPY; - self.0 - .drop_target_helper - .Drop(idata_obj, &cursor_position, *pdweffect) - .log_err(); - ScreenToClient(self.0.hwnd, &mut cursor_position) - .ok() - .log_err(); - } - let scale_factor = self.0.state.scale_factor.get(); - let input = PlatformInput::FileDrop(FileDropEvent::Submit { - position: logical_point( - cursor_position.x as f32, - cursor_position.y as f32, - scale_factor, - ), - }); - self.handle_drag_drop(input); - - Ok(()) - } -} - -#[derive(Debug, Clone)] -pub(crate) struct ClickState { - button: Cell, - last_click: Cell, - last_position: Cell>, - double_click_spatial_tolerance_width: Cell, - double_click_spatial_tolerance_height: Cell, - double_click_interval: Cell, - pub(crate) current_count: Cell, -} - -impl ClickState { - pub fn new() -> Self { - let double_click_spatial_tolerance_width = unsafe { GetSystemMetrics(SM_CXDOUBLECLK) }; - let double_click_spatial_tolerance_height = unsafe { GetSystemMetrics(SM_CYDOUBLECLK) }; - let double_click_interval = Duration::from_millis(unsafe { GetDoubleClickTime() } as u64); - - ClickState { - button: Cell::new(MouseButton::Left), - last_click: Cell::new(Instant::now()), - last_position: Cell::new(Point::default()), - double_click_spatial_tolerance_width: Cell::new(double_click_spatial_tolerance_width), - double_click_spatial_tolerance_height: Cell::new(double_click_spatial_tolerance_height), - double_click_interval: Cell::new(double_click_interval), - current_count: Cell::new(0), - } - } - - /// update self and return the needed click count - pub fn update(&self, button: MouseButton, new_position: Point) -> usize { - if self.button.get() == button && self.is_double_click(new_position) { - self.current_count.update(|it| it + 1); - } else { - self.current_count.set(1); - } - self.last_click.set(Instant::now()); - self.last_position.set(new_position); - self.button.set(button); - - self.current_count.get() - } - - pub fn system_update(&self, wparam: usize) { - match wparam { - // SPI_SETDOUBLECLKWIDTH - 29 => self - .double_click_spatial_tolerance_width - .set(unsafe { GetSystemMetrics(SM_CXDOUBLECLK) }), - // SPI_SETDOUBLECLKHEIGHT - 30 => self - .double_click_spatial_tolerance_height - .set(unsafe { GetSystemMetrics(SM_CYDOUBLECLK) }), - // SPI_SETDOUBLECLICKTIME - 32 => self - .double_click_interval - .set(Duration::from_millis(unsafe { GetDoubleClickTime() } as u64)), - _ => {} - } - } - - #[inline] - fn is_double_click(&self, new_position: Point) -> bool { - let diff = self.last_position.get() - new_position; - - self.last_click.get().elapsed() < self.double_click_interval.get() - && diff.x.0.abs() <= self.double_click_spatial_tolerance_width.get() - && diff.y.0.abs() <= self.double_click_spatial_tolerance_height.get() - } -} - -#[derive(Copy, Clone)] -struct StyleAndBounds { - style: WINDOW_STYLE, - x: i32, - y: i32, - cx: i32, - cy: i32, -} - -#[repr(C)] -struct WINDOWCOMPOSITIONATTRIBDATA { - attrib: u32, - pv_data: *mut std::ffi::c_void, - cb_data: usize, -} - -#[repr(C)] -struct AccentPolicy { - accent_state: u32, - accent_flags: u32, - gradient_color: u32, - animation_id: u32, -} - -type Color = (u8, u8, u8, u8); - -#[derive(Debug, Default, Clone)] -pub(crate) struct WindowBorderOffset { - pub(crate) width_offset: Cell, - pub(crate) height_offset: Cell, -} - -impl WindowBorderOffset { - pub(crate) fn update(&self, hwnd: HWND) -> anyhow::Result<()> { - let window_rect = unsafe { - let mut rect = std::mem::zeroed(); - GetWindowRect(hwnd, &mut rect)?; - rect - }; - let client_rect = unsafe { - let mut rect = std::mem::zeroed(); - GetClientRect(hwnd, &mut rect)?; - rect - }; - self.width_offset - .set((window_rect.right - window_rect.left) - (client_rect.right - client_rect.left)); - self.height_offset - .set((window_rect.bottom - window_rect.top) - (client_rect.bottom - client_rect.top)); - Ok(()) - } -} - -#[derive(Clone)] -struct WindowOpenStatus { - placement: WINDOWPLACEMENT, - state: WindowOpenState, -} - -#[derive(Clone, Copy)] -enum WindowOpenState { - Maximized, - Fullscreen, - Windowed, -} - -const WINDOW_CLASS_NAME: PCWSTR = w!("Zed::Window"); - -fn register_window_class(icon_handle: HICON) { - static ONCE: Once = Once::new(); - ONCE.call_once(|| { - let wc = WNDCLASSW { - lpfnWndProc: Some(window_procedure), - hIcon: icon_handle, - lpszClassName: PCWSTR(WINDOW_CLASS_NAME.as_ptr()), - style: CS_HREDRAW | CS_VREDRAW, - hInstance: get_module_handle().into(), - hbrBackground: unsafe { CreateSolidBrush(COLORREF(0x00000000)) }, - ..Default::default() - }; - unsafe { RegisterClassW(&wc) }; - }); -} - -unsafe extern "system" fn window_procedure( - hwnd: HWND, - msg: u32, - wparam: WPARAM, - lparam: LPARAM, -) -> LRESULT { - if msg == WM_NCCREATE { - let window_params = unsafe { &*(lparam.0 as *const CREATESTRUCTW) }; - let window_creation_context = window_params.lpCreateParams as *mut WindowCreateContext; - let window_creation_context = unsafe { &mut *window_creation_context }; - return match WindowsWindowInner::new(window_creation_context, hwnd, window_params) { - Ok(window_state) => { - let weak = Box::new(Rc::downgrade(&window_state)); - unsafe { set_window_long(hwnd, GWLP_USERDATA, Box::into_raw(weak) as isize) }; - window_creation_context.inner = Some(Ok(window_state)); - unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) } - } - Err(error) => { - window_creation_context.inner = Some(Err(error)); - LRESULT(0) - } - }; - } - - let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak; - if ptr.is_null() { - return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }; - } - let inner = unsafe { &*ptr }; - let result = if let Some(inner) = inner.upgrade() { - inner.handle_msg(hwnd, msg, wparam, lparam) - } else { - unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) } - }; - - if msg == WM_NCDESTROY { - unsafe { set_window_long(hwnd, GWLP_USERDATA, 0) }; - unsafe { drop(Box::from_raw(ptr)) }; - } - - result -} - -pub(crate) fn window_from_hwnd(hwnd: HWND) -> Option> { - if hwnd.is_invalid() { - return None; - } - - let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak; - if !ptr.is_null() { - let inner = unsafe { &*ptr }; - inner.upgrade() - } else { - None - } -} - -fn get_module_handle() -> HMODULE { - unsafe { - let mut h_module = std::mem::zeroed(); - GetModuleHandleExW( - GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, - windows::core::w!("ZedModule"), - &mut h_module, - ) - .expect("Unable to get module handle"); // this should never fail - - h_module - } -} - -fn register_drag_drop(window: &Rc) -> Result<()> { - let window_handle = window.hwnd; - let handler = WindowsDragDropHandler(window.clone()); - // The lifetime of `IDropTarget` is handled by Windows, it won't release until - // we call `RevokeDragDrop`. - // So, it's safe to drop it here. - let drag_drop_handler: IDropTarget = handler.into(); - unsafe { - RegisterDragDrop(window_handle, &drag_drop_handler) - .context("unable to register drag-drop event")?; - } - Ok(()) -} - -fn calculate_window_rect(bounds: Bounds, border_offset: &WindowBorderOffset) -> RECT { - // NOTE: - // The reason we're not using `AdjustWindowRectEx()` here is - // that the size reported by this function is incorrect. - // You can test it, and there are similar discussions online. - // See: https://stackoverflow.com/questions/12423584/how-to-set-exact-client-size-for-overlapped-window-winapi - // - // So we manually calculate these values here. - let mut rect = RECT { - left: bounds.left().0, - top: bounds.top().0, - right: bounds.right().0, - bottom: bounds.bottom().0, - }; - let left_offset = border_offset.width_offset.get() / 2; - let top_offset = border_offset.height_offset.get() / 2; - let right_offset = border_offset.width_offset.get() - left_offset; - let bottom_offset = border_offset.height_offset.get() - top_offset; - rect.left -= left_offset; - rect.top -= top_offset; - rect.right += right_offset; - rect.bottom += bottom_offset; - rect -} - -fn calculate_client_rect( - rect: RECT, - border_offset: &WindowBorderOffset, - scale_factor: f32, -) -> Bounds { - let left_offset = border_offset.width_offset.get() / 2; - let top_offset = border_offset.height_offset.get() / 2; - let right_offset = border_offset.width_offset.get() - left_offset; - let bottom_offset = border_offset.height_offset.get() - top_offset; - let left = rect.left + left_offset; - let top = rect.top + top_offset; - let right = rect.right - right_offset; - let bottom = rect.bottom - bottom_offset; - let physical_size = size(DevicePixels(right - left), DevicePixels(bottom - top)); - Bounds { - origin: logical_point(left as f32, top as f32, scale_factor), - size: physical_size.to_pixels(scale_factor), - } -} - -fn retrieve_window_placement( - hwnd: HWND, - display: WindowsDisplay, - initial_bounds: Bounds, - border_offset: &WindowBorderOffset, -) -> Result { - let mut placement = WINDOWPLACEMENT { - length: std::mem::size_of::() as u32, - ..Default::default() - }; - unsafe { GetWindowPlacement(hwnd, &mut placement)? }; - // the bounds may be not inside the display - let bounds = if display.check_given_bounds(initial_bounds) { - initial_bounds - } else { - display.default_bounds() - }; - // `bounds` is expressed in logical pixels for `display`, so it must be converted - // to device pixels using that display's own scale factor. The window's current - // scale factor can't be used here: `CreateWindowExW` was called with - // `CW_USEDEFAULT`, so at this point the window may still be sitting on whichever - // monitor Windows picked by default, which can have a different DPI than `display` - // and would otherwise throw off the physical position (e.g. leaving the window - // partially off-screen when moved to a monitor with a different scale factor). - let bounds = bounds.to_device_pixels(display.scale_factor()); - placement.rcNormalPosition = calculate_window_rect(bounds, border_offset); - Ok(placement) -} - -fn dwm_set_window_composition_attribute(hwnd: HWND, backdrop_type: u32) { - let mut version = unsafe { std::mem::zeroed() }; - let status = unsafe { windows::Wdk::System::SystemServices::RtlGetVersion(&mut version) }; - - // DWMWA_SYSTEMBACKDROP_TYPE is available only on version 22621 or later - // using SetWindowCompositionAttributeType as a fallback - if !status.is_ok() || version.dwBuildNumber < 22621 { - return; - } - - unsafe { - let result = DwmSetWindowAttribute( - hwnd, - DWMWA_SYSTEMBACKDROP_TYPE, - &backdrop_type as *const _ as *const _, - std::mem::size_of_val(&backdrop_type) as u32, - ); - - if !result.is_ok() { - return; - } - } -} - -fn set_window_composition_attribute(hwnd: HWND, color: Option, state: u32) { - let mut version = unsafe { std::mem::zeroed() }; - let status = unsafe { windows::Wdk::System::SystemServices::RtlGetVersion(&mut version) }; - - if !status.is_ok() || version.dwBuildNumber < 17763 { - return; - } - - unsafe { - type SetWindowCompositionAttributeType = - unsafe extern "system" fn(HWND, *mut WINDOWCOMPOSITIONATTRIBDATA) -> BOOL; - let module_name = PCSTR::from_raw(c"user32.dll".as_ptr() as *const u8); - if let Some(user32) = GetModuleHandleA(module_name) - .context("Unable to get user32.dll handle") - .log_err() - { - let func_name = PCSTR::from_raw(c"SetWindowCompositionAttribute".as_ptr() as *const u8); - let Some(raw_set_window_composition_attribute) = GetProcAddress(user32, func_name) - else { - return; - }; - let set_window_composition_attribute: SetWindowCompositionAttributeType = - std::mem::transmute(raw_set_window_composition_attribute); - let mut color = color.unwrap_or_default(); - let is_acrylic = state == 4; - if is_acrylic && color.3 == 0 { - color.3 = 1; - } - let accent = AccentPolicy { - accent_state: state, - accent_flags: if is_acrylic { 0 } else { 2 }, - gradient_color: (color.0 as u32) - | ((color.1 as u32) << 8) - | ((color.2 as u32) << 16) - | ((color.3 as u32) << 24), - animation_id: 0, - }; - let mut data = WINDOWCOMPOSITIONATTRIBDATA { - attrib: 0x13, - pv_data: &accent as *const _ as *mut _, - cb_data: std::mem::size_of::(), - }; - let _ = set_window_composition_attribute(hwnd, &mut data as *mut _ as _); - } - } -} - -// When the platform title bar is hidden, Windows may think that our application is meant to appear 'fullscreen' -// and will stop the taskbar from appearing on top of our window. Prevent this. -// https://devblogs.microsoft.com/oldnewthing/20250522-00/?p=111211 -fn set_non_rude_hwnd(hwnd: HWND, non_rude: bool) { - if non_rude { - unsafe { SetPropW(hwnd, w!("NonRudeHWND"), Some(HANDLE(1 as _))) }.log_err(); - } else { - unsafe { RemovePropW(hwnd, w!("NonRudeHWND")) }.log_err(); - } -} - -#[cfg(test)] -mod tests { - use super::ClickState; - use gpui::{DevicePixels, MouseButton, point}; - use std::time::Duration; - - #[test] - fn test_double_click_interval() { - let state = ClickState::new(); - assert_eq!( - state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(0))), - 1 - ); - assert_eq!( - state.update(MouseButton::Right, point(DevicePixels(0), DevicePixels(0))), - 1 - ); - assert_eq!( - state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(0))), - 1 - ); - assert_eq!( - state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(0))), - 2 - ); - state - .last_click - .update(|it| it - Duration::from_millis(700)); - assert_eq!( - state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(0))), - 1 - ); - } - - #[test] - fn test_double_click_spatial_tolerance() { - let state = ClickState::new(); - assert_eq!( - state.update(MouseButton::Left, point(DevicePixels(-3), DevicePixels(0))), - 1 - ); - assert_eq!( - state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(3))), - 2 - ); - assert_eq!( - state.update(MouseButton::Right, point(DevicePixels(3), DevicePixels(2))), - 1 - ); - assert_eq!( - state.update(MouseButton::Right, point(DevicePixels(10), DevicePixels(0))), - 1 - ); - } -} diff --git a/crates/gpui_pre_windows/src/wrapper.rs b/crates/gpui_pre_windows/src/wrapper.rs deleted file mode 100644 index 60bbc43..0000000 --- a/crates/gpui_pre_windows/src/wrapper.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::ops::Deref; - -use windows::Win32::{Foundation::HWND, UI::WindowsAndMessaging::HCURSOR}; - -#[derive(Debug, Clone, Copy)] -pub(crate) struct SafeCursor { - raw: HCURSOR, -} - -unsafe impl Send for SafeCursor {} -unsafe impl Sync for SafeCursor {} - -impl From for SafeCursor { - fn from(value: HCURSOR) -> Self { - SafeCursor { raw: value } - } -} - -impl Deref for SafeCursor { - type Target = HCURSOR; - - fn deref(&self) -> &Self::Target { - &self.raw - } -} - -#[derive(Debug, Clone, Copy)] -pub(crate) struct SafeHwnd { - raw: HWND, -} - -impl SafeHwnd { - pub(crate) fn as_raw(&self) -> HWND { - self.raw - } -} - -unsafe impl Send for SafeHwnd {} -unsafe impl Sync for SafeHwnd {} - -impl From for SafeHwnd { - fn from(value: HWND) -> Self { - SafeHwnd { raw: value } - } -} - -impl Deref for SafeHwnd { - type Target = HWND; - - fn deref(&self) -> &Self::Target { - &self.raw - } -} diff --git a/crates/gpui_web/Cargo.toml b/crates/gpui_web/Cargo.toml deleted file mode 100644 index 29da556..0000000 --- a/crates/gpui_web/Cargo.toml +++ /dev/null @@ -1,203 +0,0 @@ -# Vendored fork of the published `gpui-pre-web` 0.3.3 crate. -# -# The `src/` tree here is the published 0.3.3 sources verbatim, except for two -# changes in `src/events.rs` -- both marked `HeroGPUI fork:` at the site, both -# recorded in `[package.metadata.herogpui-vendor]` below, and both written up -# for upstream in `docs/upstream/gpui-web-scroll-and-ime.md`. This manifest -# additionally drops `multithreaded` from `default` (see `[features]`), which -# is a build-configuration choice rather than a source change. Nothing else -# differs; a reviewer can confirm that with -# -# diff -r crates/gpui_web/src \ -# ~/.cargo/registry/src/index.crates.io-*/gpui-pre-web-0.3.3/src -# -# `gpui_platform` pulls this crate in automatically on wasm32 (its -# `[target.'cfg(target_family = "wasm")'.dependencies.gpui_web]` table), which -# is the only edge into it -- no workspace crate names `gpui_web`, and adding -# such an edge would buy nothing, because feature sets are unioned and -# `gpui_platform` already enables the default ones. The `default` list in this -# manifest is therefore the only place the `multithreaded` feature can be -# switched. The fork itself is delivered by `[patch.crates-io]` in the -# workspace root. That patch key matters: keying the patch by a git URL, as an -# earlier Zed-git-rev layout did, matches nothing in a registry dependency -# graph and silently applies no override at all. -# -# The `version` above must stay exactly the version upstream `gpui-pre-platform` -# asks for (`gpui-pre-web = "=0.3.3"`). A mismatch makes `[patch.crates-io]` -# stop applying with no error at all: cargo resolves the registry crate, the -# fork's two hunks disappear from the build, and nothing reports it. Check with -# `cargo tree -i gpui-pre-web --target wasm32-unknown-unknown`, which must -# print this path. -# -# Unlike the 1.18.1 crate this replaced, 0.3.3 bundles no fonts -- its -# `WebPlatform` starts with an empty font database and the application is -# expected to call `App::text_system().add_fonts(..)`. `crates/herogpui-web` -# already does exactly that, so the vendored `assets/fonts` tree that the old -# fork carried is gone rather than re-added. -# -# This manifest carries its own `[workspace]` table (as the published crate -# does), which makes it a workspace root of its own rather than a member of -# HeroGPUI's. That is deliberate: it keeps upstream's lint configuration -- -# which the workspace's own `[workspace.lints]` would contradict, starting with -# `unsafe_code = "deny"` -- and keeps this un-owned code out of -# `cargo fmt --all` and `.shots/lint.ps1`. A member would have to be reformatted -# to our rules, and every reformatted line is a line a reviewer must diff by -# hand instead of by `diff -r`. -[package] -name = "gpui-pre-web" -version = "0.3.3" -publish = false -edition = "2024" -license = "Apache-2.0" -autoexamples = false -description = "HeroGPUI's fork of gpui_web 0.3.3: shift+wheel horizontal scroll and an IME mirror resync after paste" -repository = "https://github.com/zed-industries/zed" - -[package.metadata.cargo-shear] -ignored = ["scheduler"] - -# Upstream's provenance stamp, kept verbatim so the snapshot this fork is -# based on stays nameable. -[package.metadata.gpui-pre] -zed-crate = "gpui_web" -zed-version = "0.1.0" -zed-rev = "5b055fa789a8b8d38ac951a6e0cde272f66b4495" - -# The complete record of how this differs from the published crate. The first -# two entries are in `src/events.rs`; together they add 9 lines of code and -# remove 5 (the rest of each hunk is comment). The third is the `default` -# feature list above and touches no source. An incomplete record here is what -# makes a vendored fork unreviewable, so this list is the thing to update first -# when the fork changes. -[package.metadata.herogpui-vendor] -upstream = "https://crates.io/crates/gpui-pre-web" -version = "0.3.3" -changes = [ - "Map shift+wheel to horizontal scroll when the event carries no deltaX, so a mouse can scroll a horizontally scrollable region in the browser (`register_wheel`).", - "Refresh the IME mirror after both the synchronous text paste and the async image/mixed paste, so the hidden textarea does not keep pre-paste text and corrupt the next composition diff (`register_paste`).", - "Drop `multithreaded` from the `default` feature list, so optional `wasm_thread` and its `#![feature]` attribute leave the wasm32 graph and the pinned stable toolchain can build this target. Not a source change; see the comment above `[features]`.", -] - -# The third and last deviation from upstream, and the only one outside -# `src/events.rs`: upstream ships `default = ["multithreaded"]`. -# -# `multithreaded` pulls in optional `wasm_thread`, whose `lib.rs` opens with a -# `#![feature]` attribute, so it forces every wasm32 build of this workspace -- -# including the `wasm-release` artifact and CI's `wasm` job -- onto a nightly -# toolchain with `error[E0554]` from a dependency we do not own. Nothing was -# using it: `crates/herogpui-web` starts the app with -# `gpui_platform::single_threaded_web()`, and the multi-threaded platform needs -# web workers over shared wasm memory, which a browser only grants in a -# cross-origin-isolated context -- something plain GitHub Pages, which serves -# the gallery, sends no COOP/COEP headers for. -# -# This is the only place the feature can be switched. `gpui_platform` declares -# the wasm32 edge into this crate with default features on, and cargo unions -# feature sets, so `default-features = false` on an edge of ours would change -# nothing. -# -# Measured with the feature off: `wasm_thread` leaves the wasm32 graph and the -# lockfile, the pinned stable toolchain builds `-p herogpui-web` at -# `--profile wasm-release`, and the resulting artifact boots, renders, presses, -# focuses, takes keyboard input and resolves a `background_executor().timer()` -# in a browser -- indistinguishably from the nightly/`multithreaded` build, -# which is 23 KB larger. Turn `multithreaded` back on (and go back to nightly) -# only together with a cross-origin-isolated deployment that can actually use -# it. -[features] -default = [] -multithreaded = ["dep:wasm_thread", "scheduler/wasm-threads"] - -[lib] -name = "gpui_web" -path = "src/gpui_web.rs" - -[target.'cfg(target_family = "wasm")'.dependencies] -gpui = { package = "gpui-pre", version = "=0.3.3", default-features = false } -scheduler = { package = "gpui-pre-scheduler", version = "=0.3.3" } -parking_lot = { version = "0.12.1", features = ["nightly"] } -gpui_wgpu = { package = "gpui-pre-wgpu", version = "=0.3.3" } -http_client = { package = "gpui-pre-http-client", version = "=0.3.3" } -anyhow = { version = "1.0.86" } -futures = { version = "0.3.32" } -log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] } -uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] } -wasm-bindgen = { version = "0.2.120" } -wasm-bindgen-futures = { version = "0.4" } -web-time = { version = "1.1.0" } -console_error_panic_hook = { version = "0.1.7" } -js-sys = { version = "0.3" } -raw-window-handle = { version = "0.6" } -wasm_thread = { version = "0.3", features = ["es_modules"], optional = true } -web-sys = { version = "0.3", features = [ - "console", - "Blob", - "Clipboard", - "ClipboardEvent", - "ClipboardItem", - "CompositionEvent", - "CssStyleDeclaration", - "DataTransfer", - "DataTransferItem", - "DataTransferItemList", - "Document", - "DomRect", - "DragEvent", - "Element", - "EventTarget", - "File", - "HtmlCanvasElement", - "HtmlElement", - "HtmlTextAreaElement", - "IdleDeadline", - "IdleRequestOptions", - "InputEvent", - "KeyboardEvent", - "Location", - "MediaQueryList", - "MediaQueryListEvent", - "MouseEvent", - "Navigator", - "PointerEvent", - "ReadableStream", - "ReadableStreamDefaultReader", - "ReadableStreamReadResult", - "ResizeObserver", - "ResizeObserverBoxOptions", - "ResizeObserverEntry", - "ResizeObserverSize", - "ResizeObserverOptions", - "Screen", - "Storage", - "VisualViewport", - "Headers", - "Request", - "RequestCredentials", - "RequestInit", - "RequestRedirect", - "Response", - "WheelEvent", - "Window", -] } - -[lints.rust.unexpected_cfgs] -level = "allow" - -[lints.clippy] -dbg_macro = "deny" -todo = "deny" -declare_interior_mutable_const = "deny" -redundant_clone = "deny" -disallowed_methods = "deny" -type_complexity = "allow" -let_underscore_future = "allow" -single_range_in_vec_init = "allow" -too_many_arguments = "allow" -large_enum_variant = "allow" -nonminimal_bool = "allow" - -[lints.clippy.style] -level = "allow" -priority = -1 - -[workspace] diff --git a/crates/gpui_web/LICENSE-APACHE b/crates/gpui_web/LICENSE-APACHE deleted file mode 100644 index 461a0fe..0000000 --- a/crates/gpui_web/LICENSE-APACHE +++ /dev/null @@ -1,222 +0,0 @@ -Copyright 2022 - 2025 Zed Industries, Inc. - - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - - http://www.apache.org/licenses/LICENSE-2.0 - - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - - - -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 diff --git a/crates/gpui_web/src/dispatcher.rs b/crates/gpui_web/src/dispatcher.rs deleted file mode 100644 index 37ba891..0000000 --- a/crates/gpui_web/src/dispatcher.rs +++ /dev/null @@ -1,435 +0,0 @@ -use gpui::{ - PlatformDispatcher, Priority, PriorityQueueReceiver, PriorityQueueSender, RunnableVariant, -}; -use std::cell::{Cell, RefCell}; -use std::sync::Arc; -use std::sync::atomic::AtomicI32; -use std::time::Duration; -use wasm_bindgen::prelude::*; -use web_time::Instant; - -#[cfg(feature = "multithreaded")] -const MIN_BACKGROUND_THREADS: usize = 2; - -fn shared_memory_supported() -> bool { - let global = js_sys::global(); - let has_shared_array_buffer = - js_sys::Reflect::has(&global, &JsValue::from_str("SharedArrayBuffer")).unwrap_or(false); - let has_atomics = js_sys::Reflect::has(&global, &JsValue::from_str("Atomics")).unwrap_or(false); - let memory = js_sys::WebAssembly::Memory::from(wasm_bindgen::memory()); - let buffer = memory.buffer(); - let is_shared_buffer = buffer.is_instance_of::(); - has_shared_array_buffer && has_atomics && is_shared_buffer -} - -fn wait_async_supported() -> bool { - let global = js_sys::global(); - let Ok(atomics) = js_sys::Reflect::get(&global, &JsValue::from_str("Atomics")) else { - return false; - }; - let Ok(wait_async) = js_sys::Reflect::get(&atomics, &JsValue::from_str("waitAsync")) else { - return false; - }; - - wait_async.is_function() -} - -enum MainThreadItem { - Runnable(RunnableVariant), - Delayed { - runnable: RunnableVariant, - millis: i32, - }, - Idle { - runnable: RunnableVariant, - timeout: Option, - }, - Function(Box), - // TODO-Wasm: Shouldn't these run on their own dedicated thread? - RealtimeFunction(Box), -} - -struct MainThreadMailbox { - sender: PriorityQueueSender, - receiver: parking_lot::Mutex>, - signal: AtomicI32, -} - -impl MainThreadMailbox { - fn new() -> Self { - let (sender, receiver) = PriorityQueueReceiver::new(); - Self { - sender, - receiver: parking_lot::Mutex::new(receiver), - signal: AtomicI32::new(0), - } - } - - fn post(&self, priority: Priority, item: MainThreadItem) { - if self.sender.spin_send(priority, item).is_err() { - log::error!("MainThreadMailbox::send failed: receiver disconnected"); - } - - // TODO-Wasm: Verify this lock-free protocol - let view = self.signal_view(); - js_sys::Atomics::store(&view, 0, 1).ok(); - js_sys::Atomics::notify(&view, 0).ok(); - } - - fn drain(&self, window: &web_sys::Window) { - let mut receiver = self.receiver.lock(); - loop { - // We need these `spin` variants because we can't acquire a lock on the main thread. - // TODO-WASM: Should we do something different? - match receiver.spin_try_pop() { - Ok(Some(item)) => execute_on_main_thread(window, item), - Ok(None) => break, - Err(_) => break, - } - } - } - - fn signal_view(&self) -> js_sys::Int32Array { - let byte_offset = self.signal.as_ptr() as u32; - let memory = js_sys::WebAssembly::Memory::from(wasm_bindgen::memory()); - js_sys::Int32Array::new_with_byte_offset_and_length(&memory.buffer(), byte_offset, 1) - } - - fn run_waker_loop(self: &Arc, window: web_sys::Window) { - if !shared_memory_supported() { - log::warn!("SharedArrayBuffer not available; main thread mailbox waker loop disabled"); - return; - } - - let mailbox = Arc::clone(self); - wasm_bindgen_futures::spawn_local(async move { - let view = mailbox.signal_view(); - loop { - js_sys::Atomics::store(&view, 0, 0).expect("Atomics.store failed"); - - // Items posted between the previous drain and the store above - // set the signal we just cleared, so their notify is lost. - // Drain again after re-arming to avoid missing them. - mailbox.drain(&window); - - let result = match js_sys::Atomics::wait_async(&view, 0, 0) { - Ok(result) => result, - Err(error) => { - log::error!("Atomics.waitAsync failed: {error:?}"); - break; - } - }; - - let is_async = js_sys::Reflect::get(&result, &JsValue::from_str("async")) - .ok() - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - // `async: false` means the signal changed between the store and - // the wait ("not-equal"): work has already arrived, so skip - // waiting and drain immediately. - if is_async { - let promise: js_sys::Promise = - js_sys::Reflect::get(&result, &JsValue::from_str("value")) - .expect("waitAsync result missing 'value'") - .unchecked_into(); - - let _ = wasm_bindgen_futures::JsFuture::from(promise).await; - } - - mailbox.drain(&window); - } - }); - } -} - -pub struct WebDispatcher { - main_thread_id: std::thread::ThreadId, - background_sender: PriorityQueueSender, - main_thread_mailbox: Arc, - supports_threads: bool, - #[cfg(feature = "multithreaded")] - _background_threads: Vec>, -} - -impl WebDispatcher { - pub fn new(browser_window: web_sys::Window, allow_threads: bool) -> Self { - #[cfg(feature = "multithreaded")] - let (background_sender, background_receiver) = PriorityQueueReceiver::new(); - #[cfg(not(feature = "multithreaded"))] - let (background_sender, _) = PriorityQueueReceiver::new(); - - let main_thread_mailbox = Arc::new(MainThreadMailbox::new()); - - let supports_threads = cfg!(feature = "multithreaded") - && allow_threads - && shared_memory_supported() - && wait_async_supported(); - - if supports_threads { - main_thread_mailbox.run_waker_loop(browser_window.clone()); - } else if cfg!(feature = "multithreaded") && allow_threads { - log::warn!( - "Required WebAssembly threading APIs are unavailable; falling back to single-threaded dispatcher" - ); - } - - #[cfg(feature = "multithreaded")] - let background_threads = if supports_threads { - let thread_count = browser_window - .navigator() - .hardware_concurrency() - .max(MIN_BACKGROUND_THREADS as f64) as usize; - - // TODO-Wasm: Is it bad to have web workers blocking for a long time like this? - (0..thread_count) - .map(|i| { - let mut receiver = background_receiver.clone(); - wasm_thread::Builder::new() - .name(format!("background-worker-{i}")) - .spawn(move || { - loop { - let runnable: RunnableVariant = match receiver.pop() { - Ok(runnable) => runnable, - Err(_) => { - log::info!( - "background-worker-{i}: channel disconnected, exiting" - ); - break; - } - }; - - runnable.run(); - } - }) - .expect("failed to spawn background worker thread") - }) - .collect::>() - } else { - Vec::new() - }; - - Self { - main_thread_id: std::thread::current().id(), - background_sender, - main_thread_mailbox, - supports_threads, - #[cfg(feature = "multithreaded")] - _background_threads: background_threads, - } - } - - fn on_main_thread(&self) -> bool { - std::thread::current().id() == self.main_thread_id - } - - pub(crate) fn dispatch_function_on_main_thread( - &self, - function: impl FnOnce() + Send + 'static, - ) { - if self.on_main_thread() { - let callback = Closure::once_into_js(function); - browser_window().queue_microtask(callback.unchecked_ref()); - } else { - self.main_thread_mailbox - .post(Priority::High, MainThreadItem::Function(Box::new(function))); - } - } -} - -impl PlatformDispatcher for WebDispatcher { - fn is_main_thread(&self) -> bool { - self.on_main_thread() - } - - fn dispatch(&self, runnable: RunnableVariant, priority: Priority) { - if !self.supports_threads { - self.dispatch_on_main_thread(runnable, priority); - return; - } - - let result = if self.on_main_thread() { - self.background_sender.spin_send(priority, runnable) - } else { - self.background_sender.send(priority, runnable) - }; - - if let Err(error) = result { - log::error!("dispatch: failed to send to background queue: {error:?}"); - } - } - - fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority) { - if self.on_main_thread() { - schedule_runnable(&browser_window(), runnable, priority); - } else { - self.main_thread_mailbox - .post(priority, MainThreadItem::Runnable(runnable)); - } - } - - fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant) { - let millis = duration.as_millis().min(i32::MAX as u128) as i32; - if self.on_main_thread() { - let callback = Closure::once_into_js(move || { - runnable.run(); - }); - browser_window() - .set_timeout_with_callback_and_timeout_and_arguments_0( - callback.unchecked_ref(), - millis, - ) - .ok(); - } else { - self.main_thread_mailbox - .post(Priority::High, MainThreadItem::Delayed { runnable, millis }); - } - } - - fn spawn_realtime(&self, function: Box) { - if self.on_main_thread() { - let callback = Closure::once_into_js(move || { - function(); - }); - browser_window().queue_microtask(callback.unchecked_ref()); - } else { - self.main_thread_mailbox - .post(Priority::High, MainThreadItem::RealtimeFunction(function)); - } - } - - fn dispatch_on_main_thread_when_idle( - &self, - runnable: RunnableVariant, - timeout: Option, - ) { - if self.on_main_thread() { - schedule_idle_runnable(&browser_window(), runnable, timeout); - } else { - self.main_thread_mailbox - .post(Priority::Low, MainThreadItem::Idle { runnable, timeout }); - } - } - - fn idle_time_remaining(&self) -> Option { - if !self.on_main_thread() { - return None; - } - IDLE_DEADLINE.with(|deadline| { - deadline - .borrow() - .as_ref() - .map(|deadline| Duration::from_secs_f64(deadline.time_remaining() / 1000.0)) - }) - } - - fn now(&self) -> Instant { - Instant::now() - } -} - -fn browser_window() -> web_sys::Window { - web_sys::window().expect("must be running in a browser window context") -} - -fn execute_on_main_thread(window: &web_sys::Window, item: MainThreadItem) { - match item { - MainThreadItem::Runnable(runnable) => { - runnable.run(); - } - MainThreadItem::Idle { runnable, timeout } => { - schedule_idle_runnable(window, runnable, timeout); - } - MainThreadItem::Delayed { runnable, millis } => { - let callback = Closure::once_into_js(move || { - runnable.run(); - }); - window - .set_timeout_with_callback_and_timeout_and_arguments_0( - callback.unchecked_ref(), - millis, - ) - .ok(); - } - MainThreadItem::Function(function) | MainThreadItem::RealtimeFunction(function) => { - function(); - } - } -} - -thread_local! { - /// The deadline of the idle callback currently running; read by - /// [`PlatformDispatcher::idle_time_remaining`] from inside the runnable. - static IDLE_DEADLINE: RefCell> = const { RefCell::new(None) }; - /// Whether `requestIdleCallback` exists (it is absent on Safari), probed - /// on first use. - static IDLE_CALLBACK_SUPPORTED: Cell> = const { Cell::new(None) }; -} - -/// Registers one `requestIdleCallback` per runnable, mirroring how -/// `dispatch_after` maps each timer to its own platform alarm. The browser -/// already provides the queue semantics a central pump would rebuild: idle -/// callbacks run in registration order, an idle period drains as many as its -/// deadline allows, and a callback whose `timeout` expires is posted as an -/// ordinary task instead. -fn schedule_idle_runnable( - window: &web_sys::Window, - runnable: RunnableVariant, - timeout: Option, -) { - if !idle_callback_supported(window) { - // Safari: run idle work as ordinary macrotasks. With no metered - // deadline, `idle_time_remaining` stays `None` and idle tasks bound - // their own slices. - schedule_runnable(window, runnable, Priority::Low); - return; - } - let callback = Closure::once_into_js(move |deadline: web_sys::IdleDeadline| { - IDLE_DEADLINE.with(|current| *current.borrow_mut() = Some(deadline)); - runnable.run(); - IDLE_DEADLINE.with(|current| *current.borrow_mut() = None); - }); - let result = match timeout { - Some(timeout) => { - let options = web_sys::IdleRequestOptions::new(); - options.set_timeout(timeout.as_millis().min(u32::MAX as u128) as u32); - window.request_idle_callback_with_options(callback.unchecked_ref(), &options) - } - None => window.request_idle_callback(callback.unchecked_ref()), - }; - if let Err(error) = result { - log::error!("requestIdleCallback failed: {error:?}"); - } -} - -fn idle_callback_supported(window: &web_sys::Window) -> bool { - IDLE_CALLBACK_SUPPORTED.with(|supported| { - if let Some(supported) = supported.get() { - return supported; - } - let probed = - js_sys::Reflect::has(window.as_ref(), &JsValue::from_str("requestIdleCallback")) - .unwrap_or(false); - supported.set(Some(probed)); - probed - }) -} - -fn schedule_runnable(window: &web_sys::Window, runnable: RunnableVariant, priority: Priority) { - let callback = Closure::once_into_js(move || { - runnable.run(); - }); - let callback: &js_sys::Function = callback.unchecked_ref(); - - match priority { - Priority::RealtimeAudio => { - window.queue_microtask(callback); - } - _ => { - // TODO-Wasm: this ought to enqueue so we can dequeue with proper priority - window - .set_timeout_with_callback_and_timeout_and_arguments_0(callback, 0) - .ok(); - } - } -} diff --git a/crates/gpui_web/src/display.rs b/crates/gpui_web/src/display.rs deleted file mode 100644 index aaf70c6..0000000 --- a/crates/gpui_web/src/display.rs +++ /dev/null @@ -1,101 +0,0 @@ -use anyhow::Result; -use gpui::{Bounds, DisplayId, Pixels, PlatformDisplay, Point, Size, px}; - -#[derive(Debug)] -pub struct WebDisplay { - id: DisplayId, - uuid: uuid::Uuid, - browser_window: web_sys::Window, -} - -// Safety: `web_sys::Window` is only accessed from the main thread. Displays -// are handed out as `Rc` and read by GPUI on the -// foreground thread; background worker threads (when the `multithreaded` -// feature is enabled) never touch them. -unsafe impl Send for WebDisplay {} -unsafe impl Sync for WebDisplay {} - -impl WebDisplay { - pub fn new(browser_window: web_sys::Window) -> Self { - WebDisplay { - id: DisplayId::new(1), - uuid: uuid::Uuid::new_v4(), - browser_window, - } - } - - fn screen_size(&self) -> Size { - let Some(screen) = self.browser_window.screen().ok() else { - return Size { - width: px(1920.), - height: px(1080.), - }; - }; - - let width = screen.width().unwrap_or(1920) as f32; - let height = screen.height().unwrap_or(1080) as f32; - - Size { - width: px(width), - height: px(height), - } - } - - fn viewport_size(&self) -> Size { - let width = self - .browser_window - .inner_width() - .ok() - .and_then(|v| v.as_f64()) - .unwrap_or(1920.0) as f32; - let height = self - .browser_window - .inner_height() - .ok() - .and_then(|v| v.as_f64()) - .unwrap_or(1080.0) as f32; - - Size { - width: px(width), - height: px(height), - } - } -} - -impl PlatformDisplay for WebDisplay { - fn id(&self) -> DisplayId { - self.id - } - - fn uuid(&self) -> Result { - Ok(self.uuid) - } - - fn bounds(&self) -> Bounds { - let size = self.screen_size(); - Bounds { - origin: Point::default(), - size, - } - } - - fn visible_bounds(&self) -> Bounds { - let size = self.viewport_size(); - Bounds { - origin: Point::default(), - size, - } - } - - fn default_bounds(&self) -> Bounds { - let visible = self.visible_bounds(); - let width = visible.size.width * 0.75; - let height = visible.size.height * 0.75; - let origin_x = (visible.size.width - width) / 2.0; - let origin_y = (visible.size.height - height) / 2.0; - Bounds { - origin: Point::new(origin_x, origin_y), - size: Size { width, height }, - } - } -} diff --git a/crates/gpui_web/src/events.rs b/crates/gpui_web/src/events.rs deleted file mode 100644 index 26ed3be..0000000 --- a/crates/gpui_web/src/events.rs +++ /dev/null @@ -1,1494 +0,0 @@ -use std::{collections::HashMap, rc::Rc}; - -use gpui::{ - Capslock, ClipboardEntry, ClipboardItem, ClipboardString, DispatchEventResult, GestureTuning, - Image, ImageFormat, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, - MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, - Pixels, PlatformInput, Point, ScrollDelta, ScrollWheelEvent, TouchEvent, TouchId, TouchPhase, - point, px, -}; -use wasm_bindgen::prelude::*; - -use crate::ime_mirror::ImeMirror; -use crate::window::WebWindowInner; - -pub struct WebEventListeners { - _handles: Vec, -} - -/// A DOM event listener that is removed from its target when dropped. -/// -/// Dropping the `Closure` alone would leave the listener attached to the DOM -/// pointing at a freed function; the next event would then throw "closure -/// invoked after being dropped". Keeping the target alongside the closure -/// lets `Drop` unregister the listener first. -pub(crate) struct EventListenerHandle { - target: web_sys::EventTarget, - event_name: &'static str, - closure: Closure, -} - -impl EventListenerHandle { - pub(crate) fn add( - target: &web_sys::EventTarget, - event_name: &'static str, - handler: impl FnMut(JsValue) + 'static, - ) -> Self { - let closure = Closure::::new(handler); - target - .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref()) - .ok(); - Self { - target: target.clone(), - event_name, - closure, - } - } - - /// Registers with `{passive: false}` so that `preventDefault()` works. - /// Needed for events like `wheel` which are passive by default in modern - /// browsers. Removal does not need to match the `passive` option, so - /// `Drop` works the same as for [`EventListenerHandle::add`]. - fn add_non_passive( - target: &web_sys::EventTarget, - event_name: &'static str, - handler: impl FnMut(JsValue) + 'static, - ) -> Self { - let closure = Closure::::new(handler); - let target_js: &JsValue = target.as_ref(); - let callback_js: &JsValue = closure.as_ref(); - let options = js_sys::Object::new(); - js_sys::Reflect::set(&options, &"passive".into(), &false.into()).ok(); - if let Ok(add_fn_val) = js_sys::Reflect::get(target_js, &"addEventListener".into()) { - if let Ok(add_fn) = add_fn_val.dyn_into::() { - add_fn - .call3(target_js, &event_name.into(), callback_js, &options) - .ok(); - } - } - Self { - target: target.clone(), - event_name, - closure, - } - } -} - -impl Drop for EventListenerHandle { - fn drop(&mut self) { - self.target - .remove_event_listener_with_callback( - self.event_name, - self.closure.as_ref().unchecked_ref(), - ) - .ok(); - } -} - -pub(crate) struct ClickState { - last_position: Point, - last_time: f64, - current_count: usize, -} - -#[derive(Default)] -pub(crate) struct TouchIds { - next: u64, - active: HashMap, -} - -impl TouchIds { - fn start(&mut self, pointer_id: i32) -> Option { - let next = self.next.checked_add(1)?; - let touch_id = TouchId(self.next); - self.next = next; - self.active.insert(pointer_id, touch_id); - Some(touch_id) - } - - fn active(&self, pointer_id: i32) -> Option { - self.active.get(&pointer_id).copied() - } - - fn end(&mut self, pointer_id: i32) -> Option { - self.active.remove(&pointer_id) - } -} - -impl Default for ClickState { - fn default() -> Self { - Self { - last_position: Point::default(), - last_time: 0.0, - current_count: 0, - } - } -} - -impl ClickState { - fn register_click(&mut self, position: Point, time: f64) -> usize { - let distance = ((f32::from(position.x) - f32::from(self.last_position.x)).powi(2) - + (f32::from(position.y) - f32::from(self.last_position.y)).powi(2)) - .sqrt(); - - if (time - self.last_time) < 400.0 && distance < 5.0 { - self.current_count += 1; - } else { - self.current_count = 1; - } - - self.last_position = position; - self.last_time = time; - self.current_count - } -} - -impl WebWindowInner { - pub fn register_event_listeners(self: &Rc) -> WebEventListeners { - let mut handles = vec![ - self.register_pointer_down(), - self.register_pointer_up(), - self.register_pointer_cancel(), - self.register_touch_end(), - self.register_pointer_move(), - self.register_pointer_leave(), - self.register_wheel(), - self.register_context_menu(), - self.register_dragover(), - self.register_drop(), - self.register_key_down(), - self.register_key_up(), - self.register_before_input(), - self.register_input(), - self.register_paste(), - self.register_composition_start(), - self.register_composition_update(), - self.register_composition_end(), - self.register_focus(), - self.register_blur(), - self.register_pointer_enter(), - ]; - handles.extend(self.register_selection_change()); - handles.extend(self.register_visibility_change()); - handles.extend(self.register_appearance_change()); - handles.extend(self.register_fullscreen_change()); - - WebEventListeners { _handles: handles } - } - - fn listen( - self: &Rc, - event_name: &'static str, - handler: impl FnMut(JsValue) + 'static, - ) -> EventListenerHandle { - EventListenerHandle::add(self.canvas.as_ref(), event_name, handler) - } - - fn listen_input( - self: &Rc, - event_name: &'static str, - handler: impl FnMut(JsValue) + 'static, - ) -> EventListenerHandle { - EventListenerHandle::add(self.ime_mirror.event_target(), event_name, handler) - } - - fn listen_non_passive( - self: &Rc, - event_name: &'static str, - handler: impl FnMut(JsValue) + 'static, - ) -> EventListenerHandle { - EventListenerHandle::add_non_passive(self.canvas.as_ref(), event_name, handler) - } - - fn dispatch_input(&self, input: PlatformInput) -> Option { - self.with_callback(|callbacks| &mut callbacks.input, |callback| callback(input)) - } - - /// Records the latest modifier state and reports whether it changed, so - /// that `ModifiersChanged` is only dispatched on actual transitions - /// rather than for every key event. - fn update_modifiers(&self, modifiers: Modifiers, capslock: Capslock) -> bool { - let mut current_state = self.state.borrow_mut(); - let changed = current_state.modifiers != modifiers || current_state.capslock != capslock; - current_state.modifiers = modifiers; - current_state.capslock = capslock; - changed - } - - fn register_pointer_down(self: &Rc) -> EventListenerHandle { - let this = Rc::clone(self); - self.listen("pointerdown", move |event: JsValue| { - let event: web_sys::PointerEvent = event.unchecked_into(); - event.prevent_default(); - - let pointer_type = event.pointer_type(); - let position = pointer_position_in_element(&event); - this.gesture_start_visual_viewport_height - .set(this.visual_viewport_height()); - - // Capture the pointer so drags that leave the canvas keep - // delivering pointermove/pointerup here; otherwise a release - // outside the canvas is never seen and `pressed_button` stays - // stuck. The capture is released implicitly on pointerup. - this.canvas.set_pointer_capture(event.pointer_id()).ok(); - - if pointer_type == "touch" { - let Some(touch_id) = this.touch_ids.borrow_mut().start(event.pointer_id()) else { - log::error!("exhausted touch identifiers"); - return; - }; - this.state.borrow_mut().mouse_position = position; - if this.touch_tap_candidate.get().is_none() { - this.touch_tap_candidate - .set(Some((event.pointer_id(), position))); - } - this.dispatch_input(PlatformInput::Touch(TouchEvent { - id: touch_id, - phase: TouchPhase::Started, - position, - predicted_position: None, - force: None, - })); - // Keyboard and IME focus intentionally do not change here: - // whether this touch is a tap or a pan is only known at - // release, and only a tap may affect them (see - // `touch_tap_candidate`). The release handler still runs - // within a user gesture, as keyboard summoning requires. - return; - } - - let button = dom_mouse_button_to_gpui(event.button()); - let modifiers = modifiers_from_mouse_event(&event, this.is_mac); - let time = js_sys::Date::now(); - - this.pressed_button.set(Some(button)); - let click_count = this.click_state.borrow_mut().register_click(position, time); - - { - let mut current_state = this.state.borrow_mut(); - current_state.mouse_position = position; - current_state.modifiers = modifiers; - } - - this.dispatch_input(PlatformInput::MouseDown(MouseDownEvent { - button, - position, - modifiers, - click_count, - first_mouse: false, - })); - - this.ime_mirror.focus(); - }) - } - - fn pointer_targets_text_input(&self, position: Point) -> bool { - self.with_input_handler(|handler| { - handler.query_accepts_text_input() - && handler - .element_bounds() - .is_some_and(|bounds| bounds.contains(&position)) - }) - .unwrap_or(false) - } - - fn focused_input_accepts_text(&self) -> bool { - self.with_input_handler(|handler| handler.query_accepts_text_input()) - .unwrap_or(false) - } - - fn register_pointer_up(self: &Rc) -> EventListenerHandle { - let this = Rc::clone(self); - self.listen("pointerup", move |event: JsValue| { - let event: web_sys::PointerEvent = event.unchecked_into(); - event.prevent_default(); - - let position = pointer_position_in_element(&event); - - if event.pointer_type() == "touch" { - let Some(touch_id) = this.touch_ids.borrow_mut().end(event.pointer_id()) else { - return; - }; - this.state.borrow_mut().mouse_position = position; - let completes_tap = match this.touch_tap_candidate.get() { - Some((pointer_id, _)) if pointer_id == event.pointer_id() => { - this.touch_tap_candidate.set(None); - true - } - _ => false, - }; - let focused_input_accepted_text_before_tap = this.focused_input_accepts_text(); - // A recognized tap is dispatched synchronously inside this - // call, so the text-input check below sees the state the tap - // produced. - let dispatch_result = this.dispatch_input(PlatformInput::Touch(TouchEvent { - id: touch_id, - phase: TouchPhase::Ended, - position, - predicted_position: None, - force: None, - })); - - // A keyboard opening or closing mid-gesture reflows the - // layout, so the release position no longer refers to the - // content the user aimed at (a tap that summoned the keyboard - // often ends up below the shrunken layout, which would - // immediately dismiss it again). Skip the sync then, and for - // anything that wasn't a tap: pans and flings must not move - // keyboard or IME focus at all. - let viewport_stable = this.gesture_start_visual_viewport_height.get() - == this.visual_viewport_height(); - if completes_tap && viewport_stable { - let preserve_focused_input = should_preserve_focused_input( - focused_input_accepted_text_before_tap, - this.focused_input_accepts_text(), - dispatch_result, - ); - if !preserve_focused_input { - this.sync_virtual_keyboard(this.pointer_targets_text_input(position)); - } - } - this.schedule_ime_mirror_sync(); - return; - } - - let button = dom_mouse_button_to_gpui(event.button()); - let modifiers = modifiers_from_mouse_event(&event, this.is_mac); - - this.pressed_button.set(None); - let click_count = this.click_state.borrow().current_count; - - { - let mut current_state = this.state.borrow_mut(); - current_state.mouse_position = position; - current_state.modifiers = modifiers; - } - - this.dispatch_input(PlatformInput::MouseUp(MouseUpEvent { - button, - position, - modifiers, - click_count, - })); - - this.schedule_ime_mirror_sync(); - }) - } - - /// The visual viewport's current height in layout pixels, or zero when - /// the API is unavailable. - fn visual_viewport_height(&self) -> f64 { - self.browser_window - .visual_viewport() - .map_or(0.0, |viewport| viewport.height() * viewport.scale()) - } - - /// Whether the software keyboard is likely hidden — a heuristic, since - /// no cross-browser keyboard-visibility signal exists. It infers from - /// the visual viewport: a shown keyboard shrinks its height well below - /// the greatest height seen at the current width (the width only changes - /// on rotation, which restarts the calibration). `window.innerHeight` - /// can't serve as the reference because Android shrinks it along with - /// the keyboard. Unknown states err toward "visible" so ordinary - /// editable taps don't gratuitously restart the IME session. - /// - /// Restricted to coarse-pointer environments: elsewhere (desktop - /// browsers, including touchscreen laptops) viewport height tracks - /// user window resizes rather than a software keyboard, so the - /// calibration would misfire. Split-screen resizes on mobile can still - /// fool it; tracking `visualViewport` resize events around focus - /// transitions would be sturdier. - fn keyboard_likely_dismissed(&self) -> bool { - let coarse_pointer = self - .browser_window - .match_media("(pointer: coarse)") - .ok() - .flatten() - .is_some_and(|media_query_list| media_query_list.matches()); - if !coarse_pointer { - return false; - } - let Some(viewport) = self.browser_window.visual_viewport() else { - return false; - }; - let width = viewport.width() * viewport.scale(); - let height = viewport.height() * viewport.scale(); - let (probe_width, probe_height) = self.visual_viewport_probe.get(); - let max_height = if width == probe_width { - probe_height.max(height) - } else { - height - }; - self.visual_viewport_probe.set((width, max_height)); - height >= max_height * 0.85 - } - - /// The browser or OS took over the pointer (native scrolling, a system - /// gesture, the pointer being removed): no pointerup will follow, so the - /// gesture must unwind rather than complete. - fn register_pointer_cancel(self: &Rc) -> EventListenerHandle { - let this = Rc::clone(self); - self.listen("pointercancel", move |event: JsValue| { - let event: web_sys::PointerEvent = event.unchecked_into(); - if event.pointer_type() == "touch" { - let Some(touch_id) = this.touch_ids.borrow_mut().end(event.pointer_id()) else { - return; - }; - if let Some((pointer_id, _)) = this.touch_tap_candidate.get() - && pointer_id == event.pointer_id() - { - this.touch_tap_candidate.set(None); - } - this.dispatch_input(PlatformInput::Touch(TouchEvent { - id: touch_id, - phase: TouchPhase::Cancelled, - position: pointer_position_in_element(&event), - predicted_position: None, - force: None, - })); - } else { - this.pressed_button.set(None); - } - }) - } - - /// Cancels touch default handling separately because iOS does not consistently - /// transfer pointer-event cancellation to the corresponding touch event. - fn register_touch_end(self: &Rc) -> EventListenerHandle { - self.listen_non_passive("touchend", move |event: JsValue| { - let event: web_sys::Event = event.unchecked_into(); - event.prevent_default(); - }) - } - - /// See [`ImeMirror::schedule_sync`]. - fn schedule_ime_mirror_sync(self: &Rc) { - ImeMirror::schedule_sync(self); - } - - /// Aligns the software keyboard with the text input targeted by a touch tap. - /// - /// Mobile browsers show the keyboard only when an editable element is - /// focused from within a user gesture, so this runs while the tap's - /// `pointerup` is still on the stack (by which point GPUI has usually - /// painted a frame since the `MouseDown`, so the input handler reflects - /// the tap's focus change). `readOnly` suppresses the keyboard while - /// keeping the hidden input available to the IME. Leaving it blurred after - /// a non-editable tap lets the next editable tap establish a new input - /// session instead of relying on a same-task blur/focus cycle, which iOS - /// may coalesce. - /// - /// We don't use `navigator.virtualKeyboard` here because it's - /// Chromium-only. - pub(crate) fn sync_virtual_keyboard(self: &Rc, editable: bool) { - let was_editable = !self.ime_mirror.read_only(); - self.ime_mirror.set_read_only(!editable); - // Trigger a focus event only when the keyboard actually needs - // summoning. Cycling focus on every tap would restart the IME - // connection right as the keyboard reads the tapped caret's context, - // racing its word segmentation. But `focus()` on an already-focused - // element is a no-op, so a dismissed keyboard would otherwise never - // return for taps that stay within editable content: detect that - // through the visual viewport and force a fresh focus event. - let editable_needs_focus_event = editable - && (!was_editable || !self.ime_mirror.is_focused() || self.keyboard_likely_dismissed()); - if editable_needs_focus_event || (!editable && was_editable) { - self.suppress_focus_status_events.set(true); - if editable { - // A same-task blur/focus cycle may be coalesced by iOS, but - // this branch only runs when the keyboard is already gone, - // so a coalesced cycle loses nothing. - if self.ime_mirror.is_focused() { - self.ime_mirror.blur(); - } - self.ime_mirror.focus(); - } else { - self.ime_mirror.blur(); - } - self.suppress_focus_status_events.set(false); - - if editable { - let callback = wasm_bindgen::closure::Closure::once_into_js({ - let this = Rc::clone(self); - move || { - this.state.borrow_mut().is_active = true; - this.with_callback( - |callbacks| &mut callbacks.active_status_change, - |callback| callback(true), - ); - } - }); - if let Err(error) = self - .browser_window - .set_timeout_with_callback(callback.unchecked_ref()) - { - log::warn!("failed to defer web window activation: {error:?}"); - } - } - } - } - - /// Dispatches a full key press for editing intents that arrive without a - /// usable key event (Android IMEs send `key: "Unidentified"` placeholders - /// and express backspace/enter through `beforeinput` instead), so they - /// run through the same keybinding path as hardware keys. - fn dispatch_synthetic_keystroke(&self, key: &str, modifiers: Modifiers) { - let keystroke = Keystroke { - modifiers, - key: key.to_string(), - key_char: None, - }; - self.dispatch_input(PlatformInput::KeyDown(KeyDownEvent { - keystroke: keystroke.clone(), - is_held: false, - prefer_character_input: false, - })); - self.dispatch_input(PlatformInput::KeyUp(KeyUpEvent { keystroke })); - } - - fn register_pointer_move(self: &Rc) -> EventListenerHandle { - let this = Rc::clone(self); - self.listen("pointermove", move |event: JsValue| { - let event: web_sys::PointerEvent = event.unchecked_into(); - event.prevent_default(); - - let position = pointer_position_in_element(&event); - - if event.pointer_type() == "touch" { - let Some(touch_id) = this.touch_ids.borrow().active(event.pointer_id()) else { - return; - }; - this.state.borrow_mut().mouse_position = position; - // Mirror the slop rule of gpui's tap recognizer: once the - // touch travels beyond it, its release must not affect the - // keyboard. Only gpui knows what the gesture truly resolved - // to; this platform-side shadow exists because the keyboard - // decision must be made synchronously inside the browser's - // pointerup handler. - if let Some((pointer_id, start_position)) = this.touch_tap_candidate.get() - && pointer_id == event.pointer_id() - && (position - start_position).magnitude() - > f64::from(GestureTuning::default().touch_slop) - { - this.touch_tap_candidate.set(None); - } - this.dispatch_input(PlatformInput::Touch(TouchEvent { - id: touch_id, - phase: TouchPhase::Moved, - position, - predicted_position: predicted_pointer_position(&event, position), - force: None, - })); - return; - } - - let modifiers = modifiers_from_mouse_event(&event, this.is_mac); - let current_pressed = this.pressed_button.get(); - - { - let mut current_state = this.state.borrow_mut(); - current_state.mouse_position = position; - current_state.modifiers = modifiers; - } - - this.dispatch_input(PlatformInput::MouseMove(MouseMoveEvent { - position, - pressed_button: current_pressed, - modifiers, - })); - }) - } - - fn register_pointer_leave(self: &Rc) -> EventListenerHandle { - let this = Rc::clone(self); - self.listen("pointerleave", move |event: JsValue| { - let event: web_sys::PointerEvent = event.unchecked_into(); - - let position = pointer_position_in_element(&event); - let modifiers = modifiers_from_mouse_event(&event, this.is_mac); - let current_pressed = this.pressed_button.get(); - - { - let mut current_state = this.state.borrow_mut(); - current_state.mouse_position = position; - current_state.modifiers = modifiers; - current_state.is_hovered = false; - } - - this.dispatch_input(PlatformInput::MouseExited(MouseExitEvent { - position, - pressed_button: current_pressed, - modifiers, - })); - - this.with_callback( - |callbacks| &mut callbacks.hover_status_change, - |callback| callback(false), - ); - }) - } - - fn register_wheel(self: &Rc) -> EventListenerHandle { - let this = Rc::clone(self); - self.listen_non_passive("wheel", move |event: JsValue| { - let event: web_sys::WheelEvent = event.unchecked_into(); - event.prevent_default(); - - let mouse_event: &web_sys::MouseEvent = event.as_ref(); - let position = mouse_position_in_element(mouse_event); - let modifiers = modifiers_from_wheel_event(mouse_event, this.is_mac); - - // HeroGPUI fork: shift+wheel scrolls horizontally. - // - // A mouse wheel reports only `deltaY`, and every desktop platform - // treats shift+wheel as the horizontal axis; upstream passes the - // raw axes through, so a horizontally scrollable region (Tabs - // overflow, Table, the gallery's code blocks) cannot be scrolled - // at all with a wheel in the browser. A trackpad already sends a - // real `deltaX`, so the swap is taken only when `deltaX` is zero - // and the true horizontal gesture is left untouched. - let (delta_x, delta_y) = if modifiers.shift && event.delta_x() == 0.0 { - (event.delta_y(), 0.0) - } else { - (event.delta_x(), event.delta_y()) - }; - let delta_mode = event.delta_mode(); - let delta = if delta_mode == 1 { - ScrollDelta::Lines(point(-delta_x as f32, -delta_y as f32)) - } else { - ScrollDelta::Pixels(point(px(-delta_x as f32), px(-delta_y as f32))) - }; - - { - let mut current_state = this.state.borrow_mut(); - current_state.modifiers = modifiers; - } - - this.dispatch_input(PlatformInput::ScrollWheel(ScrollWheelEvent { - position, - delta, - modifiers, - touch_phase: TouchPhase::Moved, - })); - }) - } - - fn register_context_menu(self: &Rc) -> EventListenerHandle { - self.listen("contextmenu", move |event: JsValue| { - let event: web_sys::Event = event.unchecked_into(); - event.prevent_default(); - }) - } - - /// Browsers only expose dropped files as `File` objects, never as - /// filesystem paths, so no `FileDrop` input can be synthesized: GPUI's - /// `ExternalPaths` consumers would try to read paths that don't exist. - /// The events are still intercepted so the browser doesn't navigate to - /// the dropped file. Delivering actual file drops would require plumbing - /// `File` contents through a web-specific channel. - fn register_dragover(self: &Rc) -> EventListenerHandle { - self.listen("dragover", move |event: JsValue| { - let event: web_sys::DragEvent = event.unchecked_into(); - event.prevent_default(); - }) - } - - fn register_drop(self: &Rc) -> EventListenerHandle { - self.listen("drop", move |event: JsValue| { - let event: web_sys::DragEvent = event.unchecked_into(); - event.prevent_default(); - }) - } - - fn register_key_down(self: &Rc) -> EventListenerHandle { - let this = Rc::clone(self); - self.listen_input("keydown", move |event: JsValue| { - let event: web_sys::KeyboardEvent = event.unchecked_into(); - - let modifiers = modifiers_from_keyboard_event(&event, this.is_mac); - let capslock = capslock_from_keyboard_event(&event); - - if this.update_modifiers(modifiers, capslock) { - this.dispatch_input(PlatformInput::ModifiersChanged(ModifiersChangedEvent { - modifiers, - capslock, - })); - } - - let key = dom_key_to_gpui_key(&event); - - if is_modifier_only_key(&key) { - return; - } - - let is_held = event.repeat(); - let key_char = compute_key_char(&event, &key, &modifiers); - - let keystroke = Keystroke { - modifiers, - key, - key_char: key_char.clone(), - }; - - let result = this.dispatch_input(PlatformInput::KeyDown(KeyDownEvent { - keystroke, - is_held, - prefer_character_input: false, - })); - - if let Some(result) = result { - if !result.propagate { - event.prevent_default(); - this.schedule_ime_mirror_sync(); - return; - } - } - - if this.is_composing.get() || event.is_composing() { - event.prevent_default(); - return; - } - - if keystroke_inserts_text(&modifiers, this.is_mac) - && let Some(text) = key_char - { - this.with_input_handler(|handler| { - handler.replace_text_in_range(None, &text); - }); - // The character went into the input handler; suppress browser - // side-effects for the same keystroke (space scrolling the - // page, quick-find, etc.). Everything not handled above falls - // through so browser shortcuts keep their defaults. - event.prevent_default(); - } - this.schedule_ime_mirror_sync(); - }) - } - - fn register_key_up(self: &Rc) -> EventListenerHandle { - let this = Rc::clone(self); - self.listen_input("keyup", move |event: JsValue| { - let event: web_sys::KeyboardEvent = event.unchecked_into(); - - let modifiers = modifiers_from_keyboard_event(&event, this.is_mac); - let capslock = capslock_from_keyboard_event(&event); - - if this.update_modifiers(modifiers, capslock) { - this.dispatch_input(PlatformInput::ModifiersChanged(ModifiersChangedEvent { - modifiers, - capslock, - })); - } - - let key = dom_key_to_gpui_key(&event); - - if is_modifier_only_key(&key) { - return; - } - - let key_char = compute_key_char(&event, &key, &modifiers); - - let keystroke = Keystroke { - modifiers, - key, - key_char, - }; - - let result = this.dispatch_input(PlatformInput::KeyUp(KeyUpEvent { keystroke })); - if let Some(result) = result { - if !result.propagate { - event.prevent_default(); - } - } - }) - } - - /// Imports IME edits from the hidden input into the app. - /// - /// Text-editing `beforeinput` events are deliberately left uncancelled, - /// so the browser applies them to the mirror element exactly as the IME - /// expects (cancelling them and echoing the edit back programmatically - /// restarts the IME connection on every keystroke, which desynchronizes - /// the keyboard's internal state — e.g. Gboard then swallows backspaces - /// against a stale private buffer). The resulting `input` event is - /// diffed against the last known mirror text; IME edits are contiguous, - /// so a common prefix/suffix diff recovers them exactly. - /// - /// The diff supplies only the *shape* of the edit — how many UTF-16 - /// units were removed before/after the element's pre-edit selection and - /// what text replaced them. It never supplies document coordinates: - /// mirror offsets captured at sync time go stale whenever the document - /// changes underneath (this is a live collaborative document). The - /// position comes from `selected_text_range()` queried in this same - /// synchronous callback — the editor resolves its selection through - /// anchors, so the freshly-fetched offsets are exact, and nothing can - /// run between the query and the edit below. - fn register_input(self: &Rc) -> EventListenerHandle { - let this = Rc::clone(self); - self.listen_input("input", move |event: JsValue| { - let event: web_sys::InputEvent = event.unchecked_into(); - - // Composition text is delivered through the composition events; - // the mirror is reconciled once on compositionend. - if this.is_composing.get() || event.is_composing() { - return; - } - - let new_value = this.ime_mirror.value(); - let old_value = this.ime_mirror.stored_text(); - if new_value == old_value { - return; - } - - let old_units: Vec = old_value.encode_utf16().collect(); - let new_units: Vec = new_value.encode_utf16().collect(); - - // A prefix/suffix diff is ambiguous when the inserted text - // shares characters with what follows it (inserting "pactor " - // before "pact" also reads as inserting "or pact" four units - // later). The edit's true position is not ambiguous: the browser - // leaves the caret at the end of an IME edit, so the suffix is - // anchored as "everything after the post-edit caret", and the - // prefix is capped to fit. Greedy matching is only a fallback - // for edits where the anchored suffix doesn't verify. - let post_edit_caret = this - .ime_mirror - .selection_start() - .map(|caret| caret as usize); - let anchored_suffix_length = post_edit_caret - .map(|caret| new_units.len().saturating_sub(caret)) - .filter(|&suffix_length| { - suffix_length <= old_units.len() - && old_units[old_units.len() - suffix_length..] - == new_units[new_units.len() - suffix_length..] - }); - let suffix_length = anchored_suffix_length.unwrap_or_else(|| { - old_units - .iter() - .rev() - .zip(new_units.iter().rev()) - .take_while(|(old_unit, new_unit)| old_unit == new_unit) - .count() - }); - let prefix_length = old_units - .iter() - .zip(&new_units) - .take_while(|(old_unit, new_unit)| old_unit == new_unit) - .count() - .min(old_units.len() - suffix_length) - .min(new_units.len() - suffix_length); - - let inserted_text = String::from_utf16_lossy( - &new_units[prefix_length..new_units.len() - suffix_length], - ); - let replaced_old_end = old_units.len() - suffix_length; - - // The edit's shape relative to the element's pre-edit selection. - // The element is private to the IME and these syncs, so the - // stored selection is exact. - let (element_selection_start, element_selection_end) = - this.ime_mirror.stored_selection(); - let removed_before_selection = - (element_selection_start as usize).saturating_sub(prefix_length); - let removed_after_selection = - replaced_old_end.saturating_sub(element_selection_end as usize); - - let applied = this.with_input_handler(|handler| { - let Some(selection) = handler.selected_text_range(false) else { - return false; - }; - let range = selection - .range - .start - .saturating_sub(removed_before_selection) - ..selection.range.end + removed_after_selection; - handler.replace_text_in_range(Some(range), &inserted_text); - true - }); - if applied != Some(true) { - return; - } - - this.ime_mirror.adopt_element_state(); - }) - } - - /// Imports IME-driven selection moves on the mirror element into the app. - /// - /// Some IME gestures preview their effect by moving the field's - /// selection before committing an edit — Android's slide-on-backspace - /// grows a selection over the text it will delete. A native field - /// renders that selection itself; this import gives the app the same - /// chance. Like edit imports, the move is expressed relative to the - /// element's stored selection and applied to the app selection queried - /// in the same synchronous callback, never through document coordinates, - /// which go stale in a collaborative document. - /// - /// Self-inflicted events are filtered by state, not by suppression - /// flags: `selectionchange` dispatches asynchronously, after the sync or - /// import that caused it has already adopted the element's selection, so - /// a stored-state match means there is nothing to import. - /// - /// Registered on the document: Chrome dispatches text-control selection - /// changes there, not on the element. - fn register_selection_change(self: &Rc) -> Option { - let document = self.browser_window.document()?; - let this = Rc::clone(self); - Some(EventListenerHandle::add( - document.as_ref(), - "selectionchange", - move |_event: JsValue| { - if this.is_composing.get() || !this.ime_mirror.is_focused() { - return; - } - // An in-flight edit owns the selection; its import adopts it. - if this.ime_mirror.value() != this.ime_mirror.stored_text() { - return; - } - let Some(element_start) = this.ime_mirror.selection_start() else { - return; - }; - let element_end = this - .ime_mirror - .element_selection_end() - .unwrap_or(element_start); - let (stored_start, stored_end) = this.ime_mirror.stored_selection(); - if (element_start, element_end) == (stored_start, stored_end) { - return; - } - let applied = this.with_input_handler(|handler| { - let Some(selection) = handler.selected_text_range(false) else { - return false; - }; - // The app range corresponding to the stored element - // selection is exactly `selection`; a single consistent - // alignment between the two maps the moved endpoints. - // Disagreeing alignments mean the app selection changed - // underneath and the pending resync owns the element. - let alignment = selection.range.start.checked_sub(stored_start as usize); - if alignment.is_none() - || alignment != selection.range.end.checked_sub(stored_end as usize) - { - return false; - } - let alignment = alignment.unwrap(); - handler.set_selected_text_range( - alignment + element_start as usize..alignment + element_end as usize, - ); - true - }); - if applied == Some(true) { - this.ime_mirror.adopt_element_state(); - } else { - // No import is coming for this move; without a forced - // sync the mirror would keep deferring to it and show - // the IME a selection the app never adopted. - this.ime_mirror.reject_selection_import(); - this.schedule_ime_mirror_sync(); - } - }, - )) - } - - /// Software keyboards (IMEs) express editing through `beforeinput` - /// rather than key events: Android IMEs emit only a placeholder key - /// event (`key: "Unidentified"`, `keyCode` 229). This handler only - /// intercepts the intents that must not mutate the mirror element; - /// ordinary edits deliberately proceed to the element and are imported - /// by `register_input`. Desktop keystrokes never reach this handler, - /// because `register_key_down` calls `preventDefault()` for every - /// keystroke it inserts, which cancels the corresponding `beforeinput`. - fn register_before_input(self: &Rc) -> EventListenerHandle { - let this = Rc::clone(self); - self.listen_input("beforeinput", move |event: JsValue| { - let event: web_sys::InputEvent = event.unchecked_into(); - - // During composition the composition{update,end} handlers own - // the text. - if this.is_composing.get() || event.is_composing() { - return; - } - - match event.input_type().as_str() { - // Enter means "submit", not "insert a newline into the - // mirror": run it through the keybinding path instead of - // letting it mutate the element. - "insertLineBreak" | "insertParagraph" => { - event.prevent_default(); - this.dispatch_synthetic_keystroke("enter", Modifiers::default()); - this.schedule_ime_mirror_sync(); - } - // Everything else (insertText, deleteContent*, autocorrect's - // insertReplacementText, ...) is left to the browser's - // default action on the mirror element; `register_input` - // imports the resulting element diff into the editor. - _ => {} - } - }) - } - - /// Paste is delivered through the DOM `paste` event rather than - /// `Platform::read_from_clipboard`: the browser's asynchronous clipboard - /// read API cannot fit that synchronous signature, while `ClipboardEvent` - /// exposes `clipboardData` synchronously inside the event. It fires for - /// any browser-initiated paste (keyboard, menu bar, context menu). - /// - /// Text-only pastes reach the input handler synchronously. Pasted image - /// files only expose their bytes through asynchronous blob reads, so - /// pastes containing images are delivered once those reads resolve — to - /// whichever input handler is focused at that point, matching how an - /// application-level paste action would behave. - fn register_paste(self: &Rc) -> EventListenerHandle { - let this = Rc::clone(self); - self.listen_input("paste", move |event: JsValue| { - let event: web_sys::ClipboardEvent = event.unchecked_into(); - let Some(clipboard_data) = event.clipboard_data() else { - return; - }; - let text = clipboard_data - .get_data("text/plain") - .ok() - .filter(|text| !text.is_empty()); - - // File handles must be collected synchronously: the browser - // clears `clipboardData`'s item list once this handler returns, - // while the `File`s themselves stay readable afterwards. - let mut image_files = Vec::new(); - let items = clipboard_data.items(); - for index in 0..items.length() { - let Some(item) = items.get(index) else { - continue; - }; - if item.kind() != "file" { - continue; - } - let Some(format) = ImageFormat::from_mime_type(&item.type_()) else { - continue; - }; - if let Ok(Some(file)) = item.get_as_file() { - image_files.push((format, file)); - } - } - - if text.is_none() && image_files.is_empty() { - return; - } - event.prevent_default(); - - if image_files.is_empty() { - if let Some(text) = text { - this.with_input_handler(|handler| { - handler.paste(ClipboardItem::new_string(text)); - }); - // HeroGPUI fork: paste cancels the DOM edit, so refresh - // the mirror from the application's replacement before - // the next IME edit. Without this the hidden textarea - // still holds the pre-paste text, and the next - // composition or `beforeinput` diff is computed against - // stale content -- pasted text gets duplicated or eaten. - this.schedule_ime_mirror_sync(); - } - return; - } - - let this = Rc::clone(&this); - wasm_bindgen_futures::spawn_local(async move { - let mut entries = Vec::new(); - if let Some(text) = text { - entries.push(ClipboardEntry::String(ClipboardString::new(text))); - } - for (format, file) in image_files { - match crate::platform::read_blob_bytes(&file).await { - Ok(bytes) => { - entries.push(ClipboardEntry::Image(Image::from_bytes(format, bytes))); - } - Err(error) => { - log::error!( - "failed to read pasted image: {}", - crate::platform::js_error_message(&error) - ); - } - } - } - if entries.is_empty() { - return; - } - this.with_input_handler(|handler| { - handler.paste(ClipboardItem { entries }); - }); - // Same fork as the text-only path above, for the async - // image/mixed paste that resolves a frame or more later. - this.schedule_ime_mirror_sync(); - }); - }) - } - - fn register_composition_start(self: &Rc) -> EventListenerHandle { - let this = Rc::clone(self); - self.listen_input("compositionstart", move |_event: JsValue| { - this.is_composing.set(true); - }) - } - - fn register_composition_update(self: &Rc) -> EventListenerHandle { - let this = Rc::clone(self); - self.listen_input("compositionupdate", move |event: JsValue| { - let event: web_sys::CompositionEvent = event.unchecked_into(); - let data = event.data().unwrap_or_default(); - this.is_composing.set(true); - this.with_input_handler(|handler| { - handler.replace_and_mark_text_in_range(None, &data, None); - }); - }) - } - - fn register_composition_end(self: &Rc) -> EventListenerHandle { - let this = Rc::clone(self); - self.listen_input("compositionend", move |event: JsValue| { - let event: web_sys::CompositionEvent = event.unchecked_into(); - let data = event.data().unwrap_or_default(); - this.is_composing.set(false); - this.with_input_handler(|handler| { - // Only commit the final text when a marked range still - // exists. When a caret move ended the composition, the - // editor has already unmarked (keeping the composed text as - // committed content); inserting `data` at the selection - // would duplicate the word at the new caret position. - if handler.marked_text_range().is_some() { - handler.replace_text_in_range(None, &data); - } - handler.unmark_text(); - }); - // Adopt the element's post-composition state as the mirror - // baseline without writing anything: the browser applied the - // commit to the element itself, and a write here would restart - // the IME mid-commit. The deferred sync reconciles any - // app-side divergence afterwards. - this.ime_mirror.adopt_element_state(); - this.schedule_ime_mirror_sync(); - }) - } - - fn register_focus(self: &Rc) -> EventListenerHandle { - let this = Rc::clone(self); - self.listen_input("focus", move |_event: JsValue| { - if this.suppress_focus_status_events.get() { - return; - } - { - let mut state = this.state.borrow_mut(); - state.is_active = true; - } - this.with_callback( - |callbacks| &mut callbacks.active_status_change, - |callback| callback(true), - ); - }) - } - - fn register_blur(self: &Rc) -> EventListenerHandle { - let this = Rc::clone(self); - self.listen_input("blur", move |_event: JsValue| { - if this.suppress_focus_status_events.get() { - return; - } - { - let mut state = this.state.borrow_mut(); - state.is_active = false; - } - this.with_callback( - |callbacks| &mut callbacks.active_status_change, - |callback| callback(false), - ); - }) - } - - fn register_pointer_enter(self: &Rc) -> EventListenerHandle { - let this = Rc::clone(self); - self.listen("pointerenter", move |_event: JsValue| { - { - let mut state = this.state.borrow_mut(); - state.is_hovered = true; - } - this.with_callback( - |callbacks| &mut callbacks.hover_status_change, - |callback| callback(true), - ); - }) - } -} - -fn dom_key_to_gpui_key(event: &web_sys::KeyboardEvent) -> String { - let key = event.key(); - match key.as_str() { - "Enter" => "enter".to_string(), - "Backspace" => "backspace".to_string(), - "Tab" => "tab".to_string(), - "Escape" => "escape".to_string(), - "Delete" => "delete".to_string(), - " " => "space".to_string(), - "ArrowLeft" => "left".to_string(), - "ArrowRight" => "right".to_string(), - "ArrowUp" => "up".to_string(), - "ArrowDown" => "down".to_string(), - "Home" => "home".to_string(), - "End" => "end".to_string(), - "PageUp" => "pageup".to_string(), - "PageDown" => "pagedown".to_string(), - "Insert" => "insert".to_string(), - "Control" => "control".to_string(), - "Alt" => "alt".to_string(), - "Shift" => "shift".to_string(), - "Meta" => "platform".to_string(), - "CapsLock" => "capslock".to_string(), - other => { - if let Some(rest) = other.strip_prefix('F') { - if let Ok(number) = rest.parse::() { - if (1..=35).contains(&number) { - return format!("f{number}"); - } - } - } - other.to_lowercase() - } - } -} - -fn dom_mouse_button_to_gpui(button: i16) -> MouseButton { - match button { - 0 => MouseButton::Left, - 1 => MouseButton::Middle, - 2 => MouseButton::Right, - 3 => MouseButton::Navigate(NavigationDirection::Back), - 4 => MouseButton::Navigate(NavigationDirection::Forward), - _ => MouseButton::Left, - } -} - -fn modifiers_from_keyboard_event(event: &web_sys::KeyboardEvent, _is_mac: bool) -> Modifiers { - Modifiers { - control: event.ctrl_key(), - alt: event.alt_key(), - shift: event.shift_key(), - platform: event.meta_key(), - function: false, - } -} - -fn modifiers_from_mouse_event(event: &web_sys::PointerEvent, _is_mac: bool) -> Modifiers { - let mouse_event: &web_sys::MouseEvent = event.as_ref(); - Modifiers { - control: mouse_event.ctrl_key(), - alt: mouse_event.alt_key(), - shift: mouse_event.shift_key(), - platform: mouse_event.meta_key(), - function: false, - } -} - -fn modifiers_from_wheel_event(event: &web_sys::MouseEvent, _is_mac: bool) -> Modifiers { - Modifiers { - control: event.ctrl_key(), - alt: event.alt_key(), - shift: event.shift_key(), - platform: event.meta_key(), - function: false, - } -} - -fn capslock_from_keyboard_event(event: &web_sys::KeyboardEvent) -> Capslock { - Capslock { - on: event.get_modifier_state("CapsLock"), - } -} - -fn should_preserve_focused_input( - accepted_text_before_tap: bool, - accepts_text_after_tap: bool, - dispatch_result: Option, -) -> bool { - accepted_text_before_tap - && accepts_text_after_tap - && dispatch_result.is_some_and(|result| result.default_prevented) -} - -pub(crate) fn is_mac_platform(browser_window: &web_sys::Window) -> bool { - let navigator = browser_window.navigator(); - - #[allow(deprecated)] - // navigator.platform() is deprecated but navigator.userAgentData is not widely available yet - if let Ok(platform) = navigator.platform() { - if platform.contains("Mac") { - return true; - } - } - - if let Ok(user_agent) = navigator.user_agent() { - return user_agent.contains("Mac"); - } - - false -} - -fn is_modifier_only_key(key: &str) -> bool { - matches!( - key, - "control" | "alt" | "shift" | "platform" | "capslock" | "compose" | "process" - ) -} - -/// Whether a keystroke with these modifiers produces text to insert. -/// -/// On macOS, Option participates in text entry (e.g. option-n composes "~" -/// or accented characters), so only Command and Control disqualify. Elsewhere, -/// plain Alt is a shortcut modifier, but AltGr is reported by browsers as -/// control+alt and `event.key()` then carries the composed character. -fn keystroke_inserts_text(modifiers: &Modifiers, is_mac: bool) -> bool { - if is_mac { - !modifiers.platform && !modifiers.control - } else { - modifiers.is_subset_of(&Modifiers::shift()) || (modifiers.control && modifiers.alt) - } -} - -fn compute_key_char( - event: &web_sys::KeyboardEvent, - gpui_key: &str, - modifiers: &Modifiers, -) -> Option { - // AltGr arrives as control+alt with the composed character in - // `event.key()`; bare Command/Control combinations are not text. - if (modifiers.platform || modifiers.control) && !(modifiers.control && modifiers.alt) { - return None; - } - - if is_modifier_only_key(gpui_key) { - return None; - } - - if gpui_key == "space" { - return Some(" ".to_string()); - } - - let raw_key = event.key(); - - if raw_key.len() == 1 { - return Some(raw_key); - } - - None -} - -fn pointer_position_in_element(event: &web_sys::PointerEvent) -> Point { - let mouse_event: &web_sys::MouseEvent = event.as_ref(); - mouse_position_in_element(mouse_event) -} - -/// How far ahead of the raw pointer position predictions may reach. -/// -/// Browsers predict much further (Chrome offers samples out to 25ms), but -/// prediction error grows with the horizon and surfaces as jitter: the -/// emitted pan deltas gain a term proportional to lead x change in velocity, -/// which at long leads visibly reverses direction mid-drag. Measurements -/// show leads up to ~10ms track at or below the raw stream's frame-to-frame -/// variation; AOSP similarly caps touch resampling extrapolation at 8ms -/// (`RESAMPLE_MAX_PREDICTION` in `InputTransport.cpp`). -const MAX_PREDICTION_LEAD_MS: f64 = 10.; - -/// The predicted pointer position closest to [`MAX_PREDICTION_LEAD_MS`] -/// ahead of `event`, from `getPredictedEvents()`, or `None` when the browser -/// offers no prediction (Safari lacks the method, Firefox returns an empty -/// array). A prediction further out than the cap is linearly scaled back to -/// it. -/// -/// Accessed through `Reflect` because calling a missing method through the -/// web-sys binding would throw, and predicted events' `offsetX`/`offsetY` -/// are unreliable across browsers (their target may be detached), so the -/// position is derived from the client-coordinate delta against the parent -/// event, anchored to the parent's element-relative `position`. -fn predicted_pointer_position( - event: &web_sys::PointerEvent, - position: Point, -) -> Option> { - let method = js_sys::Reflect::get(event, &JsValue::from_str("getPredictedEvents")).ok()?; - let method = method.dyn_ref::()?; - let predicted_events: js_sys::Array = method.call0(event).ok()?.dyn_into().ok()?; - let mut best: Option<(f64, web_sys::PointerEvent)> = None; - for predicted in predicted_events.iter() { - let Ok(predicted) = predicted.dyn_into::() else { - continue; - }; - let lead = predicted.time_stamp() - event.time_stamp(); - if lead <= 0. { - continue; - } - let distance_to_cap = (lead - MAX_PREDICTION_LEAD_MS).abs(); - if best - .as_ref() - .is_none_or(|(best_distance, _)| distance_to_cap < *best_distance) - { - best = Some((distance_to_cap, predicted)); - } - } - let (_, predicted) = best?; - let lead = predicted.time_stamp() - event.time_stamp(); - let scale = (MAX_PREDICTION_LEAD_MS / lead).min(1.) as f32; - let event: &web_sys::MouseEvent = event.as_ref(); - let predicted: &web_sys::MouseEvent = predicted.as_ref(); - Some(point( - position.x + px((predicted.client_x() - event.client_x()) as f32 * scale), - position.y + px((predicted.client_y() - event.client_y()) as f32 * scale), - )) -} - -fn mouse_position_in_element(event: &web_sys::MouseEvent) -> Point { - // offset_x/offset_y give position relative to the target element's padding edge - point(px(event.offset_x() as f32), px(event.offset_y() as f32)) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn browser_pointer_id_reuse_gets_a_new_touch_id() { - let mut touch_ids = TouchIds::default(); - let first = touch_ids.start(7).expect("first touch id"); - let concurrent = touch_ids.start(8).expect("concurrent touch id"); - - assert_ne!(first, concurrent); - assert_eq!(touch_ids.active(7), Some(first)); - assert_eq!(touch_ids.end(7), Some(first)); - assert_eq!(touch_ids.active(7), None); - - let reused = touch_ids.start(7).expect("reused pointer touch id"); - assert_ne!(reused, first); - assert_ne!(reused, concurrent); - } - - #[test] - fn handled_tap_preserves_unchanged_text_input() { - assert!(should_preserve_focused_input( - true, - true, - Some(DispatchEventResult { - propagate: false, - default_prevented: true, - }), - )); - } - - #[test] - fn tap_does_not_preserve_unhandled_or_unfocused_input() { - let result = |default_prevented| { - Some(DispatchEventResult { - propagate: false, - default_prevented, - }) - }; - - assert!(!should_preserve_focused_input(true, true, result(false),)); - assert!(!should_preserve_focused_input(false, true, result(true),)); - assert!(!should_preserve_focused_input(true, false, result(true),)); - } -} diff --git a/crates/gpui_web/src/gpui_web.rs b/crates/gpui_web/src/gpui_web.rs deleted file mode 100644 index 6084804..0000000 --- a/crates/gpui_web/src/gpui_web.rs +++ /dev/null @@ -1,25 +0,0 @@ -#![cfg(target_family = "wasm")] - -//! GPUI's browser platform uses one document-owned canvas and supports one top-level window. -//! Browser WebGPU is preferred by default, with an automatic WebGL2 fallback. Applications can -//! force either backend with [`WebBackendPreference`]. Opening a second top-level window, or -//! reopening one after it closes, returns [`WebWindowError`]. - -mod dispatcher; -mod display; -mod events; -mod http_client; -mod ime_mirror; -mod keyboard; -mod logging; -mod platform; -mod window; - -pub use dispatcher::WebDispatcher; -pub use display::WebDisplay; -pub use gpui_wgpu::WebBackendPreference; -pub use http_client::{FetchCredentials, FetchHttpClient}; -pub use keyboard::WebKeyboardLayout; -pub use logging::init_logging; -pub use platform::{WebPlatform, WebWindowError}; -pub use window::WebWindow; diff --git a/crates/gpui_web/src/http_client.rs b/crates/gpui_web/src/http_client.rs deleted file mode 100644 index 4a00324..0000000 --- a/crates/gpui_web/src/http_client.rs +++ /dev/null @@ -1,309 +0,0 @@ -use crate::WebDispatcher; -use anyhow::{Context as _, anyhow}; -use futures::{ - AsyncRead, AsyncReadExt as _, FutureExt as _, SinkExt as _, TryStreamExt as _, - channel::{mpsc, oneshot}, -}; -use http_client::{AsyncBody, HttpClient, RedirectPolicy}; -use std::{ - io, - pin::Pin, - sync::Arc, - task::{Context, Poll}, -}; -use wasm_bindgen::JsCast as _; -use wasm_bindgen::prelude::*; - -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(catch, js_name = "fetch")] - fn global_fetch(input: &web_sys::Request) -> Result; -} - -pub struct FetchHttpClient { - dispatcher: Arc, - user_agent: Option, - credentials: FetchCredentials, -} - -/// Controls whether browser Fetch requests include credentials. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -pub enum FetchCredentials { - /// Never send credentials in the request or include credentials in the response. - Omit, - /// Only send and include credentials for same-origin requests. This is the default. - #[default] - SameOrigin, - /// Always include credentials, even for cross-origin requests. - Include, -} - -impl FetchHttpClient { - pub(crate) fn new(dispatcher: Arc) -> Self { - Self { - dispatcher, - user_agent: None, - credentials: FetchCredentials::default(), - } - } - - pub(crate) fn with_user_agent( - dispatcher: Arc, - user_agent: &str, - ) -> anyhow::Result { - Ok(Self { - dispatcher, - user_agent: Some(http_client::http::header::HeaderValue::from_str( - user_agent, - )?), - credentials: FetchCredentials::default(), - }) - } - - pub fn with_credentials(mut self, credentials: FetchCredentials) -> Self { - self.credentials = credentials; - self - } -} - -impl HttpClient for FetchHttpClient { - fn user_agent(&self) -> Option<&http_client::http::header::HeaderValue> { - self.user_agent.as_ref() - } - - fn proxy(&self) -> Option<&http_client::Url> { - None - } - - fn send( - &self, - req: http_client::http::Request, - ) -> futures::future::BoxFuture<'static, anyhow::Result>> - { - let (parts, body) = req.into_parts(); - let credentials = self.credentials; - let dispatcher = self.dispatcher.clone(); - - Box::pin(async move { - let body_bytes = read_body_to_bytes(body).await?; - let (sender, receiver) = oneshot::channel(); - - dispatcher.dispatch_function_on_main_thread(move || { - wasm_bindgen_futures::spawn_local(async move { - let result = fetch(parts, body_bytes, credentials).await; - if sender.send(result).is_err() { - log::debug!("fetch response receiver was dropped"); - } - }); - }); - - receiver.await.context("browser fetch task was canceled")? - }) - } -} - -async fn fetch( - parts: http_client::http::request::Parts, - body_bytes: Option>, - credentials: FetchCredentials, -) -> anyhow::Result> { - let init = web_sys::RequestInit::new(); - init.set_method(parts.method.as_str()); - init.set_credentials(match credentials { - FetchCredentials::Omit => web_sys::RequestCredentials::Omit, - FetchCredentials::SameOrigin => web_sys::RequestCredentials::SameOrigin, - FetchCredentials::Include => web_sys::RequestCredentials::Include, - }); - - if let Some(redirect_policy) = parts.extensions.get::() { - match redirect_policy { - RedirectPolicy::NoFollow => { - init.set_redirect(web_sys::RequestRedirect::Manual); - } - RedirectPolicy::FollowLimit(_) | RedirectPolicy::FollowAll => { - init.set_redirect(web_sys::RequestRedirect::Follow); - } - } - } - - if let Some(ref bytes) = body_bytes { - let uint8array = js_sys::Uint8Array::from(bytes.as_slice()); - init.set_body(uint8array.as_ref()); - } - - let url = parts.uri.to_string(); - let request = web_sys::Request::new_with_str_and_init(&url, &init) - .map_err(|error| anyhow!("failed to create fetch Request: {error:?}"))?; - - let request_headers = request.headers(); - for (name, value) in &parts.headers { - let value_str = value - .to_str() - .map_err(|_| anyhow!("non-ASCII header value for {name}"))?; - request_headers - .set(name.as_str(), value_str) - .map_err(|error| anyhow!("failed to set header {name}: {error:?}"))?; - } - - let promise = - global_fetch(&request).map_err(|error| anyhow!("fetch threw an error: {error:?}"))?; - let response_value = wasm_bindgen_futures::JsFuture::from(promise) - .await - .map_err(|error| anyhow!("fetch failed: {error:?}"))?; - - let web_response: web_sys::Response = response_value - .dyn_into() - .map_err(|error| anyhow!("fetch result is not a Response: {error:?}"))?; - - let status = web_response.status(); - let mut builder = http_client::http::Response::builder().status(status); - - // `Headers` is a JS iterable yielding `[name, value]` pairs. - // `js_sys::Array::from` calls `Array.from()` which accepts any iterable. - let header_pairs = js_sys::Array::from(&web_response.headers()); - for index in 0..header_pairs.length() { - match header_pairs.get(index).dyn_into::() { - Ok(pair) => match (pair.get(0).as_string(), pair.get(1).as_string()) { - (Some(name), Some(value)) => { - builder = builder.header(name, value); - } - (name, value) => { - log::warn!( - "skipping response header at index {index}: \ - name={name:?}, value={value:?}" - ); - } - }, - Err(entry) => { - log::warn!("skipping non-array header entry at index {index}: {entry:?}"); - } - } - } - - let body = match web_response.body() { - Some(stream) => { - let reader = stream - .get_reader() - .dyn_into::() - .map_err(|error| { - anyhow!("response body reader has an unexpected type: {error:?}") - })?; - AsyncBody::from_reader(ReadableStreamBody::new(reader)) - } - None => AsyncBody::empty(), - }; - - builder.body(body).map_err(|error| anyhow!(error)) -} - -// Request bodies are buffered into memory because streaming uploads require -// half-duplex Fetch support that browsers largely don't ship yet. -async fn read_body_to_bytes(mut body: AsyncBody) -> anyhow::Result>> { - let mut buffer = Vec::new(); - body.read_to_end(&mut buffer).await?; - if buffer.is_empty() { - Ok(None) - } else { - Ok(Some(buffer)) - } -} - -const RESPONSE_BODY_CHANNEL_CAPACITY: usize = 8; - -struct ReadableStreamBody { - chunks: futures::stream::IntoAsyncRead>>>, - // Dropping this sender resolves the pump's cancellation future, which - // cancels the browser-side `ReadableStream`. - _cancellation: oneshot::Sender<()>, -} - -impl ReadableStreamBody { - fn new(reader: web_sys::ReadableStreamDefaultReader) -> Self { - let (chunks_sender, chunks_receiver) = mpsc::channel(RESPONSE_BODY_CHANNEL_CAPACITY); - let (cancellation, cancellation_receiver) = oneshot::channel(); - wasm_bindgen_futures::spawn_local(pump_response_body( - reader, - chunks_sender, - cancellation_receiver, - )); - Self { - chunks: chunks_receiver.into_async_read(), - _cancellation: cancellation, - } - } -} - -impl AsyncRead for ReadableStreamBody { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buffer: &mut [u8], - ) -> Poll> { - Pin::new(&mut self.chunks).poll_read(cx, buffer) - } -} - -async fn pump_response_body( - reader: web_sys::ReadableStreamDefaultReader, - mut chunks: mpsc::Sender>>, - cancellation: oneshot::Receiver<()>, -) { - let cancellation = cancellation.fuse(); - futures::pin_mut!(cancellation); - - loop { - let read = wasm_bindgen_futures::JsFuture::from(reader.read()).fuse(); - futures::pin_mut!(read); - let result = futures::select_biased! { - _ = cancellation => { - cancel_reader(&reader).await; - return; - } - result = read => result, - }; - - let chunk = result - .map_err(|error| io::Error::other(format!("response stream failed: {error:?}"))) - .and_then(response_chunk); - match chunk { - Ok(Some(chunk)) => { - if chunks.send(Ok(chunk)).await.is_err() { - cancel_reader(&reader).await; - return; - } - } - Ok(None) => return, - Err(error) => { - if chunks.send(Err(error)).await.is_err() { - log::debug!("response body receiver was dropped after a stream error"); - } - return; - } - } - } -} - -fn response_chunk(result: JsValue) -> io::Result>> { - // `ReadableStreamReadResult` is a dictionary type, so there is no runtime - // class to check against; `unchecked_into` is the only available cast. - let result: web_sys::ReadableStreamReadResult = result.unchecked_into(); - if result.get_done().unwrap_or(false) { - return Ok(None); - } - - result - .get_value() - .dyn_into::() - .map(|bytes| Some(bytes.to_vec())) - .map_err(|value| { - io::Error::other(format!( - "response stream yielded a non-byte chunk: {value:?}" - )) - }) -} - -async fn cancel_reader(reader: &web_sys::ReadableStreamDefaultReader) { - if let Err(error) = wasm_bindgen_futures::JsFuture::from(reader.cancel()).await { - log::debug!("failed to cancel response body reader: {error:?}"); - } -} diff --git a/crates/gpui_web/src/ime_mirror.rs b/crates/gpui_web/src/ime_mirror.rs deleted file mode 100644 index 75ef807..0000000 --- a/crates/gpui_web/src/ime_mirror.rs +++ /dev/null @@ -1,605 +0,0 @@ -//! The hidden `

, - /// One-based scene clip-chain index. Zero uses the inline bounds and radii. - pub clip_index: u32, - /// Explicit GPU record padding. - pub clip_padding: u32, -} - -impl ContentMask { - /// Scale the content mask's pixel units by the given scaling factor. - pub fn scale(&self, factor: f32) -> ContentMask { - ContentMask { - bounds: self.bounds.scale(factor), - corner_radii: self.corner_radii.scale(factor), - clip_index: self.clip_index, - clip_padding: 0, - } - } - - /// Intersect without changing either mask's rounded geometry. - pub fn intersect(&self, other: &Self) -> crate::ClipRegion { - crate::ClipRegion::from(*self).intersect(&crate::ClipRegion::from(*other)) - } -} - -impl Window { - fn mark_view_dirty(&mut self, view_id: EntityId) { - // Mark ancestor views as dirty. If already in the `dirty_views` set, then all its ancestors - // should already be dirty. - for view_id in self - .rendered_frame - .dispatch_tree - .view_path_reversed(view_id) - { - if !self.dirty_views.insert(view_id) { - break; - } - } - } - - /// Registers a callback to be invoked when the window appearance changes. - pub fn observe_window_appearance( - &self, - mut callback: impl FnMut(&mut Window, &mut App) + 'static, - ) -> Subscription { - let (subscription, activate) = self.appearance_observers.insert( - (), - Box::new(move |window, cx| { - callback(window, cx); - true - }), - ); - activate(); - subscription - } - - /// Registers a callback to be invoked when the window button layout changes. - pub fn observe_button_layout_changed( - &self, - mut callback: impl FnMut(&mut Window, &mut App) + 'static, - ) -> Subscription { - let (subscription, activate) = self.button_layout_observers.insert( - (), - Box::new(move |window, cx| { - callback(window, cx); - true - }), - ); - activate(); - subscription - } - - /// Replaces the root entity of the window with a new one. - pub fn replace_root( - &mut self, - cx: &mut App, - build_view: impl FnOnce(&mut Window, &mut Context) -> E, - ) -> Entity - where - E: 'static + Render, - { - let view = cx.new(|cx| build_view(self, cx)); - self.root = Some(view.clone().into()); - self.refresh(); - view - } - - /// Returns the root entity of the window, if it has one. - pub fn root(&self) -> Option>> - where - E: 'static + Render, - { - self.root - .as_ref() - .map(|view| view.clone().downcast::().ok()) - } - - /// Obtain a handle to the window that belongs to this context. - pub fn window_handle(&self) -> AnyWindowHandle { - self.handle - } - - /// Mark the window as dirty, scheduling it to be redrawn on the next frame. - pub fn refresh(&mut self) { - if self.invalidator.not_drawing() { - self.refreshing = true; - self.invalidator.set_dirty(true); - } - } - - /// Close this window. - pub fn remove_window(&mut self) { - self.removed = true; - } - - /// Obtain the currently focused [`FocusHandle`]. If no elements are focused, returns `None`. - pub fn focused(&self, cx: &App) -> Option { - self.focus - .and_then(|id| FocusHandle::for_id(id, &cx.focus_handles)) - } - - /// While focus-lost listeners are being dispatched, returns the closest ancestor of the - /// previously focused element that can still receive focus, making it a suitable target - /// for focus restoration. Returns `None` at all other times, or when no such ancestor exists. - pub fn focus_lost_restore_target(&self, cx: &App) -> Option { - let (_leaf, ancestors) = self.focus_lost_path.split_last()?; - ancestors.iter().rev().find_map(|id| { - self.rendered_frame.dispatch_tree.focusable_node_id(*id)?; - FocusHandle::for_id(*id, &cx.focus_handles) - }) - } - - /// Move focus to the element associated with the given [`FocusHandle`]. - pub fn focus(&mut self, handle: &FocusHandle, cx: &mut App) { - if !self.focus_enabled || self.focus == Some(handle.id) { - return; - } - - self.focus = Some(handle.id); - self.focus_generation = self.focus_generation.wrapping_add(1); - self.clear_pending_keystrokes(cx); - - self.refresh(); - } - - /// Remove focus from all elements within this context's window. - pub fn blur(&mut self, cx: &mut App) { - self.clear_pending_keystrokes(cx); - - if !self.focus_enabled { - return; - } - - if self.focus.is_some() { - self.focus_generation = self.focus_generation.wrapping_add(1); - } - self.focus = None; - self.refresh(); - } - - /// Blur the window and don't allow anything in it to be focused again. - pub fn disable_focus(&mut self, cx: &mut App) { - self.blur(cx); - self.focus_enabled = false; - } - - /// Move focus to next tab stop. - pub fn focus_next(&mut self, cx: &mut App) { - if !self.focus_enabled { - return; - } - - if let Some(handle) = self.rendered_frame.tab_stops.next(self.focus.as_ref()) { - self.focus(&handle, cx) - } - } - - /// Move focus to previous tab stop. - pub fn focus_prev(&mut self, cx: &mut App) { - if !self.focus_enabled { - return; - } - - if let Some(handle) = self.rendered_frame.tab_stops.prev(self.focus.as_ref()) { - self.focus(&handle, cx) - } - } - - /// Accessor for the text system. - pub fn text_system(&self) -> &Arc { - &self.text_system - } - - /// The current text style. Which is composed of all the style refinements provided to `with_text_style`. - pub fn text_style(&self) -> TextStyle { - let mut style = TextStyle::default(); - for refinement in &self.text_style_stack { - style.refine(refinement); - } - style - } - - /// Check if the platform window is maximized. - /// - /// On some platforms (namely Windows) this is different than the bounds being the size of the display - pub fn is_maximized(&self) -> bool { - self.platform_window.is_maximized() - } - - /// request a certain window decoration (Wayland) - pub fn request_decorations(&self, decorations: WindowDecorations) { - self.platform_window.request_decorations(decorations); - } - - /// Set the exclusive zone for a layer-shell surface: how much screen space it - /// reserves so other surfaces avoid occluding it (e.g. a panel reserving space). - /// Positive values reserve that distance from the anchored edge, 0 lets the - /// surface be moved out of others' exclusive zones, and -1 ignores reserved - /// space and may extend under other surfaces. (Wayland layer-shell windows only) - pub fn set_exclusive_zone(&self, zone: Pixels) { - self.platform_window.set_exclusive_zone(zone); - } - - /// Set which anchored edge a layer-shell surface's exclusive zone applies to. - /// This is only needed to disambiguate a corner-anchored surface; otherwise the - /// edge is deduced from the anchor. The edge must be a single edge the surface - /// is anchored to, or it is ignored. (Wayland layer-shell windows only) - #[cfg(all(target_os = "linux", feature = "wayland"))] - pub fn set_exclusive_edge(&self, edge: crate::layer_shell::Anchor) { - self.platform_window.set_exclusive_edge(edge); - } - - /// Start an interactive window resize operation if this window is resizable. - pub fn start_window_resize(&self, edge: ResizeEdge) { - if self.is_resizable { - self.platform_window.start_window_resize(edge); - } - } - - /// Linux (wayland) only: Set the window's input region, the area that receives pointer - /// and touch input. Events outside it pass through to whatever is below the window. - /// - /// - `Some(rects)` restricts input to the union of `rects`, in window coordinates. - /// - `Some(&[])` is an empty region, so the window receives no pointer or touch input. - /// - `None` resets the region to the default, so the whole window receives input again. - pub fn set_input_region(&self, region: Option<&[Bounds]>) { - self.platform_window.set_input_region(region); - } - - /// Return the `WindowBounds` to indicate that how a window should be opened - /// after it has been closed - pub fn window_bounds(&self) -> WindowBounds { - self.platform_window.window_bounds() - } - - /// Return the `WindowBounds` excluding insets (Wayland and X11) - pub fn inner_window_bounds(&self) -> WindowBounds { - self.platform_window.inner_window_bounds() - } - - /// Dispatch the given action on the currently focused element. - pub fn dispatch_action(&mut self, action: Box, cx: &mut App) { - let focus_id = self.focused(cx).map(|handle| handle.id); - - let window = self.handle; - cx.defer(move |cx| { - window - .update(cx, |_, window, cx| { - let node_id = window.focus_node_id_in_rendered_frame(focus_id); - window.dispatch_action_on_node(node_id, action.as_ref(), cx); - }) - .log_err(); - }) - } - - pub(crate) fn dispatch_keystroke_observers( - &mut self, - event: &dyn Any, - action: Option>, - context_stack: Vec, - cx: &mut App, - ) { - let Some(key_down_event) = event.downcast_ref::() else { - return; - }; - - cx.keystroke_observers.clone().retain(&(), move |callback| { - (callback)( - &KeystrokeEvent { - keystroke: key_down_event.keystroke.clone(), - action: action.as_ref().map(|action| action.boxed_clone()), - context_stack: context_stack.clone(), - }, - self, - cx, - ) - }); - } - - pub(crate) fn dispatch_keystroke_interceptors( - &mut self, - event: &dyn Any, - context_stack: Vec, - cx: &mut App, - ) { - let Some(key_down_event) = event.downcast_ref::() else { - return; - }; - - cx.keystroke_interceptors - .clone() - .retain(&(), move |callback| { - (callback)( - &KeystrokeEvent { - keystroke: key_down_event.keystroke.clone(), - action: None, - context_stack: context_stack.clone(), - }, - self, - cx, - ) - }); - } - - /// Schedules the given function to be run at the end of the current effect cycle, allowing entities - /// that are currently on the stack to be returned to the app. - pub fn defer(&self, cx: &mut App, f: impl FnOnce(&mut Window, &mut App) + 'static) { - let handle = self.handle; - cx.defer(move |cx| { - handle.update(cx, |_, window, cx| f(window, cx)).ok(); - }); - } - - /// Subscribe to events emitted by a entity. - /// The entity to which you're subscribing must implement the [`EventEmitter`] trait. - /// The callback will be invoked a handle to the emitting entity, the event, and a window context for the current window. - pub fn observe( - &mut self, - observed: &Entity, - cx: &mut App, - mut on_notify: impl FnMut(Entity, &mut Window, &mut App) + 'static, - ) -> Subscription { - let entity_id = observed.entity_id(); - let observed = observed.downgrade(); - let window_handle = self.handle; - cx.new_observer( - entity_id, - Box::new(move |cx| { - window_handle - .update(cx, |_, window, cx| { - if let Some(handle) = observed.upgrade() { - on_notify(handle, window, cx); - true - } else { - false - } - }) - .unwrap_or(false) - }), - ) - } - - /// Subscribe to events emitted by a entity. - /// The entity to which you're subscribing must implement the [`EventEmitter`] trait. - /// The callback will be invoked a handle to the emitting entity, the event, and a window context for the current window. - pub fn subscribe( - &mut self, - entity: &Entity, - cx: &mut App, - mut on_event: impl FnMut(Entity, &Evt, &mut Window, &mut App) + 'static, - ) -> Subscription - where - Emitter: EventEmitter, - Evt: 'static, - { - let entity_id = entity.entity_id(); - let handle = entity.downgrade(); - let window_handle = self.handle; - cx.new_subscription( - entity_id, - ( - TypeId::of::(), - Box::new(move |event, cx| { - window_handle - .update(cx, |_, window, cx| { - if let Some(entity) = handle.upgrade() { - let event = event.downcast_ref().expect("invalid event type"); - on_event(entity, event, window, cx); - true - } else { - false - } - }) - .unwrap_or(false) - }), - ), - ) - } - - /// Register a callback to be invoked when the given `Entity` is released. - pub fn observe_release( - &self, - entity: &Entity, - cx: &mut App, - mut on_release: impl FnOnce(&mut T, &mut Window, &mut App) + 'static, - ) -> Subscription - where - T: 'static, - { - let entity_id = entity.entity_id(); - let window_handle = self.handle; - let (subscription, activate) = cx.release_listeners.insert( - entity_id, - Box::new(move |entity, cx| { - let entity = entity.downcast_mut().expect("invalid entity type"); - let _ = window_handle.update(cx, |_, window, cx| on_release(entity, window, cx)); - }), - ); - activate(); - subscription - } - - /// Creates an [`AsyncWindowContext`], which has a static lifetime and can be held across - /// await points in async code. - pub fn to_async(&self, cx: &App) -> AsyncWindowContext { - AsyncWindowContext::new_context(cx.to_async(), self.handle) - } - - /// Schedule the given closure to be run directly after the current frame is rendered. - pub fn on_next_frame(&self, callback: impl FnOnce(&mut Window, &mut App) + 'static) { - RefCell::borrow_mut(&self.next_frame_callbacks).push(Box::new(callback)); - self.platform_window.schedule_frame(); - // Next-frame callbacks create frame demand without dirtying the - // window, so the platform's frame source must be woken explicitly. - self.invalidator.wake_platform(); - } - - /// Schedule a frame to be drawn on the next animation frame. - /// - /// This is useful for elements that need to animate continuously, such as a video player or an animated GIF. - /// It will cause the window to redraw on the next frame, even if no other changes have occurred. - /// - /// If called from within a view, it will notify that view on the next frame. Otherwise, it will refresh the entire window. - /// - /// Callers driving purely decorative animations (spinners, pulses, and the - /// like) should prefer [`AnimationExt::with_animation`](crate::AnimationExt::with_animation), - /// which automatically respects [`App::reduce_motion`]. When using this - /// method directly for decorative motion, check [`App::reduce_motion`] - /// and skip the frame request when it is set. - pub fn request_animation_frame(&self) { - let entity = self.current_view(); - self.on_next_frame(move |_, cx| cx.notify(entity)); - } - - /// Runs all callbacks scheduled via [`Self::on_next_frame`], returning how many ran. - /// - /// Tests have no platform frame loop, so this simulates the delivery of the - /// next frame. - #[cfg(any(test, feature = "test-support"))] - pub fn simulate_next_frame(&mut self, cx: &mut App) -> usize { - let callbacks = self.next_frame_callbacks.take(); - let count = callbacks.len(); - for callback in callbacks { - callback(self, cx); - } - count - } - - /// Spawn the future returned by the given closure on the application thread pool. - /// The closure is provided a handle to the current window and an `AsyncWindowContext` for - /// use within your future. - #[track_caller] - pub fn spawn(&self, cx: &App, f: AsyncFn) -> Task - where - R: 'static, - AsyncFn: AsyncFnOnce(&mut AsyncWindowContext) -> R + 'static, - { - let handle = self.handle; - cx.spawn(async move |app| { - let mut async_window_cx = AsyncWindowContext::new_context(app.clone(), handle); - f(&mut async_window_cx).await - }) - } - - /// Spawn the future returned by the given closure on the application thread - /// pool, with the given priority. The closure is provided a handle to the - /// current window and an `AsyncWindowContext` for use within your future. - #[track_caller] - pub fn spawn_with_priority( - &self, - priority: Priority, - cx: &App, - f: AsyncFn, - ) -> Task - where - R: 'static, - AsyncFn: AsyncFnOnce(&mut AsyncWindowContext) -> R + 'static, - { - let handle = self.handle; - cx.spawn_with_priority(priority, async move |app| { - let mut async_window_cx = AsyncWindowContext::new_context(app.clone(), handle); - f(&mut async_window_cx).await - }) - } - - /// Notify the window that its bounds have changed. - /// - /// This updates internal state like `viewport_size` and `scale_factor` from - /// the platform window, then notifies observers. Normally called automatically - /// by the platform's resize callback, but exposed publicly for test infrastructure. - pub fn bounds_changed(&mut self, cx: &mut App) { - self.scale_factor = self.platform_window.scale_factor(); - self.viewport_size = self.platform_window.content_size(); - self.display_id = self.platform_window.display().map(|display| display.id()); - self.mouse_position = self.platform_window.mouse_position(); - - self.refresh(); - - self.bounds_observers - .clone() - .retain(&(), |callback| callback(self, cx)); - } - - /// Returns the bounds of the current window in the global coordinate space, which could span across multiple displays. - pub fn bounds(&self) -> Bounds { - self.platform_window.bounds() - } - - /// Renders the current frame's scene to a texture and returns the pixel data as an RGBA image. - /// This does not present the frame to screen - useful for visual testing where we want - /// to capture what would be rendered without displaying it or requiring the window to be visible. - #[cfg(any(test, feature = "test-support"))] - pub fn render_to_image(&self) -> anyhow::Result { - self.platform_window - .render_to_image(&self.rendered_frame.scene) - } - - /// Returns the quads in the most recently rendered frame's scene, so tests can assert on - /// painted output without rasterizing the frame. Quad bounds are in scaled pixels and are - /// not clipped; each quad carries the content mask it will be clipped to when drawn. Quads - /// whose bounds don't intersect their content mask are culled at paint time and won't appear. - #[cfg(any(test, feature = "test-support"))] - pub fn painted_quads(&self) -> Vec { - self.rendered_frame.scene.quads.clone() - } - - /// Returns the immutable rounded clip nodes referenced by painted primitives. - #[cfg(any(test, feature = "test-support"))] - pub fn painted_clips(&self) -> Vec> { - self.rendered_frame.scene.rounded_clips.clone() - } - - /// Returns painted shadows for renderer regression tests. - #[cfg(any(test, feature = "test-support"))] - pub fn painted_shadows(&self) -> Vec { - self.rendered_frame.scene.shadows.clone() - } - - /// Set the content size of the window. - pub fn resize(&mut self, size: Size) { - self.platform_window.resize(size); - } - - /// Returns whether or not the window is currently fullscreen - pub fn is_fullscreen(&self) -> bool { - self.platform_window.is_fullscreen() - } - - /// Returns whether the window is currently in simple (borderless) fullscreen, - /// where it covers the entire screen including the menu bar and notch area. - /// Always `false` on platforms other than macOS. - pub fn is_simple_fullscreen(&self) -> bool { - self.platform_window.is_simple_fullscreen() - } - - pub(crate) fn appearance_changed(&mut self, cx: &mut App) { - self.appearance = self.platform_window.appearance(); - - self.appearance_observers - .clone() - .retain(&(), |callback| callback(self, cx)); - } - - pub(crate) fn button_layout_changed(&mut self, cx: &mut App) { - self.button_layout_observers - .clone() - .retain(&(), |callback| callback(self, cx)); - } - - /// Returns the appearance of the current window. - pub fn appearance(&self) -> WindowAppearance { - self.appearance - } - - /// Returns the size of the drawable area within the window. - pub fn viewport_size(&self) -> Size { - self.viewport_size - } - - /// Returns whether this window is focused by the operating system (receiving key events). - pub fn is_window_active(&self) -> bool { - self.active.get() - } - - /// Returns whether this window is considered to be the window - /// that currently owns the mouse cursor. - /// On mac, this is equivalent to `is_window_active`. - pub fn is_window_hovered(&self) -> bool { - if cfg!(any( - target_os = "windows", - target_os = "linux", - target_os = "freebsd" - )) { - self.hovered.get() - } else { - self.is_window_active() - } - } - - /// Toggle zoom on the window. - pub fn zoom_window(&self) { - self.platform_window.zoom(); - } - - /// Opens the native title bar context menu, useful when implementing client side decorations (Wayland and X11) - pub fn show_window_menu(&self, position: Point) { - self.platform_window.show_window_menu(position) - } - - /// Handle window movement for Linux and macOS. - /// Tells the compositor to take control of window movement (Wayland and X11) - /// - /// Events may not be received during a move operation. - pub fn start_window_move(&self) { - self.platform_window.start_window_move() - } - - /// When using client side decorations, set this to the width of the invisible decorations (Wayland and X11) - pub fn set_client_inset(&mut self, inset: Pixels) { - self.client_inset = Some(inset); - self.platform_window.set_client_inset(inset); - } - - /// Returns the client_inset value by [`Self::set_client_inset`]. - pub fn client_inset(&self) -> Option { - self.client_inset - } - - /// Returns whether the title bar window controls need to be rendered by the application (Wayland and X11) - pub fn window_decorations(&self) -> Decorations { - self.platform_window.window_decorations() - } - - /// Returns whether this window is resizable. - pub fn is_resizable(&self) -> bool { - self.is_resizable - } - - /// Returns whether this window is minimizable. - pub fn is_minimizable(&self) -> bool { - self.is_minimizable - } - - /// Returns the controls supported by the platform. - pub fn window_controls(&self) -> WindowControls { - self.platform_window.window_controls() - } - - /// Updates the window's title at the platform level. - pub fn set_window_title(&mut self, title: &str) { - self.platform_window.set_title(title); - self.a11y.set_window_title(title.to_string()); - } - - /// Sets the position of the macOS traffic light buttons. - #[cfg(target_os = "macos")] - pub fn set_traffic_light_position(&self, position: Point) { - self.platform_window.set_traffic_light_position(position); - } - - /// Sets the application identifier. - pub fn set_app_id(&mut self, app_id: &str) { - self.platform_window.set_app_id(app_id); - } - - /// Sets the window background appearance. - pub fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { - self.platform_window - .set_background_appearance(background_appearance); - } - - /// Mark the window as dirty at the platform level. - pub fn set_window_edited(&mut self, edited: bool) { - self.platform_window.set_edited(edited); - } - - /// Set the path of the file this window represents. - /// On macOS, this sets the window's accessibility document property (AXDocument). - pub fn set_document_path(&self, path: Option<&std::path::Path>) { - self.platform_window.set_document_path(path); - } - - /// Determine the display on which the window is visible. - pub fn display(&self, cx: &App) -> Option> { - cx.platform - .displays() - .into_iter() - .find(|display| Some(display.id()) == self.display_id) - } - - /// Show the platform character palette. - pub fn show_character_palette(&self) { - self.platform_window.show_character_palette(); - } - - /// The scale factor of the display associated with the window. For example, it could - /// return 2.0 for a "retina" display, indicating that each logical pixel should actually - /// be rendered as two pixels on screen. - pub fn scale_factor(&self) -> f32 { - self.scale_factor - } - - /// Overrides the display scale factor for tests. - #[cfg(any(test, feature = "test-support"))] - pub fn set_scale_factor(&mut self, scale_factor: f32) { - self.scale_factor = scale_factor; - self.refresh(); - } - - /// The size of an em for the base font of the application. Adjusting this value allows the - /// UI to scale, just like zooming a web page. - pub fn rem_size(&self) -> Pixels { - self.rem_size_override_stack - .last() - .copied() - .unwrap_or(self.rem_size) - } - - /// Sets the size of an em for the base font of the application. Adjusting this value allows the - /// UI to scale, just like zooming a web page. - pub fn set_rem_size(&mut self, rem_size: impl Into) { - self.rem_size = rem_size.into(); - } - - /// Acquire a globally unique identifier for the given ElementId. - /// Only valid for the duration of the provided closure. - pub fn with_global_id( - &mut self, - element_id: ElementId, - f: impl FnOnce(&GlobalElementId, &mut Self) -> R, - ) -> R { - self.with_id(element_id, |this| { - let global_id = GlobalElementId(Arc::from(&*this.element_id_stack)); - - f(&global_id, this) - }) - } - - /// Calls the provided closure with the element ID pushed on the stack. - #[inline] - pub fn with_id( - &mut self, - element_id: impl Into, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - self.element_id_stack.push(element_id.into()); - let result = f(self); - self.element_id_stack.pop(); - result - } - - /// Executes the provided function with the specified rem size. - /// - /// This method must only be called as part of element drawing. - // This function is called in a highly recursive manner in editor - // prepainting, make sure its inlined to reduce the stack burden - #[inline] - pub fn with_rem_size(&mut self, rem_size: Option>, f: F) -> R - where - F: FnOnce(&mut Self) -> R, - { - self.invalidator.debug_assert_paint_or_prepaint(); - - if let Some(rem_size) = rem_size { - self.rem_size_override_stack.push(rem_size.into()); - let result = f(self); - self.rem_size_override_stack.pop(); - result - } else { - f(self) - } - } - - /// The line height associated with the current text style. - pub fn line_height(&self) -> Pixels { - self.text_style().line_height_in_pixels(self.rem_size()) - } - - /// Rounds a logical value to the nearest device pixel. - #[inline] - pub fn pixel_snap(&self, value: Pixels) -> Pixels { - px(round_to_device_pixel(value.0, self.scale_factor()) / self.scale_factor()) - } - - /// f64 variant of [`Self::pixel_snap`]. - #[inline] - pub fn pixel_snap_f64(&self, value: f64) -> f64 { - let scale_factor = f64::from(self.scale_factor()); - round_half_toward_zero_f64(value * scale_factor) / scale_factor - } - - /// Snaps a bounds' origin and size to the nearest device pixel. - #[inline] - pub fn pixel_snap_bounds(&self, bounds: Bounds) -> Bounds { - bounds.map(|c| self.pixel_snap(c)) - } - - /// Snaps a point's coordinates to the nearest device pixel. - #[inline] - pub fn pixel_snap_point(&self, position: Point) -> Point { - position.map(|c| self.pixel_snap(c)) - } - - #[inline] - fn snap_bounds(&self, bounds: Bounds) -> Bounds { - let scale_factor = self.scale_factor(); - let left = round_to_device_pixel(bounds.left().0, scale_factor); - let top = round_to_device_pixel(bounds.top().0, scale_factor); - let right = round_to_device_pixel(bounds.right().0, scale_factor).max(left); - let bottom = round_to_device_pixel(bounds.bottom().0, scale_factor).max(top); - Bounds::from_corners( - point(ScaledPixels(left), ScaledPixels(top)), - point(ScaledPixels(right), ScaledPixels(bottom)), - ) - } - - /// Rounds half-to-zero but clamps any non-zero input up to 1 dp so thin strokes do not disappear. - #[inline] - fn snap_stroke(&self, value: Pixels) -> ScaledPixels { - ScaledPixels(round_stroke_to_device_pixel(value.0, self.scale_factor())) - } - - #[inline] - fn snap_border_widths(&self, edges: Edges) -> Edges { - edges.map(|e| self.snap_stroke(*e)) - } - - /// Floors the near edge and ceils the far edge, producing a strict superset of the raw region. - #[inline] - fn cover_bounds(&self, bounds: Bounds) -> Bounds { - let scale_factor = self.scale_factor(); - let left = floor_to_device_pixel(bounds.left().0, scale_factor); - let top = floor_to_device_pixel(bounds.top().0, scale_factor); - let right = ceil_to_device_pixel(bounds.right().0, scale_factor).max(left); - let bottom = ceil_to_device_pixel(bounds.bottom().0, scale_factor).max(top); - Bounds::from_corners( - point(ScaledPixels(left), ScaledPixels(top)), - point(ScaledPixels(right), ScaledPixels(bottom)), - ) - } - - #[inline] - fn snapped_content_mask(&mut self) -> ContentMask { - let region = self.content_mask(); - let mut clip_index = 0; - for shape in ®ion.rounded_clips { - let mut clip = shape.scale(self.scale_factor()); - // Use the original element's snapped shape, as paint_quad does. - // A descendant's culling bounds must never relocate these curves. - clip.bounds = self.snap_bounds(shape.bounds); - clip.parent = clip_index; - clip_index = self.next_frame.scene.insert_clip(clip); - } - ContentMask { - bounds: self.cover_bounds(region.bounds), - corner_radii: Corners::default(), - clip_index, - clip_padding: 0, - } - } - - /// Call to prevent the default action of an event. Currently only used to prevent - /// parent elements from becoming focused on mouse down. - pub fn prevent_default(&mut self) { - self.default_prevented = true; - } - - /// Obtain whether default has been prevented for the event currently being dispatched. - pub fn default_prevented(&self) -> bool { - self.default_prevented - } - - /// Determine whether the given action is available along the dispatch path to the currently focused element. - pub fn is_action_available(&self, action: &dyn Action, cx: &App) -> bool { - let node_id = - self.focus_node_id_in_rendered_frame(self.focused(cx).map(|handle| handle.id)); - self.rendered_frame - .dispatch_tree - .is_action_available(action, node_id) - } - - /// Determine whether the given action is available along the dispatch path to the given focus_handle. - pub fn is_action_available_in(&self, action: &dyn Action, focus_handle: &FocusHandle) -> bool { - let node_id = self.focus_node_id_in_rendered_frame(Some(focus_handle.id)); - self.rendered_frame - .dispatch_tree - .is_action_available(action, node_id) - } - - /// The position of the mouse relative to the window. - pub fn mouse_position(&self) -> Point { - self.mouse_position - } - - /// Captures the pointer for the given hitbox. While captured, all mouse move and mouse up - /// events will be routed to listeners that check this hitbox's `is_hovered` status, - /// regardless of actual hit testing. This enables drag operations that continue - /// even when the pointer moves outside the element's bounds. - /// - /// The capture is automatically released on mouse up. - pub fn capture_pointer(&mut self, hitbox_id: HitboxId) { - self.captured_hitbox = Some(hitbox_id); - } - - /// Releases any active pointer capture. - pub fn release_pointer(&mut self) { - self.captured_hitbox = None; - } - - /// Returns the hitbox that has captured the pointer, if any. - pub fn captured_hitbox(&self) -> Option { - self.captured_hitbox - } - - /// Captures the current long press for the given entity. - /// - /// The capture is released when the gesture ends or is cancelled, or when - /// a replacement touch begins. A listener must also call - /// [`Self::prevent_default`] on the started event to claim the gesture. - pub fn capture_long_press(&mut self, entity: &Entity) { - self.long_press_capture = Some(entity.entity_id()); - } - - /// Returns whether the given entity has captured the current long press. - pub fn has_long_press_capture(&self, entity: &Entity) -> bool { - self.long_press_capture == Some(entity.entity_id()) - } - - /// The current state of the keyboard's modifiers - pub fn modifiers(&self) -> Modifiers { - self.modifiers - } - - /// Returns true if the last input event was keyboard-based (key press, tab navigation, etc.) - /// This is used for focus-visible styling to show focus indicators only for keyboard navigation. - pub fn last_input_was_keyboard(&self) -> bool { - self.last_input_modality == InputModality::Keyboard - } - - pub(crate) fn last_input_was_touch(&self) -> bool { - self.last_input_modality == InputModality::Touch - } - - /// The current state of the keyboard's capslock - pub fn capslock(&self) -> Capslock { - self.capslock - } - - /// Produces a new frame and assigns it to `rendered_frame`. To actually show - /// the contents of the new [`Scene`], use [`Self::present`]. - #[profiling::function] - pub fn draw(&mut self, cx: &mut App) -> ArenaClearNeeded { - // Drain every draw in profiler builds so a previous frame's - // first-invalidation timestamp can't be attributed to this one. - #[cfg(feature = "profiler")] - let frame_dirty = self.invalidator.take_frame_dirty(); - #[cfg(feature = "profiler")] - self.window_profiler.begin_draw(); - - // Set up the per-App arena for element allocation during this draw. - // This ensures that multiple test Apps have isolated arenas. - let arena_scope = ElementArenaScope::enter(&cx.element_arena); - - self.invalidate_entities(); - cx.entities.clear_accessed(); - debug_assert!(self.rendered_entity_stack.is_empty()); - self.invalidator.set_dirty(false); - self.requested_autoscroll = None; - - // Restore the previously-used input handler. - // Place it back into a None slot (left by a previous .take()) so that - // cached paint_range indices in reuse_paint find the handler at the - // expected position. - if let Some(input_handler) = self.platform_window.take_input_handler() { - if let Some(slot) = self - .rendered_frame - .input_handlers - .iter_mut() - .rev() - .find(|h| h.is_none()) - { - *slot = Some(input_handler); - } else { - self.rendered_frame.input_handlers.push(Some(input_handler)); - } - } - if !cx.mode.skip_drawing() { - self.draw_roots(cx); - #[cfg(feature = "profiler")] - { - let viewport_size = self.viewport_size; - let scale_factor = self.scale_factor(); - self.debug_frame_overlay.paint( - &mut self.next_frame.scene, - viewport_size, - scale_factor, - ); - } - } - self.dirty_views.clear(); - self.next_frame.window_active = self.active.get(); - - // Register requested input handler with the platform window. - // Use .take() instead of .pop() to preserve Vec length, so that cached - // paint_range indices remain valid for reuse_paint on the next frame. - // Search backwards to find the last Some entry, since reuse_paint may - // have copied None slots from the previous frame. (Fixes #50456) - let focused_text_input_active = if let Some(mut input_handler) = self - .next_frame - .input_handlers - .iter_mut() - .rev() - .find_map(|h| h.take()) - { - let accepts_text_input = input_handler.accepts_text_input(self, cx); - self.platform_window.set_input_handler(input_handler); - accepts_text_input - } else { - false - }; - self.apply_text_input_configuration(cx); - if focused_text_input_active != self.focused_text_input_active { - self.focused_text_input_active = focused_text_input_active; - self.platform_window - .text_input_state_changed(if focused_text_input_active { - TextInputStateChange::FocusGained - } else { - TextInputStateChange::FocusLost - }); - } - - self.layout_engine.as_mut().unwrap().clear(); - self.text_system().finish_frame(); - self.next_frame.finish(&mut self.rendered_frame); - - self.invalidator.set_phase(DrawPhase::Focus); - let previous_focus_path = self.rendered_frame.focus_path(); - let previous_window_active = self.rendered_frame.window_active; - mem::swap(&mut self.rendered_frame, &mut self.next_frame); - self.next_frame.clear(); - let current_focus_path = self.rendered_frame.focus_path(); - let current_window_active = self.rendered_frame.window_active; - let mut focus_before_listeners = self.focus; - - if previous_focus_path != current_focus_path - || previous_window_active != current_window_active - { - if !previous_focus_path.is_empty() && current_focus_path.is_empty() { - self.focus_lost_path = previous_focus_path.clone(); - self.focus_lost_listeners - .clone() - .retain(&(), |listener| listener(self, cx)); - self.focus_lost_path = SmallVec::new(); - // The focus-lost fallback (e.g. a workspace refocusing itself) may target - // an element that isn't part of the element tree, in which case scheduling - // a redraw below would dispatch focus-lost again, looping forever. Only - // track focus movement caused by the focus listeners. - focus_before_listeners = self.focus; - } - - let event = WindowFocusEvent { - previous_focus_path: if previous_window_active { - previous_focus_path - } else { - Default::default() - }, - current_focus_path: if current_window_active { - current_focus_path - } else { - Default::default() - }, - }; - self.focus_listeners - .clone() - .retain(&(), |listener| listener(&event, self, cx)); - } - - debug_assert!(self.rendered_entity_stack.is_empty()); - self.record_entities_accessed(cx); - self.reset_cursor_style(cx); - self.refreshing = false; - self.invalidator.set_phase(DrawPhase::None); - // Focus listeners may move focus (e.g. a dock forwarding focus to its active - // panel). `Window::focus` suppresses `refresh` while a draw is in progress, so - // schedule another frame here to render the new focus state and dispatch the - // resulting focus events. - if self.focus != focus_before_listeners { - self.refresh(); - } - self.needs_present.set(true); - - #[cfg(feature = "profiler")] - { - let draw_duration = self - .window_profiler - .end_draw(frame_dirty.dirty_at, frame_dirty.invalidations); - self.debug_frame_overlay.record_frame(draw_duration); - } - - // Exit the scope to obtain the arena-clear token this draw owes; the - // scope's teardown itself happens in `ElementArenaScope::drop`. - arena_scope.exit(&cx.element_arena) - } - - fn record_entities_accessed(&mut self, cx: &mut App) { - let mut entities_ref = cx.entities.accessed_entities.get_mut(); - let mut entities = mem::take(entities_ref.deref_mut()); - let handle = self.handle; - cx.record_entities_accessed( - handle, - // Try moving window invalidator into the Window - self.invalidator.clone(), - &entities, - ); - let mut entities_ref = cx.entities.accessed_entities.get_mut(); - mem::swap(&mut entities, entities_ref.deref_mut()); - } - - fn invalidate_entities(&mut self) { - let mut views = self.invalidator.take_views(); - for entity in views.drain() { - self.mark_view_dirty(entity); - } - self.invalidator.replace_views(views); - } - - #[profiling::function] - fn present(&mut self) { - #[cfg(feature = "profiler")] - let _foreground_turn = profiler::journal::foreground_turn(); - #[cfg(feature = "profiler")] - let present_start = Instant::now(); - self.platform_window.draw(&self.rendered_frame.scene); - #[cfg(feature = "profiler")] - self.window_profiler.record_present( - present_start, - Instant::now(), - self.active.get(), - !self.next_frame_callbacks.borrow().is_empty(), - ); - self.needs_present.set(false); - profiling::finish_frame!(); - } - - /// Presents the most recently drawn frame if it hasn't been presented yet. - /// - /// Benchmarks drive drawing synchronously rather than through a platform - /// frame-request loop, so they call this after each measured update to - /// submit the frame like production presentation would. - #[cfg(any(feature = "bench-support", all(test, feature = "profiler")))] - pub fn present_if_needed(&mut self) { - if self.needs_present.get() { - self.present(); - } - } - - /// Returns a snapshot of the current input-latency histograms. - #[cfg(feature = "profiler")] - pub fn input_latency_snapshot(&self) -> profiler::InputLatencySnapshot { - self.window_profiler.input_latency_snapshot() - } - - /// Returns a snapshot of the current frame-duration histograms. - #[cfg(feature = "profiler")] - pub fn frame_duration_snapshot(&self) -> profiler::FrameDurationSnapshot { - self.window_profiler.frame_duration_snapshot() - } - - /// Returns the current mode of the debug frame overlay. - #[cfg(feature = "profiler")] - pub fn debug_frame_overlay_mode(&self) -> DebugFrameOverlayMode { - self.debug_frame_overlay.mode() - } - - /// Sets the mode of the debug frame overlay and schedules a redraw. - #[cfg(feature = "profiler")] - pub fn set_debug_frame_overlay_mode(&mut self, mode: DebugFrameOverlayMode) { - self.debug_frame_overlay.set_mode(mode); - self.refresh(); - } - - /// Advances the debug frame overlay through its hidden, frame-time-only, - /// and detailed modes. - #[cfg(feature = "profiler")] - pub fn cycle_debug_frame_overlay_mode(&mut self) { - self.set_debug_frame_overlay_mode(self.debug_frame_overlay.mode().next()); - } - - /// Clears the debug frame overlay's frame-time statistics, except for the - /// total frame count, and schedules a redraw. - #[cfg(feature = "profiler")] - pub fn reset_debug_frame_overlay_stats(&mut self) { - self.debug_frame_overlay.reset_stats(); - self.refresh(); - } - - fn draw_roots(&mut self, cx: &mut App) { - self.invalidator.set_phase(DrawPhase::Prepaint); - self.tooltip_bounds.take(); - - self.a11y.sync_active_flag(); - if self.a11y.is_active() { - self.a11y.begin_frame(); - } - - let _inspector_width: Pixels = rems(30.0).to_pixels(self.rem_size()); - let root_size = { - #[cfg(any(feature = "inspector", debug_assertions))] - { - if self.inspector.is_some() { - let mut size = self.viewport_size; - size.width = (size.width - _inspector_width).max(px(0.0)); - size - } else { - self.viewport_size - } - } - #[cfg(not(any(feature = "inspector", debug_assertions)))] - { - self.viewport_size - } - }; - - // Layout all root elements. Like the root element on the web, which - // stretches to fill the viewport unless explicitly sized, window roots - // fill the window when their size is `auto`. - let scale_factor = self.scale_factor(); - let mut root_element = self.root.as_ref().unwrap().clone().into_any_element(); - let root_layout_id = root_element.request_layout(self, cx); - self.layout_engine - .as_mut() - .unwrap() - .stretch_auto_size_to_fill(root_layout_id, root_size, scale_factor); - root_element.prepaint_as_root(Point::default(), root_size.into(), self, cx); - - #[cfg(any(feature = "inspector", debug_assertions))] - let inspector_element = self.prepaint_inspector(_inspector_width, cx); - - self.prepaint_deferred_draws(cx); - - let mut prompt_element = None; - let mut active_drag_element = None; - let mut tooltip_element = None; - if let Some(prompt) = self.prompt.take() { - let mut element = prompt.view.any_view().into_any_element(); - let prompt_layout_id = element.request_layout(self, cx); - self.layout_engine - .as_mut() - .unwrap() - .stretch_auto_size_to_fill(prompt_layout_id, root_size, scale_factor); - element.prepaint_as_root(Point::default(), root_size.into(), self, cx); - prompt_element = Some(element); - self.prompt = Some(prompt); - } else if let Some(active_drag) = cx.active_drag.take() { - let mut element = active_drag.view.clone().into_any_element(); - let offset = self.mouse_position() - active_drag.cursor_offset; - element.prepaint_as_root(offset, AvailableSpace::min_size(), self, cx); - active_drag_element = Some(element); - cx.active_drag = Some(active_drag); - } else { - tooltip_element = self.prepaint_tooltip(cx); - } - - self.mouse_hit_test = self.next_frame.hit_test(self.mouse_position); - - // Now actually paint the elements. - self.invalidator.set_phase(DrawPhase::Paint); - root_element.paint(self, cx); - - #[cfg(any(feature = "inspector", debug_assertions))] - self.paint_inspector(inspector_element, cx); - - self.paint_deferred_draws(cx); - - if let Some(mut prompt_element) = prompt_element { - prompt_element.paint(self, cx); - } else if let Some(mut drag_element) = active_drag_element { - drag_element.paint(self, cx); - } else if let Some(mut tooltip_element) = tooltip_element { - tooltip_element.paint(self, cx); - } - - #[cfg(any(feature = "inspector", debug_assertions))] - self.paint_inspector_hitbox(cx); - - // a11y may have been activated/deactivated halfway through the frame - let a11y_active_start_of_frame = self.a11y.is_active(); - self.a11y.sync_active_flag(); - let a11y_active_end_of_frame = self.a11y.is_active(); - - let should_send_a11y_update = a11y_active_start_of_frame && a11y_active_end_of_frame; - - if a11y_active_start_of_frame { - // Harvest frame metadata for the debug dump while the live window - // and frame are still in scope. - let frame_info = crate::window::a11y::debug::FrameDebugInfo { - viewport_size: self.viewport_size, - scale_factor: self.scale_factor, - tab_stop_count: self.next_frame.tab_stops.tab_stop_count(), - }; - // clear the builder state regardless - let tree_update = self.a11y.end_frame(frame_info); - - if should_send_a11y_update { - log::debug!( - "Sending a11y tree update: {} nodes", - tree_update.nodes.len() - ); - self.platform_window.a11y_tree_update(tree_update); - } - } - } - - fn prepaint_tooltip(&mut self, cx: &mut App) -> Option { - // Use indexing instead of iteration to avoid borrowing self for the duration of the loop. - for tooltip_request_index in (0..self.next_frame.tooltip_requests.len()).rev() { - let Some(Some(tooltip_request)) = self - .next_frame - .tooltip_requests - .get(tooltip_request_index) - .cloned() - else { - log::error!("Unexpectedly absent TooltipRequest"); - continue; - }; - let mut element = tooltip_request.tooltip.view.clone().into_any_element(); - let mouse_position = tooltip_request.tooltip.mouse_position; - let tooltip_size = element.layout_as_root(AvailableSpace::min_size(), self, cx); - - let mut tooltip_bounds = - Bounds::new(mouse_position + point(px(1.), px(1.)), tooltip_size); - let window_bounds = Bounds { - origin: Point::default(), - size: self.viewport_size(), - }; - - if tooltip_bounds.right() > window_bounds.right() { - let new_x = mouse_position.x - tooltip_bounds.size.width - px(1.); - if new_x >= Pixels::ZERO { - tooltip_bounds.origin.x = new_x; - } else { - tooltip_bounds.origin.x = cmp::max( - Pixels::ZERO, - tooltip_bounds.origin.x - tooltip_bounds.right() - window_bounds.right(), - ); - } - } - - if tooltip_bounds.bottom() > window_bounds.bottom() { - let new_y = mouse_position.y - tooltip_bounds.size.height - px(1.); - if new_y >= Pixels::ZERO { - tooltip_bounds.origin.y = new_y; - } else { - tooltip_bounds.origin.y = cmp::max( - Pixels::ZERO, - tooltip_bounds.origin.y - tooltip_bounds.bottom() - window_bounds.bottom(), - ); - } - } - - // It's possible for an element to have an active tooltip while not being painted (e.g. - // via the `visible_on_hover` method). Since mouse listeners are not active in this - // case, instead update the tooltip's visibility here. - let is_visible = - (tooltip_request.tooltip.check_visible_and_update)(tooltip_bounds, self, cx); - if !is_visible { - continue; - } - - self.with_absolute_element_offset(tooltip_bounds.origin, |window| { - element.prepaint(window, cx) - }); - - self.tooltip_bounds = Some(TooltipBounds { - id: tooltip_request.id, - bounds: tooltip_bounds, - }); - return Some(element); - } - None - } - - fn prepaint_deferred_draws(&mut self, cx: &mut App) { - assert_eq!(self.element_id_stack.len(), 0); - - // Process deferred draws in multiple rounds to support nesting. - // Each round processes all current deferred draws, which may push new ones. - // - // The draws are processed in place rather than being moved out of - // `next_frame.deferred_draws`: `prepaint_index` snapshots that vector's - // length, so any prepaint range recorded during a round (view caches, - // nested deferred draws) must index the same vector `reuse_prepaint` - // slices on the next frame. Moving the draws out and re-appending them - // shifts the indices of nested draws, causing reused subtrees to graft - // the wrong deferred draws and panic in the dispatch tree. - let mut round_start = 0; - let mut depth = 0; - loop { - let round_end = self.next_frame.deferred_draws.len(); - if round_start == round_end { - break; - } - // Limit maximum nesting depth to prevent infinite loops. - assert!(depth < 10, "Exceeded maximum (10) deferred depth"); - depth += 1; - - // Sort this round by priority. - let mut traversal_order = (round_start..round_end).collect::>(); - traversal_order.sort_by_key(|ix| self.next_frame.deferred_draws[*ix].priority); - - for deferred_draw_ix in traversal_order { - let (element, parent_node, current_view, rem_size, absolute_offset, prepaint_range) = { - let deferred_draw = &mut self.next_frame.deferred_draws[deferred_draw_ix]; - self.element_id_stack - .clone_from(&deferred_draw.element_id_stack); - self.text_style_stack - .clone_from(&deferred_draw.text_style_stack); - ( - deferred_draw.element.take(), - deferred_draw.parent_node, - deferred_draw.current_view, - deferred_draw.rem_size, - deferred_draw.absolute_offset, - deferred_draw.prepaint_range.clone(), - ) - }; - self.next_frame.dispatch_tree.set_active_node(parent_node); - - let prepaint_start = self.prepaint_index(); - if let Some(mut element) = element { - self.with_rendered_view(current_view, |window| { - window.with_rem_size(Some(rem_size), |window| { - window.with_absolute_element_offset(absolute_offset, |window| { - element.prepaint(window, cx); - }); - }); - }); - self.next_frame.deferred_draws[deferred_draw_ix].element = Some(element); - } else { - self.reuse_prepaint(prepaint_range); - } - let prepaint_end = self.prepaint_index(); - self.next_frame.deferred_draws[deferred_draw_ix].prepaint_range = - prepaint_start..prepaint_end; - } - - self.element_id_stack.clear(); - self.text_style_stack.clear(); - round_start = round_end; - } - } - - fn paint_deferred_draws(&mut self, cx: &mut App) { - assert_eq!(self.element_id_stack.len(), 0); - - // Paint all deferred draws in priority order. - // Since prepaint has already processed nested deferreds, we just paint them all. - if self.next_frame.deferred_draws.len() == 0 { - return; - } - - let traversal_order = self.deferred_draw_traversal_order(); - let mut deferred_draws = mem::take(&mut self.next_frame.deferred_draws); - for deferred_draw_ix in traversal_order { - let mut deferred_draw = &mut deferred_draws[deferred_draw_ix]; - self.element_id_stack - .clone_from(&deferred_draw.element_id_stack); - self.next_frame - .dispatch_tree - .set_active_node(deferred_draw.parent_node); - - let paint_start = self.paint_index(); - let content_mask = deferred_draw.content_mask.clone(); - if let Some(element) = deferred_draw.element.as_mut() { - self.with_rendered_view(deferred_draw.current_view, |window| { - window.with_content_mask(content_mask, |window| { - window.with_rem_size(Some(deferred_draw.rem_size), |window| { - element.paint(window, cx); - }); - }) - }) - } else { - self.reuse_paint(deferred_draw.paint_range.clone()); - } - let paint_end = self.paint_index(); - deferred_draw.paint_range = paint_start..paint_end; - } - self.next_frame.deferred_draws = deferred_draws; - self.element_id_stack.clear(); - } - - fn deferred_draw_traversal_order(&mut self) -> SmallVec<[usize; 8]> { - let deferred_count = self.next_frame.deferred_draws.len(); - let mut sorted_indices = (0..deferred_count).collect::>(); - sorted_indices.sort_by_key(|ix| self.next_frame.deferred_draws[*ix].priority); - sorted_indices - } - - pub(crate) fn prepaint_index(&self) -> PrepaintStateIndex { - PrepaintStateIndex { - hitboxes_index: self.next_frame.hitboxes.len(), - tooltips_index: self.next_frame.tooltip_requests.len(), - deferred_draws_index: self.next_frame.deferred_draws.len(), - dispatch_tree_index: self.next_frame.dispatch_tree.len(), - accessed_element_states_index: self.next_frame.accessed_element_states.len(), - line_layout_index: self.text_system.layout_index(), - } - } - - pub(crate) fn reuse_prepaint(&mut self, range: Range) { - self.next_frame.hitboxes.extend( - self.rendered_frame.hitboxes[range.start.hitboxes_index..range.end.hitboxes_index] - .iter() - .cloned(), - ); - self.next_frame.tooltip_requests.extend( - self.rendered_frame.tooltip_requests - [range.start.tooltips_index..range.end.tooltips_index] - .iter_mut() - .map(|request| request.take()), - ); - self.next_frame.accessed_element_states.extend( - self.rendered_frame.accessed_element_states[range.start.accessed_element_states_index - ..range.end.accessed_element_states_index] - .iter() - .map(|(id, type_id)| (id.clone(), *type_id)), - ); - self.text_system - .reuse_layouts(range.start.line_layout_index..range.end.line_layout_index); - - let reused_subtree = self.next_frame.dispatch_tree.reuse_subtree( - range.start.dispatch_tree_index..range.end.dispatch_tree_index, - &mut self.rendered_frame.dispatch_tree, - self.focus, - ); - - if reused_subtree.contains_focus() { - self.next_frame.focus = self.focus; - } - - self.next_frame.deferred_draws.extend( - self.rendered_frame.deferred_draws - [range.start.deferred_draws_index..range.end.deferred_draws_index] - .iter() - .map(|deferred_draw| DeferredDraw { - current_view: deferred_draw.current_view, - parent_node: reused_subtree.refresh_node_id(deferred_draw.parent_node), - element_id_stack: deferred_draw.element_id_stack.clone(), - text_style_stack: deferred_draw.text_style_stack.clone(), - content_mask: deferred_draw.content_mask.clone(), - rem_size: deferred_draw.rem_size, - priority: deferred_draw.priority, - element: None, - absolute_offset: deferred_draw.absolute_offset, - prepaint_range: deferred_draw.prepaint_range.clone(), - paint_range: deferred_draw.paint_range.clone(), - }), - ); - } - - pub(crate) fn paint_index(&self) -> PaintIndex { - PaintIndex { - scene_index: self.next_frame.scene.len(), - mouse_listeners_index: self.next_frame.mouse_listeners.len(), - input_handlers_index: self.next_frame.input_handlers.len(), - cursor_styles_index: self.next_frame.cursor_styles.len(), - accessed_element_states_index: self.next_frame.accessed_element_states.len(), - tab_handle_index: self.next_frame.tab_stops.paint_index(), - line_layout_index: self.text_system.layout_index(), - } - } - - pub(crate) fn reuse_paint(&mut self, range: Range) { - self.next_frame.cursor_styles.extend( - self.rendered_frame.cursor_styles - [range.start.cursor_styles_index..range.end.cursor_styles_index] - .iter() - .cloned(), - ); - self.next_frame.input_handlers.extend( - self.rendered_frame.input_handlers - [range.start.input_handlers_index..range.end.input_handlers_index] - .iter_mut() - .map(|handler| handler.take()), - ); - self.next_frame.mouse_listeners.extend( - self.rendered_frame.mouse_listeners - [range.start.mouse_listeners_index..range.end.mouse_listeners_index] - .iter_mut() - .map(|listener| listener.take()), - ); - self.next_frame.accessed_element_states.extend( - self.rendered_frame.accessed_element_states[range.start.accessed_element_states_index - ..range.end.accessed_element_states_index] - .iter() - .map(|(id, type_id)| (id.clone(), *type_id)), - ); - self.next_frame.tab_stops.replay( - &self.rendered_frame.tab_stops.insertion_history - [range.start.tab_handle_index..range.end.tab_handle_index], - ); - - self.text_system - .reuse_layouts(range.start.line_layout_index..range.end.line_layout_index); - self.next_frame.scene.replay( - range.start.scene_index..range.end.scene_index, - &self.rendered_frame.scene, - ); - } - - /// Push a text style onto the stack, and call a function with that style active. - /// Use [`Window::text_style`] to get the current, combined text style. This method - /// should only be called as part of element drawing. - pub fn with_text_style(&mut self, style: Option, f: F) -> R - where - F: FnOnce(&mut Self) -> R, - { - self.invalidator.debug_assert_paint_or_prepaint(); - if let Some(style) = style { - self.text_style_stack.push(style); - let result = f(self); - self.text_style_stack.pop(); - result - } else { - f(self) - } - } - - /// Updates the cursor style at the platform level. This method should only be called - /// during the paint phase of element drawing. - pub fn set_cursor_style(&mut self, style: CursorStyle, hitbox: &Hitbox) { - self.invalidator.debug_assert_paint(); - self.next_frame.cursor_styles.push(CursorStyleRequest { - hitbox_id: Some(hitbox.id), - style, - }); - } - - /// Updates the cursor style for the entire window at the platform level. A cursor - /// style using this method will have precedence over any cursor style set using - /// `set_cursor_style`. This method should only be called during the paint - /// phase of element drawing. - pub fn set_window_cursor_style(&mut self, style: CursorStyle) { - self.invalidator.debug_assert_paint(); - self.next_frame.cursor_styles.push(CursorStyleRequest { - hitbox_id: None, - style, - }) - } - - /// Sets a tooltip to be rendered for the upcoming frame. This method should only be called - /// during the paint phase of element drawing. - pub fn set_tooltip(&mut self, tooltip: AnyTooltip) -> TooltipId { - self.invalidator.debug_assert_prepaint(); - let id = TooltipId(post_inc(&mut self.next_tooltip_id.0)); - self.next_frame - .tooltip_requests - .push(Some(TooltipRequest { id, tooltip })); - id - } - - /// Invoke the given function with the given content mask after intersecting it - /// with the current mask. This method should only be called during element drawing. - // This function is called in a highly recursive manner in editor - // prepainting, make sure its inlined to reduce the stack burden - #[inline] - pub fn with_content_mask( - &mut self, - mask: Option>, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - self.invalidator.debug_assert_paint_or_prepaint(); - if let Some(mask) = mask { - let mask = mask.into().intersect(&self.content_mask()); - self.content_mask_stack.push(mask); - let result = f(self); - self.content_mask_stack.pop(); - result - } else { - f(self) - } - } - - /// Updates the global element offset relative to the current offset. This is used to implement - /// scrolling. This method should only be called during the prepaint phase of element drawing. - pub fn with_element_offset( - &mut self, - offset: Point, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - self.invalidator.debug_assert_prepaint(); - - if offset.is_zero() { - return f(self); - }; - - let abs_offset = self.element_offset() + offset; - self.with_absolute_element_offset(abs_offset, f) - } - - /// Updates the global element offset based on the given offset. This is used to implement - /// drag handles and other manual painting of elements. This method should only be called during - /// the prepaint phase of element drawing. - pub fn with_absolute_element_offset( - &mut self, - offset: Point, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - self.invalidator.debug_assert_prepaint(); - self.element_offset_stack.push(offset); - let result = f(self); - self.element_offset_stack.pop(); - result - } - - pub(crate) fn with_element_opacity( - &mut self, - opacity: Option, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - self.invalidator.debug_assert_paint_or_prepaint(); - - let Some(opacity) = opacity else { - return f(self); - }; - - let previous_opacity = self.element_opacity; - self.element_opacity = previous_opacity * opacity; - let result = f(self); - self.element_opacity = previous_opacity; - result - } - - /// Perform prepaint on child elements in a "retryable" manner, so that any side effects - /// of prepaints can be discarded before prepainting again. This is used to support autoscroll - /// where we need to prepaint children to detect the autoscroll bounds, then adjust the - /// element offset and prepaint again. See [`crate::List`] for an example. This method should only be - /// called during the prepaint phase of element drawing. - pub fn transact(&mut self, f: impl FnOnce(&mut Self) -> Result) -> Result { - self.invalidator.debug_assert_prepaint(); - let index = self.prepaint_index(); - let result = f(self); - if result.is_err() { - self.next_frame.hitboxes.truncate(index.hitboxes_index); - self.next_frame - .tooltip_requests - .truncate(index.tooltips_index); - self.next_frame - .deferred_draws - .truncate(index.deferred_draws_index); - self.next_frame - .dispatch_tree - .truncate(index.dispatch_tree_index); - self.next_frame - .accessed_element_states - .truncate(index.accessed_element_states_index); - self.text_system.truncate_layouts(index.line_layout_index); - } - result - } - - /// When you call this method during [`Element::prepaint`], containing elements will attempt to - /// scroll to cause the specified bounds to become visible. When they decide to autoscroll, they will call - /// [`Element::prepaint`] again with a new set of bounds. See [`crate::List`] for an example of an element - /// that supports this method being called on the elements it contains. This method should only be - /// called during the prepaint phase of element drawing. - pub fn request_autoscroll(&mut self, bounds: Bounds) { - self.invalidator.debug_assert_prepaint(); - self.requested_autoscroll = Some(bounds); - } - - /// This method can be called from a containing element such as [`crate::List`] to support the autoscroll behavior - /// described in [`Self::request_autoscroll`]. - pub fn take_autoscroll(&mut self) -> Option> { - self.invalidator.debug_assert_prepaint(); - self.requested_autoscroll.take() - } - - /// Asynchronously load an asset, if the asset hasn't finished loading this will return None. - /// Your view will be re-drawn once the asset has finished loading. - /// - /// Note that the multiple calls to this method will only result in one `Asset::load` call at a - /// time. - pub fn use_asset(&mut self, source: &A::Source, cx: &mut App) -> Option { - let (task, is_first) = cx.fetch_asset::(source); - task.clone().now_or_never().or_else(|| { - if is_first { - let entity_id = self.current_view(); - self.spawn(cx, { - let task = task.clone(); - async move |cx| { - task.await; - - cx.on_next_frame(move |_, cx| { - cx.notify(entity_id); - }); - } - }) - .detach(); - } - - None - }) - } - - /// Asynchronously load an asset, if the asset hasn't finished loading or doesn't exist this will return None. - /// Your view will not be re-drawn once the asset has finished loading. - /// - /// Note that the multiple calls to this method will only result in one `Asset::load` call at a - /// time. - pub fn get_asset(&mut self, source: &A::Source, cx: &mut App) -> Option { - let (task, _) = cx.fetch_asset::(source); - task.now_or_never() - } - /// Obtain the current element offset. This method should only be called during the - /// prepaint phase of element drawing. - pub fn element_offset(&self) -> Point { - self.invalidator.debug_assert_prepaint(); - self.element_offset_stack - .last() - .copied() - .unwrap_or_default() - } - - /// Obtain the current element opacity. This method should only be called during the - /// prepaint phase of element drawing. - #[inline] - pub(crate) fn element_opacity(&self) -> f32 { - self.invalidator.debug_assert_paint_or_prepaint(); - self.element_opacity - } - - /// Obtain the current content mask. This method should only be called during element drawing. - pub fn content_mask(&self) -> crate::ClipRegion { - self.invalidator.debug_assert_paint_or_prepaint(); - self.content_mask_stack.last().cloned().unwrap_or_else(|| { - ContentMask { - bounds: Bounds { - origin: Point::default(), - size: self.viewport_size, - }, - ..Default::default() - } - .into() - }) - } - - /// Provide elements in the called function with a new namespace in which their identifiers must be unique. - /// This can be used within a custom element to distinguish multiple sets of child elements. - pub fn with_element_namespace( - &mut self, - element_id: impl Into, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - self.element_id_stack.push(element_id.into()); - let result = f(self); - self.element_id_stack.pop(); - result - } - - /// Use a piece of state that exists as long this element is being rendered in consecutive frames. - pub fn use_keyed_state( - &mut self, - key: impl Into, - cx: &mut App, - init: impl FnOnce(&mut Self, &mut Context) -> S, - ) -> Entity { - let current_view = self.current_view(); - self.with_global_id(key.into(), |global_id, window| { - window.with_element_state(global_id, |state: Option>, window| { - if let Some(state) = state { - (state.clone(), state) - } else { - let new_state = cx.new(|cx| init(window, cx)); - cx.observe(&new_state, move |_, cx| { - cx.notify(current_view); - }) - .detach(); - (new_state.clone(), new_state) - } - }) - }) - } - - /// Use a piece of state that exists as long this element is being rendered in consecutive frames, without needing to specify a key - /// - /// NOTE: This method uses the location of the caller to generate an ID for this state. - /// If this is not sufficient to identify your state (e.g. you're rendering a list item), - /// you can provide a custom ElementID using the `use_keyed_state` method. - #[track_caller] - pub fn use_state( - &mut self, - cx: &mut App, - init: impl FnOnce(&mut Self, &mut Context) -> S, - ) -> Entity { - self.use_keyed_state( - ElementId::CodeLocation(*core::panic::Location::caller()), - cx, - init, - ) - } - - /// Updates or initializes state for an element with the given id that lives across multiple - /// frames. If an element with this ID existed in the rendered frame, its state will be passed - /// to the given closure. The state returned by the closure will be stored so it can be referenced - /// when drawing the next frame. This method should only be called as part of element drawing. - pub fn with_element_state( - &mut self, - global_id: &GlobalElementId, - f: impl FnOnce(Option, &mut Self) -> (R, S), - ) -> R - where - S: 'static, - { - self.invalidator.debug_assert_paint_or_prepaint(); - - let key = (global_id.clone(), TypeId::of::()); - self.next_frame.accessed_element_states.push(key.clone()); - - if let Some(any) = self - .next_frame - .element_states - .remove(&key) - .or_else(|| self.rendered_frame.element_states.remove(&key)) - { - let ElementStateBox { - inner, - #[cfg(debug_assertions)] - type_name, - } = any; - // Using the extra inner option to avoid needing to reallocate a new box. - let mut state_box = inner - .downcast::>() - .map_err(|_| { - #[cfg(debug_assertions)] - { - anyhow::anyhow!( - "invalid element state type for id, requested {:?}, actual: {:?}", - std::any::type_name::(), - type_name - ) - } - - #[cfg(not(debug_assertions))] - { - anyhow::anyhow!( - "invalid element state type for id, requested {:?}", - std::any::type_name::(), - ) - } - }) - .unwrap(); - - let state = state_box.take().expect( - "reentrant call to with_element_state for the same state type and element id", - ); - let (result, state) = f(Some(state), self); - state_box.replace(state); - self.next_frame.element_states.insert( - key, - ElementStateBox { - inner: state_box, - #[cfg(debug_assertions)] - type_name, - }, - ); - result - } else { - let (result, state) = f(None, self); - self.next_frame.element_states.insert( - key, - ElementStateBox { - inner: Box::new(Some(state)), - #[cfg(debug_assertions)] - type_name: std::any::type_name::(), - }, - ); - result - } - } - - /// A variant of `with_element_state` that allows the element's id to be optional. This is a convenience - /// method for elements where the element id may or may not be assigned. Prefer using `with_element_state` - /// when the element is guaranteed to have an id. - /// - /// The first option means 'no ID provided' - /// The second option means 'not yet initialized' - pub fn with_optional_element_state( - &mut self, - global_id: Option<&GlobalElementId>, - f: impl FnOnce(Option>, &mut Self) -> (R, Option), - ) -> R - where - S: 'static, - { - self.invalidator.debug_assert_paint_or_prepaint(); - - if let Some(global_id) = global_id { - self.with_element_state(global_id, |state, cx| { - let (result, state) = f(Some(state), cx); - let state = - state.expect("you must return some state when you pass some element id"); - (result, state) - }) - } else { - let (result, state) = f(None, self); - debug_assert!( - state.is_none(), - "you must not return an element state when passing None for the global id" - ); - result - } - } - - /// Executes the given closure within the context of a tab group. - #[inline] - pub fn with_tab_group(&mut self, index: Option, f: impl FnOnce(&mut Self) -> R) -> R { - if let Some(index) = index { - self.next_frame.tab_stops.begin_group(index); - let result = f(self); - self.next_frame.tab_stops.end_group(); - result - } else { - f(self) - } - } - - /// Defers the drawing of the given element, scheduling it to be painted on top of the currently-drawn tree - /// at a later time. The `priority` parameter determines the drawing order relative to other deferred elements, - /// with higher values being drawn on top. - /// - /// When `content_mask` is provided, the deferred element will be clipped to that region during - /// both prepaint and paint. When `None`, no additional clipping is applied. - /// - /// This method should only be called as part of the prepaint phase of element drawing. - pub fn defer_draw( - &mut self, - element: AnyElement, - absolute_offset: Point, - priority: usize, - content_mask: Option, - ) { - self.invalidator.debug_assert_prepaint(); - let parent_node = self.next_frame.dispatch_tree.active_node_id().unwrap(); - self.next_frame.deferred_draws.push(DeferredDraw { - current_view: self.current_view(), - parent_node, - element_id_stack: self.element_id_stack.clone(), - text_style_stack: self.text_style_stack.clone(), - content_mask, - rem_size: self.rem_size(), - priority, - element: Some(element), - absolute_offset, - prepaint_range: PrepaintStateIndex::default()..PrepaintStateIndex::default(), - paint_range: PaintIndex::default()..PaintIndex::default(), - }); - } - - /// Creates a new painting layer for the specified bounds. A "layer" is a batch - /// of geometry that are non-overlapping and have the same draw order. This is typically used - /// for performance reasons. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn paint_layer(&mut self, bounds: Bounds, f: impl FnOnce(&mut Self) -> R) -> R { - self.invalidator.debug_assert_paint(); - - let content_mask = self.content_mask(); - let clipped_bounds = bounds.intersect(&content_mask.bounds); - if !clipped_bounds.is_empty() { - self.next_frame - .scene - .push_layer(self.cover_bounds(clipped_bounds)); - } - - let result = f(self); - - if !clipped_bounds.is_empty() { - self.next_frame.scene.pop_layer(); - } - - result - } - - /// Paint the drop (non-inset) shadows from `shadows` into the scene at the current - /// z-index. Inset shadows are skipped; paint those with [`Self::paint_inset_shadows`] - /// after the element's background so they layer on top of the fill. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn paint_drop_shadows( - &mut self, - bounds: Bounds, - corner_radii: Corners, - shadows: &[BoxShadow], - ) { - self.invalidator.debug_assert_paint(); - - let scale_factor = self.scale_factor(); - let content_mask = self.snapped_content_mask(); - let opacity = self.element_opacity(); - let element_bounds = self.cover_bounds(bounds); - let element_corner_radii = corner_radii.scale(scale_factor); - for shadow in shadows { - if shadow.inset { - continue; - } - let shadow_bounds = (bounds + shadow.offset).dilate(shadow.spread_radius); - self.next_frame.scene.insert_primitive(Shadow { - order: 0, - blur_radius: shadow.blur_radius.scale(scale_factor), - bounds: self.cover_bounds(shadow_bounds), - content_mask, - corner_radii: corner_radii.scale(scale_factor), - color: shadow.color.opacity(opacity), - element_bounds, - element_corner_radii, - inset: 0, - pad: 0, - }); - } - } - - /// Paint the inset shadows from `shadows` into the scene at the current z-index. Should - /// be called after the element's background so the shadow layers on top of the fill. - /// Drop shadows are skipped; paint those with [`Self::paint_drop_shadows`] before the background. - pub fn paint_inset_shadows( - &mut self, - bounds: Bounds, - corner_radii: Corners, - shadows: &[BoxShadow], - ) { - self.invalidator.debug_assert_paint(); - - let scale_factor = self.scale_factor(); - let content_mask = self.snapped_content_mask(); - let opacity = self.element_opacity(); - let element_bounds = self.cover_bounds(bounds); - let element_corner_radii = corner_radii.scale(scale_factor); - for shadow in shadows { - if !shadow.inset { - continue; - } - let hole = (bounds + shadow.offset).dilate(-shadow.spread_radius); - // Clamp at zero so a large spread can't produce negative radii, which would - // break the SDF in the shader. - let zero = Pixels::ZERO; - let hole_corner_radii = Corners { - top_left: (corner_radii.top_left - shadow.spread_radius).max(zero), - top_right: (corner_radii.top_right - shadow.spread_radius).max(zero), - bottom_right: (corner_radii.bottom_right - shadow.spread_radius).max(zero), - bottom_left: (corner_radii.bottom_left - shadow.spread_radius).max(zero), - }; - self.next_frame.scene.insert_primitive(Shadow { - order: 0, - blur_radius: shadow.blur_radius.scale(scale_factor), - bounds: self.cover_bounds(hole), - content_mask, - corner_radii: hole_corner_radii.scale(scale_factor), - color: shadow.color.opacity(opacity), - element_bounds, - element_corner_radii, - inset: 1, - pad: 0, - }); - } - } - - fn largest_border_interior(quad: &Quad) -> Bounds { - let radii = &quad.corner_radii; - let widths = &quad.border_widths; - let edge_radii = Edges { - top: radii.top_left.max(radii.top_right), - right: radii.top_right.max(radii.bottom_right), - bottom: radii.bottom_left.max(radii.bottom_right), - left: radii.top_left.max(radii.bottom_left), - }; - - let antialias_inset = point(ScaledPixels(1.0), ScaledPixels(1.0)); - let inset_bounds = |top_left_inset, bottom_right_inset| { - Bounds::from_corners( - quad.bounds.origin + top_left_inset + antialias_inset, - quad.bounds.bottom_right() - bottom_right_inset - antialias_inset, - ) - }; - - // Rounded corners need only be excluded on one axis. Either candidate - // is empty of border pixels, so use the larger interior. - let horizontal_band = inset_bounds( - point(widths.left, widths.top.max(edge_radii.top)), - point(widths.right, widths.bottom.max(edge_radii.bottom)), - ); - let vertical_band = inset_bounds( - point(widths.left.max(edge_radii.left), widths.top), - point(widths.right.max(edge_radii.right), widths.bottom), - ); - - let area = |bounds: &Bounds| { - bounds.size.width.0.max(0.) * bounds.size.height.0.max(0.) - }; - if area(&horizontal_band) >= area(&vertical_band) { - horizontal_band - } else { - vertical_band - } - } - - /// Paint one or more quads into the scene for the next frame at the current stacking context. - /// Quads are colored rectangular regions with an optional background, border, and corner radius. - /// see [`fill`], [`outline`], and [`quad`] to construct this type. - /// - /// This method should only be called as part of the paint phase of element drawing. - /// - /// Note that the `quad.corner_radii` are allowed to exceed the bounds, creating sharp corners - /// where the circular arcs meet. This will not display well when combined with dashed borders. - /// Use `Corners::clamp_radii_for_quad_size` if the radii should fit within the bounds. - pub fn paint_quad(&mut self, quad: PaintQuad) { - self.invalidator.debug_assert_paint(); - - let opacity = self.element_opacity(); - let snapped_bounds = self.snap_bounds(quad.bounds); - let snapped_border_widths = self.snap_border_widths(quad.border_widths); - let quad = Quad { - order: 0, - bounds: snapped_bounds, - content_mask: self.snapped_content_mask(), - background: quad.background.opacity(opacity), - border_color: quad.border_color.opacity(opacity), - corner_radii: quad.corner_radii.scale(self.scale_factor()), - border_widths: snapped_border_widths, - border_style: quad.border_style, - }; - - if !quad.background.is_transparent() { - self.next_frame.scene.insert_primitive(quad); - return; - } - - // Splitting a border-only quad around its empty interior avoids shading - // every transparent pixel inside large outlines. - let outer_bounds = quad.bounds; - let inner_bounds = Self::largest_border_interior(&quad); - - if inner_bounds.is_empty() { - self.next_frame.scene.insert_primitive(quad); - return; - } - - let strips = [ - // Top - Bounds::from_corners( - outer_bounds.origin, - point(outer_bounds.right(), inner_bounds.top()), - ), - // Bottom - Bounds::from_corners( - point(outer_bounds.left(), inner_bounds.bottom()), - outer_bounds.bottom_right(), - ), - // Left - Bounds::from_corners( - point(outer_bounds.left(), inner_bounds.top()), - inner_bounds.bottom_left(), - ), - // Right - Bounds::from_corners( - inner_bounds.top_right(), - point(outer_bounds.right(), inner_bounds.bottom()), - ), - ]; - - for strip in strips { - let content_mask_bounds = quad.content_mask.bounds.intersect(&strip); - if !content_mask_bounds.is_empty() { - self.next_frame.scene.insert_primitive(Quad { - content_mask: ContentMask { - bounds: content_mask_bounds, - ..quad.content_mask - }, - ..quad - }); - } - } - } - - /// Paint the given `Path` into the scene for the next frame at the current z-index. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn paint_path(&mut self, mut path: Path, color: impl Into) { - self.invalidator.debug_assert_paint(); - - let scale_factor = self.scale_factor(); - let content_mask = self.snapped_content_mask(); - let opacity = self.element_opacity(); - let color: Background = color.into(); - path.color = color.opacity(opacity); - let mut path = path.scale(scale_factor); - path.content_mask = content_mask; - self.next_frame.scene.insert_primitive(path); - } - - /// Paint an underline into the scene for the next frame at the current z-index. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn paint_underline( - &mut self, - origin: Point, - width: Pixels, - style: &UnderlineStyle, - ) { - self.invalidator.debug_assert_paint(); - - let scale_factor = self.scale_factor(); - let thickness = self.snap_stroke(style.thickness); - let height = if style.wavy { - ScaledPixels(thickness.0 * 3.) - } else { - thickness - }; - let bounds = Bounds { - origin: origin.map(|c| ScaledPixels(round_to_device_pixel(c.0, scale_factor))), - size: size(self.snap_stroke(width), height), - }; - let element_opacity = self.element_opacity(); - - let content_mask = self.snapped_content_mask(); - self.next_frame.scene.insert_primitive(Underline { - order: 0, - pad: 0, - bounds, - content_mask, - color: style.color.unwrap_or_default().opacity(element_opacity), - thickness, - wavy: style.wavy.into(), - }); - } - - /// Paint a strikethrough into the scene for the next frame at the current z-index. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn paint_strikethrough( - &mut self, - origin: Point, - width: Pixels, - style: &StrikethroughStyle, - ) { - self.invalidator.debug_assert_paint(); - - let scale_factor = self.scale_factor(); - let height = style.thickness; - let bounds = Bounds { - origin: origin.map(|c| ScaledPixels(round_to_device_pixel(c.0, scale_factor))), - size: size(self.snap_stroke(width), self.snap_stroke(height)), - }; - let opacity = self.element_opacity(); - - let content_mask = self.snapped_content_mask(); - self.next_frame.scene.insert_primitive(Underline { - order: 0, - pad: 0, - bounds, - content_mask, - thickness: self.snap_stroke(style.thickness), - color: style.color.unwrap_or_default().opacity(opacity), - wavy: false.into(), - }); - } - - /// Paints a monochrome (non-emoji) glyph into the scene for the next frame at the current z-index. - /// - /// The y component of the origin is the baseline of the glyph. - /// You should generally prefer to use the [`ShapedLine::paint`](crate::ShapedLine::paint) or - /// [`WrappedLine::paint`](crate::WrappedLine::paint) methods in the [`TextSystem`](crate::TextSystem). - /// This method is only useful if you need to paint a single glyph that has already been shaped. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn paint_glyph( - &mut self, - origin: Point, - font_id: FontId, - glyph_id: GlyphId, - font_size: Pixels, - color: Hsla, - ) -> Result<()> { - self.invalidator.debug_assert_paint(); - - let element_opacity = self.element_opacity(); - let scale_factor = self.scale_factor(); - let glyph_origin = origin.scale(scale_factor); - - let quantized_origin = Point::new( - round_half_toward_zero(glyph_origin.x.0 * SUBPIXEL_VARIANTS_X as f32) - / SUBPIXEL_VARIANTS_X as f32, - round_half_toward_zero(glyph_origin.y.0 * SUBPIXEL_VARIANTS_Y as f32) - / SUBPIXEL_VARIANTS_Y as f32, - ); - let subpixel_variant = Point::new( - (quantized_origin.x.fract() * SUBPIXEL_VARIANTS_X as f32) as u8, - (quantized_origin.y.fract() * SUBPIXEL_VARIANTS_Y as f32) as u8, - ); - let integer_origin = quantized_origin.map(|c| ScaledPixels(c.trunc())); - let subpixel_rendering = self.should_use_subpixel_rendering(font_id, font_size); - let dilation = self.text_system().glyph_dilation_for_color(color); - let params = RenderGlyphParams { - font_id, - glyph_id, - font_size, - subpixel_variant, - scale_factor, - is_emoji: false, - subpixel_rendering, - dilation, - }; - - let raster_bounds = self.text_system().raster_bounds(¶ms)?; - if !raster_bounds.is_zero() { - let tile = self - .sprite_atlas - .get_or_insert_with(¶ms.clone().into(), &mut || { - let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?; - Ok(Some((size, Cow::Owned(bytes)))) - })? - .expect("Callback above only errors or returns Some"); - let bounds = Bounds { - origin: integer_origin + raster_bounds.origin.map(Into::into), - size: tile.bounds.size.map(Into::into), - }; - let content_mask = self.snapped_content_mask(); - - if subpixel_rendering { - self.next_frame.scene.insert_primitive(SubpixelSprite { - order: 0, - pad: 0, - bounds, - content_mask, - color: color.opacity(element_opacity), - tile, - transformation: TransformationMatrix::unit(), - }); - } else { - self.next_frame.scene.insert_primitive(MonochromeSprite { - order: 0, - pad: 0, - bounds, - content_mask, - color: color.opacity(element_opacity), - tile, - transformation: TransformationMatrix::unit(), - }); - } - } - Ok(()) - } - - fn should_use_subpixel_rendering(&self, font_id: FontId, font_size: Pixels) -> bool { - if self.platform_window.background_appearance() != WindowBackgroundAppearance::Opaque { - return false; - } - - if !self.platform_window.is_subpixel_rendering_supported() { - return false; - } - - let mode = match self.text_rendering_mode.get() { - TextRenderingMode::PlatformDefault => self - .text_system() - .recommended_rendering_mode(font_id, font_size), - mode => mode, - }; - - mode == TextRenderingMode::Subpixel - } - - /// Paints an emoji glyph into the scene for the next frame at the current z-index. - /// - /// The y component of the origin is the baseline of the glyph. - /// You should generally prefer to use the [`ShapedLine::paint`](crate::ShapedLine::paint) or - /// [`WrappedLine::paint`](crate::WrappedLine::paint) methods in the [`TextSystem`](crate::TextSystem). - /// This method is only useful if you need to paint a single emoji that has already been shaped. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn paint_emoji( - &mut self, - origin: Point, - font_id: FontId, - glyph_id: GlyphId, - font_size: Pixels, - ) -> Result<()> { - self.invalidator.debug_assert_paint(); - - let scale_factor = self.scale_factor(); - let glyph_origin = origin.scale(scale_factor); - let integer_origin = glyph_origin.map(|c| ScaledPixels(round_half_toward_zero(c.0))); - let params = RenderGlyphParams { - font_id, - glyph_id, - font_size, - subpixel_variant: Default::default(), - scale_factor, - is_emoji: true, - subpixel_rendering: false, - dilation: 0, - }; - - let raster_bounds = self.text_system().raster_bounds(¶ms)?; - if !raster_bounds.is_zero() { - let tile = self - .sprite_atlas - .get_or_insert_with(¶ms.clone().into(), &mut || { - let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?; - Ok(Some((size, Cow::Owned(bytes)))) - })? - .expect("Callback above only errors or returns Some"); - - let bounds = Bounds { - origin: integer_origin + raster_bounds.origin.map(Into::into), - size: tile.bounds.size.map(Into::into), - }; - let content_mask = self.snapped_content_mask(); - let opacity = self.element_opacity(); - - self.next_frame.scene.insert_primitive(PolychromeSprite { - order: 0, - pad: 0, - grayscale: false.into(), - bounds, - corner_radii: Default::default(), - content_mask, - tile, - opacity, - }); - } - Ok(()) - } - - /// Paint a monochrome SVG into the scene for the next frame at the current stacking context. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn paint_svg( - &mut self, - bounds: Bounds, - path: SharedString, - mut data: Option<&[u8]>, - transformation: TransformationMatrix, - color: Hsla, - cx: &App, - ) -> Result<()> { - self.invalidator.debug_assert_paint(); - - let element_opacity = self.element_opacity(); - let bounds = self.snap_bounds(bounds); - - let params = RenderSvgParams { - path, - size: bounds.size.map(|pixels| { - DevicePixels::from((pixels.0 * SMOOTH_SVG_SCALE_FACTOR).ceil() as i32) - }), - }; - - let Some(tile) = - self.sprite_atlas - .get_or_insert_with(¶ms.clone().into(), &mut || { - let Some((size, bytes)) = cx.svg_renderer.render_alpha_mask(¶ms, data)? - else { - return Ok(None); - }; - Ok(Some((size, Cow::Owned(bytes)))) - })? - else { - return Ok(()); - }; - let content_mask = self.snapped_content_mask(); - let svg_bounds = Bounds { - origin: bounds.center() - - Point::new( - ScaledPixels(tile.bounds.size.width.0 as f32 / SMOOTH_SVG_SCALE_FACTOR / 2.), - ScaledPixels(tile.bounds.size.height.0 as f32 / SMOOTH_SVG_SCALE_FACTOR / 2.), - ), - size: tile - .bounds - .size - .map(|value| ScaledPixels(value.0 as f32 / SMOOTH_SVG_SCALE_FACTOR)), - }; - let final_bounds = svg_bounds - .map_origin(|value| ScaledPixels(round_half_toward_zero(value.0))) - .map_size(|size| size.ceil()); - - self.next_frame.scene.insert_primitive(MonochromeSprite { - order: 0, - pad: 0, - bounds: final_bounds, - content_mask, - color: color.opacity(element_opacity), - tile, - transformation, - }); - - Ok(()) - } - - /// Paint an image into the scene for the next frame at the current z-index. - /// This method will panic if the frame_index is not valid - /// - /// This method should only be called as part of the paint phase of element drawing. - /// Paint an image into `bounds`, positioning and scaling it according to `image_bounds`. - /// - /// The visible region rendered is `bounds.intersect(&image_bounds)`, with `corner_radii` - /// applied to `bounds`. - pub fn paint_image( - &mut self, - bounds: Bounds, - image_bounds: Bounds, - corner_radii: Corners, - data: Arc, - frame_index: usize, - grayscale: bool, - ) -> Result<()> { - self.invalidator.debug_assert_paint(); - - let visible_bounds = bounds.intersect(&image_bounds); - if visible_bounds.size.width <= Pixels::ZERO || visible_bounds.size.height <= Pixels::ZERO { - return Ok(()); - } - if image_bounds.size.width <= Pixels::ZERO || image_bounds.size.height <= Pixels::ZERO { - return Ok(()); - } - - let params = RenderImageParams { - image_id: data.id, - frame_index, - }; - - let tile = self - .sprite_atlas - .get_or_insert_with(¶ms.into(), &mut || { - Ok(Some(( - data.size(frame_index), - Cow::Borrowed( - data.as_bytes(frame_index) - .expect("It's the caller's job to pass a valid frame index"), - ), - ))) - })? - .expect("Callback above only returns Some"); - - let visible_bounds_snapped = self.snap_bounds(visible_bounds); - - let sub_tile = if visible_bounds == image_bounds { - tile - } else { - let x_offset_ratio = - (visible_bounds.origin.x - image_bounds.origin.x) / image_bounds.size.width; - let y_offset_ratio = - (visible_bounds.origin.y - image_bounds.origin.y) / image_bounds.size.height; - let width_ratio = visible_bounds.size.width / image_bounds.size.width; - let height_ratio = visible_bounds.size.height / image_bounds.size.height; - - let tile_origin_x = tile.bounds.origin.x.0; - let tile_origin_y = tile.bounds.origin.y.0; - let tile_width = tile.bounds.size.width.0; - let tile_height = tile.bounds.size.height.0; - - let sub_origin_x = tile_origin_x + (x_offset_ratio * tile_width as f32).round() as i32; - let sub_origin_y = tile_origin_y + (y_offset_ratio * tile_height as f32).round() as i32; - let sub_width = (width_ratio * tile_width as f32).round() as i32; - let sub_height = (height_ratio * tile_height as f32).round() as i32; - - let max_x = tile_origin_x + tile_width; - let max_y = tile_origin_y + tile_height; - - let clamped_origin_x = sub_origin_x.clamp(tile_origin_x, max_x); - let clamped_origin_y = sub_origin_y.clamp(tile_origin_y, max_y); - let clamped_width = sub_width.min(max_x - clamped_origin_x).max(0); - let clamped_height = sub_height.min(max_y - clamped_origin_y).max(0); - - AtlasTile { - bounds: Bounds { - origin: point( - DevicePixels(clamped_origin_x), - DevicePixels(clamped_origin_y), - ), - size: size(DevicePixels(clamped_width), DevicePixels(clamped_height)), - }, - ..tile - } - }; - - let content_mask = self.snapped_content_mask(); - let corner_radii = corner_radii - .clamp_radii_for_quad_size(visible_bounds.size) - .scale(self.scale_factor()); - let opacity = self.element_opacity(); - - self.next_frame.scene.insert_primitive(PolychromeSprite { - order: 0, - pad: 0, - grayscale: grayscale.into(), - bounds: visible_bounds_snapped, - content_mask, - corner_radii, - tile: sub_tile, - opacity, - }); - Ok(()) - } - - /// Paint a surface into the scene for the next frame at the current z-index. - /// - /// This method should only be called as part of the paint phase of element drawing. - #[cfg(target_os = "macos")] - pub fn paint_surface(&mut self, bounds: Bounds, image_buffer: CVPixelBuffer) { - use crate::PaintSurface; - - self.invalidator.debug_assert_paint(); - - let bounds = self.snap_bounds(bounds); - let content_mask = self.snapped_content_mask(); - self.next_frame.scene.insert_primitive(PaintSurface { - order: 0, - bounds, - content_mask, - image_buffer, - }); - } - - /// Removes an image from the sprite atlas. - pub fn drop_image(&mut self, data: Arc) -> Result<()> { - for frame_index in 0..data.frame_count() { - let params = RenderImageParams { - image_id: data.id, - frame_index, - }; - - self.sprite_atlas.remove(¶ms.clone().into()); - } - - Ok(()) - } - - /// Returns whether every frame of an image is present in the sprite atlas. - #[cfg(any(test, feature = "test-support"))] - pub fn has_image_atlas_entry(&self, data: &RenderImage) -> bool { - data.frame_count() > 0 - && (0..data.frame_count()).all(|frame_index| { - self.sprite_atlas.contains( - &RenderImageParams { - image_id: data.id, - frame_index, - } - .into(), - ) - }) - } - - /// Add a node to the layout tree for the current frame. Takes the `Style` of the element for which - /// layout is being requested, along with the layout ids of any children. This method is called during - /// calls to the [`Element::request_layout`] trait method and enables any element to participate in layout. - /// - /// This method should only be called as part of the request_layout or prepaint phase of element drawing. - #[must_use] - pub fn request_layout( - &mut self, - style: Style, - children: impl IntoIterator, - cx: &mut App, - ) -> LayoutId { - self.invalidator.debug_assert_prepaint(); - - cx.layout_id_buffer.clear(); - cx.layout_id_buffer.extend(children); - let rem_size = self.rem_size(); - let scale_factor = self.scale_factor(); - - self.layout_engine.as_mut().unwrap().request_layout( - style, - rem_size, - scale_factor, - &cx.layout_id_buffer, - ) - } - - /// Add a node to the layout tree for the current frame. Instead of taking a `Style` and children, - /// this variant takes a function that is invoked during layout so you can use arbitrary logic to - /// determine the element's size. One place this is used internally is when measuring text. - /// - /// The given closure is invoked at layout time with the known dimensions and available space and - /// returns a `Size`. - /// - /// This method should only be called as part of the request_layout or prepaint phase of element drawing. - pub fn request_measured_layout(&mut self, style: Style, measure: F) -> LayoutId - where - F: Fn(Size>, Size, &mut Window, &mut App) -> Size - + 'static, - { - self.invalidator.debug_assert_prepaint(); - - let rem_size = self.rem_size(); - let scale_factor = self.scale_factor(); - self.layout_engine - .as_mut() - .unwrap() - .request_measured_layout(style, rem_size, scale_factor, measure) - } - - /// Compute the layout for the given id within the given available space. - /// This method is called for its side effect, typically by the framework prior to painting. - /// After calling it, you can request the bounds of the given layout node id or any descendant. - /// - /// This method should only be called as part of the prepaint phase of element drawing. - pub fn compute_layout( - &mut self, - layout_id: LayoutId, - available_space: Size, - cx: &mut App, - ) { - self.invalidator.debug_assert_prepaint(); - - let mut layout_engine = self.layout_engine.take().unwrap(); - layout_engine.compute_layout(layout_id, available_space, self, cx); - self.layout_engine = Some(layout_engine); - } - - /// Obtain the bounds computed for the given LayoutId relative to the window. This method will usually be invoked by - /// GPUI itself automatically in order to pass your element its `Bounds` automatically. - /// - /// This method should only be called as part of element drawing. - pub fn layout_bounds(&mut self, layout_id: LayoutId) -> Bounds { - self.invalidator.debug_assert_prepaint(); - - let scale_factor = self.scale_factor(); - let mut bounds = self - .layout_engine - .as_mut() - .unwrap() - .layout_bounds(layout_id, scale_factor) - .map(Into::into); - let snapped_offset = self.pixel_snap_point(self.element_offset()); - bounds.origin += snapped_offset; - bounds - } - - /// This method should be called during `prepaint`. You can use - /// the returned [Hitbox] during `paint` or in an event handler - /// to determine whether the inserted hitbox was the topmost. - /// - /// This method should only be called as part of the prepaint phase of element drawing. - pub fn insert_hitbox(&mut self, bounds: Bounds, behavior: HitboxBehavior) -> Hitbox { - self.invalidator.debug_assert_prepaint(); - - let content_mask = self.content_mask(); - let mut id = self.next_hitbox_id; - self.next_hitbox_id = self.next_hitbox_id.next(); - let hitbox = Hitbox { - id, - bounds, - content_mask, - behavior, - }; - self.next_frame.hitboxes.push(hitbox.clone()); - hitbox - } - - /// Set a hitbox which will act as a control area of the platform window. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn insert_window_control_hitbox(&mut self, area: WindowControlArea, hitbox: Hitbox) { - self.invalidator.debug_assert_paint(); - self.next_frame.window_control_hitboxes.push((area, hitbox)); - } - - /// Sets the key context for the current element. This context will be used to translate - /// keybindings into actions. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn set_key_context(&mut self, context: KeyContext) { - self.invalidator.debug_assert_paint(); - self.next_frame.dispatch_tree.set_key_context(context); - } - - /// Sets the focus handle for the current element. This handle will be used to manage focus state - /// and keyboard event dispatch for the element. - /// - /// This method should only be called as part of the prepaint phase of element drawing. - pub fn set_focus_handle(&mut self, focus_handle: &FocusHandle, _: &App) { - self.invalidator.debug_assert_prepaint(); - if focus_handle.is_focused(self) { - self.next_frame.focus = Some(focus_handle.id); - } - self.next_frame.dispatch_tree.set_focus_id(focus_handle.id); - } - - /// Sets the view id for the current element, which will be used to manage view caching. - /// - /// This method should only be called as part of element prepaint. We plan on removing this - /// method eventually when we solve some issues that require us to construct editor elements - /// directly instead of always using editors via views. - pub fn set_view_id(&mut self, view_id: EntityId) { - self.invalidator.debug_assert_prepaint(); - self.next_frame.dispatch_tree.set_view_id(view_id); - } - - /// Get the entity ID for the currently rendering view - pub fn current_view(&self) -> EntityId { - self.invalidator.debug_assert_paint_or_prepaint(); - self.rendered_entity_stack.last().copied().unwrap() - } - - #[inline] - pub(crate) fn with_rendered_view( - &mut self, - id: EntityId, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - self.rendered_entity_stack.push(id); - let result = f(self); - self.rendered_entity_stack.pop(); - result - } - - /// Executes the provided function with the specified image cache. - pub fn with_image_cache(&mut self, image_cache: Option, f: F) -> R - where - F: FnOnce(&mut Self) -> R, - { - if let Some(image_cache) = image_cache { - self.image_cache_stack.push(image_cache); - let result = f(self); - self.image_cache_stack.pop(); - result - } else { - f(self) - } - } - - /// Sets an input handler, such as [`ElementInputHandler`][element_input_handler], which interfaces with the - /// platform to receive textual input with proper integration with concerns such - /// as IME interactions. This handler will be active for the upcoming frame until the following frame is - /// rendered. - /// - /// This method should only be called as part of the paint phase of element drawing. - /// - /// [element_input_handler]: crate::ElementInputHandler - pub fn handle_input( - &mut self, - focus_handle: &FocusHandle, - input_handler: impl InputHandler, - cx: &App, - ) { - self.invalidator.debug_assert_paint(); - - if focus_handle.is_focused(self) { - let cx = self.to_async(cx); - self.next_frame - .input_handlers - .push(Some(PlatformInputHandler::new(cx, Box::new(input_handler)))); - } - } - - /// Forwards the focused input handler's [`TextInputConfiguration`] to the - /// platform window when it differs from the last forwarded value. With no - /// input handler the default configuration applies, so a field's - /// preferences don't outlive its focus. - fn apply_text_input_configuration(&mut self, cx: &mut App) { - let configuration = match self.platform_window.take_input_handler() { - Some(mut input_handler) => { - let configuration = input_handler.text_input_configuration(self, cx); - self.platform_window.set_input_handler(input_handler); - configuration - } - None => TextInputConfiguration::default(), - }; - if self.last_text_input_configuration.as_ref() != Some(&configuration) { - self.platform_window - .set_text_input_configuration(configuration.clone()); - self.last_text_input_configuration = Some(configuration); - } - } - - /// Register a mouse event listener on the window for the next frame. The type of event - /// is determined by the first parameter of the given listener. When the next frame is rendered - /// the listener will be cleared. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn on_mouse_event( - &mut self, - mut listener: impl FnMut(&Event, DispatchPhase, &mut Window, &mut App) + 'static, - ) { - self.invalidator.debug_assert_paint(); - - self.next_frame.mouse_listeners.push(Some(Box::new( - move |event: &dyn Any, phase: DispatchPhase, window: &mut Window, cx: &mut App| { - if let Some(event) = event.downcast_ref() { - listener(event, phase, window, cx) - } - }, - ))); - } - - /// Register a key event listener on this node for the next frame. The type of event - /// is determined by the first parameter of the given listener. When the next frame is rendered - /// the listener will be cleared. - /// - /// This is a fairly low-level method, so prefer using event handlers on elements unless you have - /// a specific need to register a listener yourself. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn on_key_event( - &mut self, - listener: impl Fn(&Event, DispatchPhase, &mut Window, &mut App) + 'static, - ) { - self.invalidator.debug_assert_paint(); - - self.next_frame.dispatch_tree.on_key_event(Rc::new( - move |event: &dyn Any, phase, window: &mut Window, cx: &mut App| { - if let Some(event) = event.downcast_ref::() { - listener(event, phase, window, cx) - } - }, - )); - } - - /// Register a modifiers changed event listener on the window for the next frame. - /// - /// This is a fairly low-level method, so prefer using event handlers on elements unless you have - /// a specific need to register a global listener. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn on_modifiers_changed( - &mut self, - listener: impl Fn(&ModifiersChangedEvent, &mut Window, &mut App) + 'static, - ) { - self.invalidator.debug_assert_paint(); - - self.next_frame.dispatch_tree.on_modifiers_changed(Rc::new( - move |event: &ModifiersChangedEvent, window: &mut Window, cx: &mut App| { - listener(event, window, cx) - }, - )); - } - - /// Register a listener to be called when the given focus handle or one of its descendants receives focus. - /// This does not fire if the given focus handle - or one of its descendants - was previously focused. - /// Returns a subscription and persists until the subscription is dropped. - pub fn on_focus_in( - &mut self, - handle: &FocusHandle, - cx: &mut App, - mut listener: impl FnMut(&mut Window, &mut App) + 'static, - ) -> Subscription { - let focus_id = handle.id; - let (subscription, activate) = - self.new_focus_listener(Box::new(move |event, window, cx| { - if event.is_focus_in(focus_id) { - listener(window, cx); - } - true - })); - cx.defer(move |_| activate()); - subscription - } - - /// Register a listener to be called when the given focus handle or one of its descendants loses focus. - /// Returns a subscription and persists until the subscription is dropped. - pub fn on_focus_out( - &mut self, - handle: &FocusHandle, - cx: &mut App, - mut listener: impl FnMut(FocusOutEvent, &mut Window, &mut App) + 'static, - ) -> Subscription { - let focus_id = handle.id; - let (subscription, activate) = - self.new_focus_listener(Box::new(move |event, window, cx| { - if let Some(blurred_id) = event.previous_focus_path.last().copied() - && event.is_focus_out(focus_id) - { - let event = FocusOutEvent { - blurred: WeakFocusHandle { - id: blurred_id, - handles: Arc::downgrade(&cx.focus_handles), - }, - }; - listener(event, window, cx) - } - true - })); - cx.defer(move |_| activate()); - subscription - } - - fn reset_cursor_style(&self, cx: &mut App) { - // Set the cursor only if we're the active window. - if self.is_window_hovered() { - let style = self - .rendered_frame - .cursor_style(self) - .unwrap_or(CursorStyle::Arrow); - cx.platform.set_cursor_style(style); - } - } - - /// Dispatch a given keystroke as though the user had typed it. - /// You can create a keystroke with Keystroke::parse(""). - pub fn dispatch_keystroke(&mut self, keystroke: Keystroke, cx: &mut App) -> bool { - let keystroke = keystroke.with_simulated_ime(); - let result = self.dispatch_event( - PlatformInput::KeyDown(KeyDownEvent { - keystroke: keystroke.clone(), - is_held: false, - prefer_character_input: false, - }), - cx, - ); - if !result.propagate { - return true; - } - - if let Some(input) = keystroke.key_char - && let Some(mut input_handler) = self.platform_window.take_input_handler() - { - input_handler.dispatch_input(&input, self, cx); - self.platform_window.set_input_handler(input_handler); - return true; - } - - false - } - - /// Return a key binding string for an action, to display in the UI. Uses the highest precedence - /// binding for the action (last binding added to the keymap). - pub fn keystroke_text_for(&self, action: &dyn Action) -> String { - self.highest_precedence_binding_for_action(action) - .map(|binding| { - binding - .keystrokes() - .iter() - .map(ToString::to_string) - .collect::>() - .join(" ") - }) - .unwrap_or_else(|| action.name().to_string()) - } - - /// Dispatch a mouse, keyboard, or touch event on the window. - #[profiling::function] - pub fn dispatch_event(&mut self, event: PlatformInput, cx: &mut App) -> DispatchEventResult { - #[cfg(feature = "profiler")] - self.window_profiler.begin_input(event.kind_name()); - let update_count_before = self.invalidator.update_count(); - // Track input modality for focus-visible styling and hover suppression. - // Hover is suppressed during keyboard modality so that keyboard navigation - // doesn't show hover highlights on the item under the mouse cursor. - let old_modality = self.last_input_modality; - self.last_input_modality = match &event { - PlatformInput::KeyDown(_) => InputModality::Keyboard, - PlatformInput::MouseMove(_) | PlatformInput::MouseDown(_) => InputModality::Mouse, - PlatformInput::Touch(_) => InputModality::Touch, - _ => self.last_input_modality, - }; - if self.last_input_modality != old_modality { - self.refresh(); - } - - // Handlers may set this to false by calling `stop_propagation`. - cx.propagate_event = true; - // Handlers may set this to true by calling `prevent_default`. - self.default_prevented = false; - - let event = match event { - // Track the mouse position with our own state, since accessing the platform - // API for the mouse position can only occur on the main thread. - PlatformInput::MouseMove(mouse_move) => { - self.mouse_position = mouse_move.position; - self.modifiers = mouse_move.modifiers; - PlatformInput::MouseMove(mouse_move) - } - PlatformInput::MouseDown(mouse_down) => { - self.mouse_position = mouse_down.position; - self.modifiers = mouse_down.modifiers; - PlatformInput::MouseDown(mouse_down) - } - PlatformInput::MouseUp(mouse_up) => { - self.mouse_position = mouse_up.position; - self.modifiers = mouse_up.modifiers; - PlatformInput::MouseUp(mouse_up) - } - PlatformInput::MousePressure(mouse_pressure) => { - PlatformInput::MousePressure(mouse_pressure) - } - PlatformInput::MouseExited(mouse_exited) => { - self.modifiers = mouse_exited.modifiers; - PlatformInput::MouseExited(mouse_exited) - } - PlatformInput::ModifiersChanged(modifiers_changed) => { - self.modifiers = modifiers_changed.modifiers; - self.capslock = modifiers_changed.capslock; - PlatformInput::ModifiersChanged(modifiers_changed) - } - PlatformInput::ScrollWheel(scroll_wheel) => { - self.mouse_position = scroll_wheel.position; - self.modifiers = scroll_wheel.modifiers; - PlatformInput::ScrollWheel(scroll_wheel) - } - PlatformInput::Pinch(pinch) => { - self.mouse_position = pinch.position; - self.modifiers = pinch.modifiers; - PlatformInput::Pinch(pinch) - } - // Translate dragging and dropping of external files from the operating system - // to internal drag and drop events. - PlatformInput::FileDrop(file_drop) => match file_drop { - FileDropEvent::Entered { position, paths } => { - self.mouse_position = position; - let source_window = self.handle.window_id(); - if !cx.restore_platform_drag(source_window) && cx.active_drag.is_none() { - cx.active_drag = Some(AnyDrag { - value: Arc::new(paths.clone()), - view: cx.new(|_| paths).into(), - cursor_offset: position, - cursor_style: None, - external_payload_source: None, - }); - } - PlatformInput::MouseMove(MouseMoveEvent { - position, - pressed_button: Some(MouseButton::Left), - modifiers: Modifiers::default(), - }) - } - FileDropEvent::Pending { position } => { - self.mouse_position = position; - PlatformInput::MouseMove(MouseMoveEvent { - position, - pressed_button: Some(MouseButton::Left), - modifiers: Modifiers::default(), - }) - } - FileDropEvent::Submit { position } => { - cx.activate(true); - self.mouse_position = position; - PlatformInput::MouseUp(MouseUpEvent { - button: MouseButton::Left, - position, - modifiers: Modifiers::default(), - click_count: 1, - }) - } - FileDropEvent::Exited => { - if !cx.hand_restored_drag_to_platform(self.handle.window_id()) { - cx.active_drag.take(); - } - self.refresh(); - PlatformInput::FileDrop(FileDropEvent::Exited) - } - FileDropEvent::Ended => { - cx.end_platform_drag(self.handle.window_id()); - self.refresh(); - PlatformInput::FileDrop(FileDropEvent::Ended) - } - }, - PlatformInput::Touch(touch) => PlatformInput::Touch(touch), - PlatformInput::LongPress(long_press) => { - self.mouse_position = if long_press.phase == crate::TouchPhase::Started { - long_press.start_position - } else { - long_press.position - }; - if long_press.phase == crate::TouchPhase::Started { - self.long_press_capture = None; - } - PlatformInput::LongPress(long_press) - } - PlatformInput::TouchDrag(touch_drag) => { - self.mouse_position = touch_drag.start_position; - PlatformInput::TouchDrag(touch_drag) - } - PlatformInput::KeyDown(_) | PlatformInput::KeyUp(_) => event, - }; - - if let Some(any_mouse_event) = event.mouse_event() { - self.dispatch_mouse_event(any_mouse_event, cx); - } else if let Some(any_key_event) = event.keyboard_event() { - self.dispatch_key_event(any_key_event, cx); - } else if let Some(touch_event) = event.touch_event() { - self.dispatch_touch_event(touch_event, cx); - } - if let PlatformInput::LongPress(long_press) = &event { - match long_press.phase { - crate::TouchPhase::Started if !self.default_prevented => { - self.long_press_capture = None; - } - crate::TouchPhase::Ended | crate::TouchPhase::Cancelled => { - self.long_press_capture = None; - } - crate::TouchPhase::Started | crate::TouchPhase::Moved => {} - } - } - - // Must run after the move is dispatched: the platform owns the gesture afterwards, so this - // is the last chance for drag listeners to see the pointer leave and reset their state. - self.promote_external_drag_to_platform(&event, cx); - - let caused_invalidation = self.invalidator.update_count() > update_count_before; - if caused_invalidation { - self.input_rate_tracker.borrow_mut().record_input(); - } - #[cfg(feature = "profiler")] - self.window_profiler.end_input(caused_invalidation); - - DispatchEventResult { - propagate: cx.propagate_event, - default_prevented: self.default_prevented, - } - } - - fn promote_external_drag_to_platform(&mut self, event: &PlatformInput, cx: &mut App) { - let PlatformInput::MouseMove(mouse_move) = event else { - return; - }; - if mouse_move.pressed_button != Some(MouseButton::Left) { - return; - } - if Bounds::new(Point::default(), self.viewport_size).contains(&mouse_move.position) { - return; - } - if !self.platform_window.can_start_external_drag() { - return; - } - let Some(payload_source) = cx - .active_drag - .as_mut() - .and_then(|drag| drag.external_payload_source.take()) - else { - return; - }; - let Some(payload) = payload_source(self, cx) else { - return; - }; - if self.platform_window.start_external_drag(&payload) - && cx.hand_active_drag_to_platform(self.handle.window_id()) - { - self.refresh(); - } - } - - /// Whether recognized touch pans may use the platform's predicted touch - /// positions ([`TouchEvent::predicted_position`]) to compensate for input - /// latency. Defaults to true. - pub fn touch_prediction_enabled(&self) -> bool { - self.touch_prediction_enabled - } - - /// Sets whether recognized touch pans may use the platform's predicted - /// touch positions. Disabling drops [`TouchEvent::predicted_position`] - /// before gesture recognition, so pans track only raw touch positions. - pub fn set_touch_prediction_enabled(&mut self, enabled: bool) { - self.touch_prediction_enabled = enabled; - } - - /// Runs the portable gesture recognizer over a raw touch event and - /// dispatches whatever it resolves (scroll steps, synthesized taps) - /// through the ordinary mouse-event path. - fn dispatch_touch_event(&mut self, event: &TouchEvent, cx: &mut App) { - let mut event = event.clone(); - if !self.touch_prediction_enabled { - event.predicted_position = None; - } - let recognized_gestures = self.touch_gestures.handle_event(&event); - if event.phase == crate::TouchPhase::Started - && let Some(touch_drag) = self.touch_gestures.offer_touch_drag(event.id) - { - self.dispatch_recognized_touch_gesture(touch_drag, cx); - } - if event.phase == crate::TouchPhase::Started - && self.touch_gestures.pending_long_press().is_some() - { - self.long_press_capture = None; - } - let mut tapped = false; - for gesture in recognized_gestures { - tapped |= matches!(gesture, RecognizedTouchGesture::Tap { .. }); - self.dispatch_recognized_touch_gesture(gesture, cx); - } - if event.phase == crate::TouchPhase::Started { - self.schedule_long_press_timer(cx); - } else if self.touch_gestures.pending_long_press().is_none() { - self.long_press_timer.take(); - } - // The platform's touch-release handler may inspect the input handler - // as soon as this dispatch returns (the web platform decides virtual - // keyboard visibility there, inside the user gesture). Input handlers - // are registered during draw, so draw now to make them reflect any - // focus change the tap just caused. - if tapped && self.invalidator.is_dirty() { - self.draw(cx).clear(cx); - } - if self.touch_gestures.has_momentum() { - self.schedule_touch_momentum_tick(); - } - } - - fn dispatch_recognized_touch_gesture(&mut self, gesture: RecognizedTouchGesture, cx: &mut App) { - match gesture { - RecognizedTouchGesture::Scroll(scroll_wheel) => { - self.mouse_position = scroll_wheel.position; - cx.propagate_event = true; - self.dispatch_mouse_event(&scroll_wheel, cx); - } - RecognizedTouchGesture::Tap { down, up } => { - self.mouse_position = up.position; - cx.propagate_event = true; - self.dispatch_mouse_event(&down, cx); - cx.propagate_event = true; - self.dispatch_mouse_event(&up, cx); - } - RecognizedTouchGesture::TouchDrag(touch_drag) => { - self.mouse_position = touch_drag.start_position; - cx.propagate_event = true; - self.default_prevented = false; - let started = touch_drag.phase == crate::TouchPhase::Started; - self.dispatch_mouse_event(&touch_drag, cx); - if started { - self.touch_gestures - .resolve_touch_drag(self.default_prevented); - } - } - RecognizedTouchGesture::LongPress(long_press) => { - self.mouse_position = if long_press.phase == crate::TouchPhase::Started { - long_press.start_position - } else { - long_press.position - }; - cx.propagate_event = true; - self.default_prevented = false; - let started = long_press.phase == crate::TouchPhase::Started; - let ended = matches!( - long_press.phase, - crate::TouchPhase::Ended | crate::TouchPhase::Cancelled - ); - self.dispatch_mouse_event(&long_press, cx); - if started { - let claimed = self.default_prevented; - self.touch_gestures.resolve_long_press(claimed); - if !claimed { - self.long_press_capture = None; - } - } - if ended { - self.long_press_capture = None; - } - } - } - } - - fn schedule_long_press_timer(&mut self, cx: &mut App) { - self.long_press_timer.take(); - let Some((touch_id, duration)) = self.touch_gestures.pending_long_press() else { - return; - }; - self.long_press_timer = Some(self.spawn(cx, async move |cx| { - cx.background_executor.timer(duration).await; - cx.update(move |window, cx| { - window.long_press_timer.take(); - if let Some(gesture) = window.touch_gestures.offer_long_press(touch_id) { - window.dispatch_recognized_touch_gesture(gesture, cx); - } - }) - .log_err(); - })); - } - - fn schedule_touch_momentum_tick(&mut self) { - self.on_next_frame(|window, cx| { - if let Some(gesture) = window.touch_gestures.tick_momentum() { - window.dispatch_recognized_touch_gesture(gesture, cx); - } - if window.touch_gestures.has_momentum() { - window.schedule_touch_momentum_tick(); - } - }); - } - - fn dispatch_mouse_event(&mut self, event: &dyn Any, cx: &mut App) { - let hit_test = self.rendered_frame.hit_test(self.mouse_position()); - if hit_test != self.mouse_hit_test { - self.mouse_hit_test = hit_test; - self.reset_cursor_style(cx); - } - - #[cfg(any(feature = "inspector", debug_assertions))] - if self.is_inspector_picking(cx) { - self.handle_inspector_mouse_event(event, cx); - // When inspector is picking, all other mouse handling is skipped. - return; - } - - let mut mouse_listeners = mem::take(&mut self.rendered_frame.mouse_listeners); - - // Capture phase, events bubble from back to front. Handlers for this phase are used for - // special purposes, such as detecting events outside of a given Bounds. - for listener in &mut mouse_listeners { - let listener = listener.as_mut().unwrap(); - listener(event, DispatchPhase::Capture, self, cx); - if !cx.propagate_event { - break; - } - } - - // Bubble phase, where most normal handlers do their work. - if cx.propagate_event { - for listener in mouse_listeners.iter_mut().rev() { - let listener = listener.as_mut().unwrap(); - listener(event, DispatchPhase::Bubble, self, cx); - if !cx.propagate_event { - break; - } - } - } - - self.rendered_frame.mouse_listeners = mouse_listeners; - - if cx.has_active_drag() { - if event.is::() { - // If this was a mouse move event, redraw the window so that the - // active drag can follow the mouse cursor. - self.refresh(); - } else if event.is::() { - // If this was a mouse up event, cancel the active drag and redraw - // the window. - cx.active_drag = None; - self.refresh(); - } - } - - // Auto-release pointer capture on mouse up - if event.is::() && self.captured_hitbox.is_some() { - self.captured_hitbox = None; - } - } - - fn dispatch_key_event(&mut self, event: &dyn Any, cx: &mut App) { - if self.invalidator.is_dirty() { - self.draw(cx).clear(cx); - } - - let node_id = self.focus_node_id_in_rendered_frame(self.focus); - let dispatch_path = self.rendered_frame.dispatch_tree.dispatch_path(node_id); - - let mut keystroke: Option = None; - - if let Some(event) = event.downcast_ref::() { - if event.modifiers.number_of_modifiers() == 0 - && self.pending_modifier.modifiers.number_of_modifiers() == 1 - && !self.pending_modifier.saw_other_input - { - let key = match self.pending_modifier.modifiers { - modifiers if modifiers.shift => Some("shift"), - modifiers if modifiers.control => Some("control"), - modifiers if modifiers.alt => Some("alt"), - modifiers if modifiers.platform => Some("platform"), - modifiers if modifiers.function => Some("function"), - _ => None, - }; - if let Some(key) = key { - keystroke = Some(Keystroke { - key: key.to_string(), - key_char: None, - modifiers: Modifiers::default(), - }); - } - } - - if self.pending_modifier.modifiers.number_of_modifiers() == 0 - && event.modifiers.number_of_modifiers() == 1 - { - self.pending_modifier.saw_other_input = false - } else if event.modifiers.number_of_modifiers() > 1 { - self.pending_modifier.saw_other_input = true - } - self.pending_modifier.modifiers = event.modifiers - } else if let Some(key_down_event) = event.downcast_ref::() { - self.pending_modifier.saw_other_input = true; - keystroke = Some(key_down_event.keystroke.clone()); - if key_down_event.keystroke.key_char.is_some() - && matches!( - cx.cursor_hide_mode, - CursorHideMode::OnTyping | CursorHideMode::OnTypingAndAction - ) - { - cx.platform.hide_cursor_until_mouse_moves(); - } - } - - let Some(keystroke) = keystroke else { - self.finish_dispatch_key_event(event, dispatch_path, self.context_stack(), cx); - return; - }; - - cx.propagate_event = true; - self.dispatch_keystroke_interceptors(event, self.context_stack(), cx); - if !cx.propagate_event { - self.finish_dispatch_key_event(event, dispatch_path, self.context_stack(), cx); - return; - } - - let mut currently_pending = self.pending_input.take().unwrap_or_default(); - if currently_pending.focus.is_some() && currently_pending.focus != self.focus { - currently_pending = PendingInput::default(); - } - - let match_result = self.rendered_frame.dispatch_tree.dispatch_key( - currently_pending.keystrokes, - keystroke, - &dispatch_path, - ); - - if !match_result.to_replay.is_empty() { - self.replay_pending_input(match_result.to_replay, cx); - cx.propagate_event = true; - } - - if !match_result.pending.is_empty() { - let previous_timeout = currently_pending.timeout.take(); - currently_pending.keystrokes = match_result.pending; - currently_pending.focus = self.focus; - - let text_input_requires_timeout = event - .downcast_ref::() - .filter(|key_down| key_down.keystroke.key_char.is_some()) - .and_then(|_| self.platform_window.take_input_handler()) - .map_or(false, |mut input_handler| { - let accepts = input_handler.accepts_text_input(self, cx); - self.platform_window.set_input_handler(input_handler); - accepts - }); - - let needs_timeout = previous_timeout.is_some() - || match_result.pending_has_binding - || text_input_requires_timeout; - currently_pending.timeout = if needs_timeout { - match previous_timeout { - Some(mut timeout) if timeout.is_paused() => { - timeout.reset_duration(PENDING_INPUT_TIMEOUT); - Some(timeout) - } - previous_timeout => { - drop(previous_timeout); - Some(self.new_pending_input_timeout(PENDING_INPUT_TIMEOUT, cx)) - } - } - } else { - None - }; - self.pending_input = Some(currently_pending); - self.pending_input_changed(cx); - cx.propagate_event = false; - return; - } - - let skip_bindings = event - .downcast_ref::() - .filter(|key_down_event| key_down_event.prefer_character_input) - .map(|_| { - self.platform_window - .take_input_handler() - .map_or(false, |mut input_handler| { - let accepts = input_handler.accepts_text_input(self, cx); - self.platform_window.set_input_handler(input_handler); - // If modifiers are not excessive (e.g. AltGr), and the input handler is accepting text input, - // we prefer the text input over bindings. - accepts - }) - }) - .unwrap_or(false); - - if !skip_bindings { - for binding in match_result.bindings { - self.dispatch_action_on_node(node_id, binding.action.as_ref(), cx); - if !cx.propagate_event { - self.dispatch_keystroke_observers( - event, - Some(binding.action), - match_result.context_stack, - cx, - ); - self.pending_input_changed(cx); - return; - } - } - } - - self.finish_dispatch_key_event(event, dispatch_path, match_result.context_stack, cx); - self.pending_input_changed(cx); - } - - fn new_pending_input_timeout(&self, duration: Duration, cx: &App) -> PendingInputTimeout { - let (started_at, task) = self.start_pending_input_timeout(duration, cx); - PendingInputTimeout { - duration, - remaining: duration, - state: PendingInputTimeoutState::Running { started_at, task }, - } - } - - fn start_pending_input_timeout(&self, remaining: Duration, cx: &App) -> (Instant, Task<()>) { - let started_at = cx.background_executor().now(); - let task = self.spawn(cx, async move |cx| { - cx.background_executor.timer(remaining).await; - cx.update(move |window, cx| { - let Some(currently_pending) = window - .pending_input - .take() - .filter(|pending| pending.focus == window.focus) - else { - return; - }; - - let node_id = window.focus_node_id_in_rendered_frame(window.focus); - let dispatch_path = window.rendered_frame.dispatch_tree.dispatch_path(node_id); - - let to_replay = window - .rendered_frame - .dispatch_tree - .flush_dispatch(currently_pending.keystrokes, &dispatch_path); - - window.pending_input_changed(cx); - window.replay_pending_input(to_replay, cx) - }) - .log_err(); - }); - (started_at, task) - } - - fn finish_dispatch_key_event( - &mut self, - event: &dyn Any, - dispatch_path: SmallVec<[DispatchNodeId; 32]>, - context_stack: Vec, - cx: &mut App, - ) { - self.dispatch_key_down_up_event(event, &dispatch_path, cx); - if !cx.propagate_event { - return; - } - - self.dispatch_modifiers_changed_event(event, &dispatch_path, cx); - if !cx.propagate_event { - return; - } - - self.dispatch_keystroke_observers(event, None, context_stack, cx); - } - - pub(crate) fn pending_input_changed(&mut self, cx: &mut App) { - self.pending_input_observers - .clone() - .retain(&(), |callback| callback(self, cx)); - } - - fn defer_pending_input_changed(&self, cx: &mut App) { - // Avoid re-entrant entity updates by deferring observer notifications to the end of the - // current effect cycle, and only for this window. - let window_handle = self.handle; - cx.defer(move |cx| { - window_handle - .update(cx, |_, window, cx| { - window.pending_input_changed(cx); - }) - .ok(); - }); - } - - fn dispatch_key_down_up_event( - &mut self, - event: &dyn Any, - dispatch_path: &SmallVec<[DispatchNodeId; 32]>, - cx: &mut App, - ) { - // Capture phase - for node_id in dispatch_path { - let node = self.rendered_frame.dispatch_tree.node(*node_id); - - for key_listener in node.key_listeners.clone() { - key_listener(event, DispatchPhase::Capture, self, cx); - if !cx.propagate_event { - return; - } - } - } - - // Bubble phase - for node_id in dispatch_path.iter().rev() { - // Handle low level key events - let node = self.rendered_frame.dispatch_tree.node(*node_id); - for key_listener in node.key_listeners.clone() { - key_listener(event, DispatchPhase::Bubble, self, cx); - if !cx.propagate_event { - return; - } - } - } - } - - fn dispatch_modifiers_changed_event( - &mut self, - event: &dyn Any, - dispatch_path: &SmallVec<[DispatchNodeId; 32]>, - cx: &mut App, - ) { - let Some(event) = event.downcast_ref::() else { - return; - }; - for node_id in dispatch_path.iter().rev() { - let node = self.rendered_frame.dispatch_tree.node(*node_id); - for listener in node.modifiers_changed_listeners.clone() { - listener(event, self, cx); - if !cx.propagate_event { - return; - } - } - } - } - - /// Determine whether a potential multi-stroke key binding is in progress on this window. - pub fn has_pending_keystrokes(&self) -> bool { - self.pending_input().is_some() - } - - #[cfg(test)] - pub(crate) fn pending_input_is_none(&self) -> bool { - self.pending_input.is_none() - } - - pub(crate) fn clear_pending_keystrokes(&mut self, cx: &mut App) { - if self.pending_input.take().is_some() { - self.defer_pending_input_changed(cx); - } - } - - /// Returns pending input that can still complete a multi-stroke key binding. Input left over - /// from a previous focus can never complete one. - pub fn pending_input(&self) -> Option> { - self.pending_input - .as_ref() - .filter(|pending_input| pending_input.focus == self.focus) - .map(|pending_input| PendingInputStatus { - keystrokes: pending_input.keystrokes.as_slice(), - timeout: pending_input - .timeout - .as_ref() - .map(PendingInputTimeout::status), - }) - } - - /// Pauses or resumes the current pending input timeout on behalf of `owner`. - /// - /// A paused timeout resumes automatically if `owner` is released. Returns whether the timeout - /// state changed. A timeout paused by one owner cannot be resumed by another. - pub fn set_pending_input_timeout_paused( - &mut self, - owner: &Entity, - paused: bool, - cx: &mut App, - ) -> bool { - let owner_id = owner.entity_id(); - if !paused { - return self.resume_pending_input_timeout(owner_id, cx); - } - - let timeout = self - .pending_input - .as_ref() - .filter(|pending_input| pending_input.focus == self.focus) - .and_then(|pending_input| pending_input.timeout.as_ref()); - let Some(timeout) = timeout else { - return false; - }; - if timeout.is_paused() { - return false; - } - - let release_subscription = self.observe_release(owner, cx, move |_, window, cx| { - window.resume_pending_input_timeout(owner_id, cx); - }); - let now = cx.background_executor().now(); - let changed = self - .pending_input - .as_mut() - .filter(|pending_input| pending_input.focus == self.focus) - .and_then(|pending_input| pending_input.timeout.as_mut()) - .is_some_and(|timeout| { - timeout.pause( - PendingInputTimeoutPause { - owner_id, - _release_subscription: release_subscription, - }, - now, - ) - }); - - if changed { - self.defer_pending_input_changed(cx); - } - changed - } - - fn resume_pending_input_timeout(&mut self, owner_id: EntityId, cx: &mut App) -> bool { - let Some(remaining) = self - .pending_input - .as_ref() - .and_then(|pending_input| pending_input.timeout.as_ref()) - .filter(|timeout| timeout.pause_owner_id() == Some(owner_id)) - .map(|timeout| timeout.remaining) - else { - return false; - }; - - let (started_at, task) = self.start_pending_input_timeout(remaining, cx); - let changed = self - .pending_input - .as_mut() - .and_then(|pending_input| pending_input.timeout.as_mut()) - .is_some_and(|timeout| timeout.resume(owner_id, started_at, task)); - - if changed { - self.defer_pending_input_changed(cx); - } - changed - } - - /// Returns the currently pending input keystrokes that might result in a multi-stroke key binding. - pub fn pending_input_keystrokes(&self) -> Option<&[Keystroke]> { - self.pending_input() - .map(|pending_input| pending_input.keystrokes()) - } - - fn replay_pending_input(&mut self, replays: SmallVec<[Replay; 1]>, cx: &mut App) { - let node_id = self.focus_node_id_in_rendered_frame(self.focus); - let dispatch_path = self.rendered_frame.dispatch_tree.dispatch_path(node_id); - - 'replay: for replay in replays { - let event = KeyDownEvent { - keystroke: replay.keystroke.clone(), - is_held: false, - prefer_character_input: true, - }; - - cx.propagate_event = true; - for binding in replay.bindings { - self.dispatch_action_on_node(node_id, binding.action.as_ref(), cx); - if !cx.propagate_event { - self.dispatch_keystroke_observers( - &event, - Some(binding.action), - Vec::default(), - cx, - ); - continue 'replay; - } - } - - self.dispatch_key_down_up_event(&event, &dispatch_path, cx); - if !cx.propagate_event { - continue 'replay; - } - if let Some(input) = replay.keystroke.key_char.as_ref().cloned() - && let Some(mut input_handler) = self.platform_window.take_input_handler() - { - input_handler.dispatch_input(&input, self, cx); - self.platform_window.set_input_handler(input_handler) - } - } - } - - fn focus_node_id_in_rendered_frame(&self, focus_id: Option) -> DispatchNodeId { - focus_id - .and_then(|focus_id| { - self.rendered_frame - .dispatch_tree - .focusable_node_id(focus_id) - }) - .unwrap_or_else(|| self.rendered_frame.dispatch_tree.root_node_id()) - } - - fn dispatch_action_on_node( - &mut self, - node_id: DispatchNodeId, - action: &dyn Action, - cx: &mut App, - ) { - self.dispatch_action_on_node_inner(node_id, action, cx); - - if !cx.propagate_event - && cx.cursor_hide_mode == CursorHideMode::OnTypingAndAction - && self.last_input_was_keyboard() - { - cx.platform.hide_cursor_until_mouse_moves(); - } - } - - fn dispatch_action_on_node_inner( - &mut self, - node_id: DispatchNodeId, - action: &dyn Action, - cx: &mut App, - ) { - let dispatch_path = self.rendered_frame.dispatch_tree.dispatch_path(node_id); - - // Capture phase for global actions. - cx.propagate_event = true; - if let Some(mut global_listeners) = cx - .global_action_listeners - .remove(&action.as_any().type_id()) - { - for listener in &global_listeners { - #[cfg(feature = "profiler")] - self.window_profiler.begin_action_handler(action, cx); - listener(action.as_any(), DispatchPhase::Capture, cx); - #[cfg(feature = "profiler")] - self.window_profiler.end_action_handler(); - if !cx.propagate_event { - break; - } - } - - global_listeners.extend( - cx.global_action_listeners - .remove(&action.as_any().type_id()) - .unwrap_or_default(), - ); - - cx.global_action_listeners - .insert(action.as_any().type_id(), global_listeners); - } - - if !cx.propagate_event { - return; - } - - // Capture phase for window actions. - for node_id in &dispatch_path { - let node = self.rendered_frame.dispatch_tree.node(*node_id); - for DispatchActionListener { - action_type, - listener, - } in node.action_listeners.clone() - { - let any_action = action.as_any(); - if action_type == any_action.type_id() { - #[cfg(feature = "profiler")] - self.window_profiler.begin_action_handler(action, cx); - listener(any_action, DispatchPhase::Capture, self, cx); - #[cfg(feature = "profiler")] - self.window_profiler.end_action_handler(); - - if !cx.propagate_event { - return; - } - } - } - } - - // Bubble phase for window actions. - for node_id in dispatch_path.iter().rev() { - let node = self.rendered_frame.dispatch_tree.node(*node_id); - for DispatchActionListener { - action_type, - listener, - } in node.action_listeners.clone() - { - let any_action = action.as_any(); - if action_type == any_action.type_id() { - cx.propagate_event = false; // Actions stop propagation by default during the bubble phase - #[cfg(feature = "profiler")] - self.window_profiler.begin_action_handler(action, cx); - listener(any_action, DispatchPhase::Bubble, self, cx); - #[cfg(feature = "profiler")] - self.window_profiler.end_action_handler(); - - if !cx.propagate_event { - return; - } - } - } - } - - // Bubble phase for global actions. - if let Some(mut global_listeners) = cx - .global_action_listeners - .remove(&action.as_any().type_id()) - { - for listener in global_listeners.iter().rev() { - cx.propagate_event = false; // Actions stop propagation by default during the bubble phase - - #[cfg(feature = "profiler")] - self.window_profiler.begin_action_handler(action, cx); - listener(action.as_any(), DispatchPhase::Bubble, cx); - #[cfg(feature = "profiler")] - self.window_profiler.end_action_handler(); - if !cx.propagate_event { - break; - } - } - - global_listeners.extend( - cx.global_action_listeners - .remove(&action.as_any().type_id()) - .unwrap_or_default(), - ); - - cx.global_action_listeners - .insert(action.as_any().type_id(), global_listeners); - } - } - - /// Register the given handler to be invoked whenever the global of the given type - /// is updated. - pub fn observe_global( - &mut self, - cx: &mut App, - f: impl Fn(&mut Window, &mut App) + 'static, - ) -> Subscription { - let window_handle = self.handle; - let (subscription, activate) = cx.global_observers.insert( - TypeId::of::(), - Box::new(move |cx| { - window_handle - .update(cx, |_, window, cx| f(window, cx)) - .is_ok() - }), - ); - cx.defer(move |_| activate()); - subscription - } - - /// Focus the current window and bring it to the foreground at the platform level. - pub fn activate_window(&self) { - self.platform_window.activate(); - } - - /// Requests that the operating system draw attention to this window. - pub fn request_attention(&self) { - self.platform_window.request_attention(); - } - - /// Minimize the current window at the platform level. - pub fn minimize_window(&self) { - self.platform_window.minimize(); - } - - /// Toggle full screen status on the current window at the platform level. - pub fn toggle_fullscreen(&self) { - self.platform_window.toggle_fullscreen(); - } - - /// Toggle simple (borderless) fullscreen, where the window covers the entire - /// screen including the menu bar and, on notched displays, the area around the - /// notch. Unlike [`Window::toggle_fullscreen`], this does not move the window - /// into its own Mission Control space. Only has an effect on macOS. - pub fn toggle_simple_fullscreen(&self) { - self.platform_window.toggle_simple_fullscreen(); - } - - /// Updates the IME panel position suggestions for languages like japanese, chinese. - pub fn invalidate_character_coordinates(&self) { - self.on_next_frame(|window, cx| { - if let Some(mut input_handler) = window.platform_window.take_input_handler() { - if let Some(bounds) = input_handler.selected_bounds(window, cx) { - window.platform_window.update_ime_position(bounds); - } - window.platform_window.set_input_handler(input_handler); - } - }); - } - - /// Present a platform dialog. - /// The provided message will be presented, along with buttons for each answer. - /// When a button is clicked, the returned Receiver will receive the index of the clicked button. - pub fn prompt( - &mut self, - level: PromptLevel, - message: &str, - detail: Option<&str>, - answers: &[T], - cx: &mut App, - ) -> oneshot::Receiver - where - T: Clone + Into, - { - let prompt_builder = cx.prompt_builder.take(); - let Some(prompt_builder) = prompt_builder else { - unreachable!("Re-entrant window prompting is not supported by GPUI"); - }; - - let answers = answers - .iter() - .map(|answer| answer.clone().into()) - .collect::>(); - - let receiver = match &prompt_builder { - PromptBuilder::Default => self - .platform_window - .prompt(level, message, detail, &answers) - .unwrap_or_else(|| { - self.build_custom_prompt(&prompt_builder, level, message, detail, &answers, cx) - }), - PromptBuilder::Custom(_) => { - self.build_custom_prompt(&prompt_builder, level, message, detail, &answers, cx) - } - }; - - cx.prompt_builder = Some(prompt_builder); - - receiver - } - - fn build_custom_prompt( - &mut self, - prompt_builder: &PromptBuilder, - level: PromptLevel, - message: &str, - detail: Option<&str>, - answers: &[PromptButton], - cx: &mut App, - ) -> oneshot::Receiver { - let (sender, receiver) = oneshot::channel(); - let handle = PromptHandle::new(sender); - let handle = (prompt_builder)(level, message, detail, answers, handle, self, cx); - self.prompt = Some(handle); - receiver - } - - /// Returns whether a prompt rendered by GPUI is currently active in this window. - /// - /// This is only true for prompts rendered in the window (see - /// [`App::set_prompt_builder`]), not for platform-native prompt dialogs. - pub fn has_active_prompt(&self) -> bool { - self.prompt.is_some() - } - - /// Returns the current context stack. - pub fn context_stack(&self) -> Vec { - let node_id = self.focus_node_id_in_rendered_frame(self.focus); - let dispatch_tree = &self.rendered_frame.dispatch_tree; - dispatch_tree - .dispatch_path(node_id) - .iter() - .filter_map(move |&node_id| dispatch_tree.node(node_id).context.clone()) - .collect() - } - - /// Returns all available actions for the focused element. - pub fn available_actions(&self, cx: &App) -> Vec> { - let node_id = self.focus_node_id_in_rendered_frame(self.focus); - let mut actions = self.rendered_frame.dispatch_tree.available_actions(node_id); - for action_type in cx.global_action_listeners.keys() { - if let Err(ix) = actions.binary_search_by_key(action_type, |a| a.as_any().type_id()) { - let action = cx.actions.build_action_type(action_type).ok(); - if let Some(action) = action { - actions.insert(ix, action); - } - } - } - actions - } - - /// Returns key bindings that invoke an action on the currently focused element. Bindings are - /// returned in the order they were added. For display, the last binding should take precedence. - pub fn bindings_for_action(&self, action: &dyn Action) -> Vec { - self.rendered_frame - .dispatch_tree - .bindings_for_action(action, &self.rendered_frame.dispatch_tree.context_stack) - } - - /// Returns the highest precedence key binding that invokes an action on the currently focused - /// element. This is more efficient than getting the last result of `bindings_for_action`. - pub fn highest_precedence_binding_for_action(&self, action: &dyn Action) -> Option { - self.rendered_frame - .dispatch_tree - .highest_precedence_binding_for_action( - action, - &self.rendered_frame.dispatch_tree.context_stack, - ) - } - - /// Returns the key bindings for an action in a context. - pub fn bindings_for_action_in_context( - &self, - action: &dyn Action, - context: KeyContext, - ) -> Vec { - let dispatch_tree = &self.rendered_frame.dispatch_tree; - dispatch_tree.bindings_for_action(action, &[context]) - } - - /// Returns the highest precedence key binding for an action in a context. This is more - /// efficient than getting the last result of `bindings_for_action_in_context`. - pub fn highest_precedence_binding_for_action_in_context( - &self, - action: &dyn Action, - context: KeyContext, - ) -> Option { - let dispatch_tree = &self.rendered_frame.dispatch_tree; - dispatch_tree.highest_precedence_binding_for_action(action, &[context]) - } - - /// Returns any bindings that would invoke an action on the given focus handle if it were - /// focused. Bindings are returned in the order they were added. For display, the last binding - /// should take precedence. - pub fn bindings_for_action_in( - &self, - action: &dyn Action, - focus_handle: &FocusHandle, - ) -> Vec { - let dispatch_tree = &self.rendered_frame.dispatch_tree; - let Some(context_stack) = self.context_stack_for_focus_handle(focus_handle) else { - return vec![]; - }; - dispatch_tree.bindings_for_action(action, &context_stack) - } - - /// Returns the highest precedence key binding that would invoke an action on the given focus - /// handle if it were focused. This is more efficient than getting the last result of - /// `bindings_for_action_in`. - pub fn highest_precedence_binding_for_action_in( - &self, - action: &dyn Action, - focus_handle: &FocusHandle, - ) -> Option { - let dispatch_tree = &self.rendered_frame.dispatch_tree; - let context_stack = self.context_stack_for_focus_handle(focus_handle)?; - dispatch_tree.highest_precedence_binding_for_action(action, &context_stack) - } - - /// Find the bindings that can follow the current input sequence for the current context stack. - pub fn possible_bindings_for_input(&self, input: &[Keystroke]) -> Vec { - self.rendered_frame - .dispatch_tree - .possible_next_bindings_for_input(input, &self.context_stack()) - } - - fn context_stack_for_focus_handle( - &self, - focus_handle: &FocusHandle, - ) -> Option> { - let dispatch_tree = &self.rendered_frame.dispatch_tree; - let node_id = dispatch_tree.focusable_node_id(focus_handle.id)?; - let context_stack: Vec<_> = dispatch_tree - .dispatch_path(node_id) - .into_iter() - .filter_map(|node_id| dispatch_tree.node(node_id).context.clone()) - .collect(); - Some(context_stack) - } - - /// Returns a generic event listener that invokes the given listener with the view and context associated with the given view handle. - pub fn listener_for( - &self, - view: &Entity, - f: impl Fn(&mut T, &E, &mut Window, &mut Context) + 'static, - ) -> impl Fn(&E, &mut Window, &mut App) + 'static { - let view = view.downgrade(); - move |e: &E, window: &mut Window, cx: &mut App| { - view.update(cx, |view, cx| f(view, e, window, cx)).ok(); - } - } - - /// Returns a generic handler that invokes the given handler with the view and context associated with the given view handle. - pub fn handler_for) + 'static>( - &self, - entity: &Entity, - f: Callback, - ) -> impl Fn(&mut Window, &mut App) + 'static { - let entity = entity.downgrade(); - move |window: &mut Window, cx: &mut App| { - entity.update(cx, |entity, cx| f(entity, window, cx)).ok(); - } - } - - /// Register a callback that can interrupt the closing of the current window based the returned boolean. - /// If the callback returns false, the window won't be closed. - pub fn on_window_should_close( - &self, - cx: &App, - f: impl Fn(&mut Window, &mut App) -> bool + 'static, - ) { - let mut cx = self.to_async(cx); - self.platform_window.on_should_close(Box::new(move || { - cx.update(|window, cx| f(window, cx)).unwrap_or(true) - })) - } - - /// Register an action listener on this node for the next frame. The type of action - /// is determined by the first parameter of the given listener. When the next frame is rendered - /// the listener will be cleared. - /// - /// This is a fairly low-level method, so prefer using action handlers on elements unless you have - /// a specific need to register a listener yourself. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn on_action( - &mut self, - action_type: TypeId, - listener: impl Fn(&dyn Any, DispatchPhase, &mut Window, &mut App) + 'static, - ) { - self.invalidator.debug_assert_paint(); - - self.next_frame - .dispatch_tree - .on_action(action_type, Rc::new(listener)); - } - - /// Register a capturing action listener on this node for the next frame if the condition is true. - /// The type of action is determined by the first parameter of the given listener. When the next - /// frame is rendered the listener will be cleared. - /// - /// This is a fairly low-level method, so prefer using action handlers on elements unless you have - /// a specific need to register a listener yourself. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn on_action_when( - &mut self, - condition: bool, - action_type: TypeId, - listener: impl Fn(&dyn Any, DispatchPhase, &mut Window, &mut App) + 'static, - ) { - self.invalidator.debug_assert_paint(); - - if condition { - self.next_frame - .dispatch_tree - .on_action(action_type, Rc::new(listener)); - } - } - - /// Read information about the GPU backing this window. - /// Currently returns None on Mac and Windows. - pub fn gpu_specs(&self) -> Option { - self.platform_window.gpu_specs() - } - - /// Perform titlebar double-click action. - /// This is macOS specific. - pub fn titlebar_double_click(&self) { - self.platform_window - .titlebar_double_click(self.is_resizable, self.is_minimizable); - } - - /// Gets the window's title at the platform level. - /// This is macOS specific. - pub fn window_title(&self) -> String { - self.platform_window.get_title() - } - - /// Returns a list of all tabbed windows and their titles. - /// This is macOS specific. - pub fn tabbed_windows(&self) -> Option> { - self.platform_window.tabbed_windows() - } - - /// Returns the tab bar visibility. - /// This is macOS specific. - pub fn tab_bar_visible(&self) -> bool { - self.platform_window.tab_bar_visible() - } - - /// Merges all open windows into a single tabbed window. - /// This is macOS specific. - pub fn merge_all_windows(&self) { - self.platform_window.merge_all_windows() - } - - /// Moves the tab to a new containing window. - /// This is macOS specific. - pub fn move_tab_to_new_window(&self) { - self.platform_window.move_tab_to_new_window() - } - - /// Shows or hides the window tab overview. - /// This is macOS specific. - pub fn toggle_window_tab_overview(&self) { - self.platform_window.toggle_window_tab_overview() - } - - /// Sets the tabbing identifier for the window. - /// This is macOS specific. - pub fn set_tabbing_identifier(&self, tabbing_identifier: Option) { - self.platform_window - .set_tabbing_identifier(tabbing_identifier) - } - - /// Request the OS to play an alert sound. On some platforms this is associated - /// with the window, for others it's just a simple global function call. - pub fn play_system_bell(&self) { - self.platform_window.play_system_bell() - } - - /// Returns whether accessibility features are active for this frame, - /// i.e. whether assistive technology (such as a screen reader) is - /// connected and an accessibility tree is being built. - /// - /// Use this to skip computing data during rendering that is only - /// observable through the accessibility tree. When accessibility is - /// activated, a redraw is forced, so gated work is recomputed before the - /// next tree update is sent to the platform. - /// - /// See the [accessibility guide](crate::_accessibility) for an overview. - pub fn is_a11y_active(&self) -> bool { - self.a11y.is_active() - } - - /// Debug representation of the last frame's accessibility information. - pub fn debug_a11y_tree_json(&self) -> Option { - self.a11y.debug_tree_json() - } - - /// Register a listener for an accessibility action on a specific node. - /// The listener will be called when a screen reader requests the given - /// action on the node identified by `node_id`. - /// - /// See the [accessibility guide](crate::_accessibility) for an overview. - pub fn on_a11y_action( - &mut self, - node_id: accesskit::NodeId, - action: accesskit::Action, - listener: impl FnMut(Option<&accesskit::ActionData>, &mut Window, &mut App) + 'static, - ) { - self.a11y - .action_listeners - .entry(node_id) - .or_default() - .push((action, Box::new(listener))); - } - - #[cfg(not(target_family = "wasm"))] - pub(crate) fn handle_a11y_action(&mut self, request: accesskit::ActionRequest, cx: &mut App) { - // Take listeners out temporarily so the closures can borrow Window - // mutably, then restore them afterward. - if let Some(mut listeners) = self.a11y.action_listeners.remove(&request.target_node) { - let extra_data = request.data.as_ref(); - let mut matched = false; - for (action, listener) in &mut listeners { - if *action == request.action { - listener(extra_data, self, cx); - matched = true; - } - } - self.a11y - .action_listeners - .insert(request.target_node, listeners); - if matched { - return; - } - } - - // Fall back to built-in action handling. - match request.action { - accesskit::Action::Click => { - if let Some(bounds) = self.a11y.node_bounds.get(&request.target_node).copied() { - let center = bounds.center(); - let mouse_down = PlatformInput::MouseDown(crate::MouseDownEvent { - button: MouseButton::Left, - position: center, - modifiers: Modifiers::default(), - click_count: 1, - first_mouse: false, - }); - let mouse_up = PlatformInput::MouseUp(MouseUpEvent { - button: MouseButton::Left, - position: center, - modifiers: Modifiers::default(), - click_count: 1, - }); - self.dispatch_event(mouse_down, cx); - self.dispatch_event(mouse_up, cx); - } - } - accesskit::Action::Focus => { - if let Some(focus_id) = self.a11y.focus_ids.get(&request.target_node).copied() - && let Some(handle) = FocusHandle::for_id(focus_id, &cx.focus_handles) - { - self.focus(&handle, cx); - } - } - accesskit::Action::Blur => { - self.blur(cx); - } - _ => { - log::debug!( - "Unhandled a11y action: {:?} on {:?}", - request.action, - request.target_node - ); - } - } - } - - /// Toggles the inspector mode on this window. - #[cfg(any(feature = "inspector", debug_assertions))] - pub fn toggle_inspector(&mut self, cx: &mut App) { - self.inspector = match self.inspector { - None => Some(cx.new(|_| Inspector::new())), - Some(_) => None, - }; - self.refresh(); - } - - /// Returns true if the window is in inspector mode. - pub fn is_inspector_picking(&self, _cx: &App) -> bool { - #[cfg(any(feature = "inspector", debug_assertions))] - { - if let Some(inspector) = &self.inspector { - return inspector.read(_cx).is_picking(); - } - } - false - } - - /// Executes the provided function with mutable access to an inspector state. - #[cfg(any(feature = "inspector", debug_assertions))] - pub fn with_inspector_state( - &mut self, - _inspector_id: Option<&crate::InspectorElementId>, - cx: &mut App, - f: impl FnOnce(&mut Option, &mut Self) -> R, - ) -> R { - if let Some(inspector_id) = _inspector_id - && let Some(inspector) = &self.inspector - { - let inspector = inspector.clone(); - let active_element_id = inspector.read(cx).active_element_id(); - if Some(inspector_id) == active_element_id { - return inspector.update(cx, |inspector, _cx| { - inspector.with_active_element_state(self, f) - }); - } - } - f(&mut None, self) - } - - #[cfg(any(feature = "inspector", debug_assertions))] - pub(crate) fn build_inspector_element_id( - &mut self, - path: crate::InspectorElementPath, - ) -> crate::InspectorElementId { - self.invalidator.debug_assert_paint_or_prepaint(); - let path = Rc::new(path); - let next_instance_id = self - .next_frame - .next_inspector_instance_ids - .entry(path.clone()) - .or_insert(0); - let instance_id = *next_instance_id; - *next_instance_id += 1; - crate::InspectorElementId { path, instance_id } - } - - #[cfg(any(feature = "inspector", debug_assertions))] - fn prepaint_inspector(&mut self, inspector_width: Pixels, cx: &mut App) -> Option { - if let Some(inspector) = self.inspector.take() { - let mut inspector_element = AnyView::from(inspector.clone()).into_any_element(); - inspector_element.prepaint_as_root( - point(self.viewport_size.width - inspector_width, px(0.0)), - size(inspector_width, self.viewport_size.height).into(), - self, - cx, - ); - self.inspector = Some(inspector); - Some(inspector_element) - } else { - None - } - } - - #[cfg(any(feature = "inspector", debug_assertions))] - fn paint_inspector(&mut self, mut inspector_element: Option, cx: &mut App) { - if let Some(mut inspector_element) = inspector_element { - inspector_element.paint(self, cx); - }; - } - - /// Registers a hitbox that can be used for inspector picking mode, allowing users to select and - /// inspect UI elements by clicking on them. - #[cfg(any(feature = "inspector", debug_assertions))] - pub fn insert_inspector_hitbox( - &mut self, - hitbox_id: HitboxId, - inspector_id: Option<&crate::InspectorElementId>, - cx: &App, - ) { - self.invalidator.debug_assert_paint_or_prepaint(); - if !self.is_inspector_picking(cx) { - return; - } - if let Some(inspector_id) = inspector_id { - self.next_frame - .inspector_hitboxes - .insert(hitbox_id, inspector_id.clone()); - } - } - - #[cfg(any(feature = "inspector", debug_assertions))] - fn paint_inspector_hitbox(&mut self, cx: &App) { - if let Some(inspector) = self.inspector.as_ref() { - let inspector = inspector.read(cx); - if let Some((hitbox_id, _)) = self.hovered_inspector_hitbox(inspector, &self.next_frame) - && let Some(hitbox) = self - .next_frame - .hitboxes - .iter() - .find(|hitbox| hitbox.id == hitbox_id) - { - self.paint_quad(crate::fill(hitbox.bounds, crate::rgba(0x61afef4d))); - } - } - } - - #[cfg(any(feature = "inspector", debug_assertions))] - fn handle_inspector_mouse_event(&mut self, event: &dyn Any, cx: &mut App) { - let Some(inspector) = self.inspector.clone() else { - return; - }; - if event.downcast_ref::().is_some() { - inspector.update(cx, |inspector, _cx| { - if let Some((_, inspector_id)) = - self.hovered_inspector_hitbox(inspector, &self.rendered_frame) - { - inspector.hover(inspector_id, self); - } - }); - } else if event.downcast_ref::().is_some() { - inspector.update(cx, |inspector, _cx| { - if let Some((_, inspector_id)) = - self.hovered_inspector_hitbox(inspector, &self.rendered_frame) - { - inspector.select(inspector_id, self); - } - }); - } else if let Some(event) = event.downcast_ref::() { - // This should be kept in sync with SCROLL_LINES in x11 platform. - const SCROLL_LINES: f32 = 3.0; - const SCROLL_PIXELS_PER_LAYER: f32 = 36.0; - let delta_y = event - .delta - .pixel_delta(px(SCROLL_PIXELS_PER_LAYER / SCROLL_LINES)) - .y; - if let Some(inspector) = self.inspector.clone() { - inspector.update(cx, |inspector, _cx| { - if let Some(depth) = inspector.pick_depth.as_mut() { - *depth += f32::from(delta_y) / SCROLL_PIXELS_PER_LAYER; - let max_depth = self.mouse_hit_test.ids.len() as f32 - 0.5; - if *depth < 0.0 { - *depth = 0.0; - } else if *depth > max_depth { - *depth = max_depth; - } - if let Some((_, inspector_id)) = - self.hovered_inspector_hitbox(inspector, &self.rendered_frame) - { - inspector.set_active_element_id(inspector_id, self); - } - } - }); - } - } - } - - #[cfg(any(feature = "inspector", debug_assertions))] - fn hovered_inspector_hitbox( - &self, - inspector: &Inspector, - frame: &Frame, - ) -> Option<(HitboxId, crate::InspectorElementId)> { - if let Some(pick_depth) = inspector.pick_depth { - let depth = (pick_depth as i64).try_into().unwrap_or(0); - let max_skipped = self.mouse_hit_test.ids.len().saturating_sub(1); - let skip_count = (depth as usize).min(max_skipped); - for hitbox_id in self.mouse_hit_test.ids.iter().skip(skip_count) { - if let Some(inspector_id) = frame.inspector_hitboxes.get(hitbox_id) { - return Some((*hitbox_id, inspector_id.clone())); - } - } - } - None - } - - /// For testing: set the current modifier keys state. - /// This does not generate any events. - #[cfg(any(test, feature = "test-support"))] - pub fn set_modifiers(&mut self, modifiers: Modifiers) { - self.modifiers = modifiers; - } - - /// For testing: simulate a mouse move event to the given position. - /// This dispatches the event through the normal event handling path, - /// which will trigger hover states and tooltips. - #[cfg(any(test, feature = "test-support"))] - pub fn simulate_mouse_move(&mut self, position: Point, cx: &mut App) { - let event = PlatformInput::MouseMove(MouseMoveEvent { - position, - modifiers: self.modifiers, - pressed_button: None, - }); - let _ = self.dispatch_event(event, cx); - } -} - -// #[derive(Clone, Copy, Eq, PartialEq, Hash)] -slotmap::new_key_type! { - /// A unique identifier for a window. - pub struct WindowId; -} - -impl WindowId { - /// Converts this window ID to a `u64`. - pub fn as_u64(&self) -> u64 { - self.0.as_ffi() - } -} - -impl From for WindowId { - fn from(value: u64) -> Self { - WindowId(slotmap::KeyData::from_ffi(value)) - } -} - -/// A handle to a window with a specific root view type. -/// Note that this does not keep the window alive on its own. -#[derive(Deref, DerefMut)] -pub struct WindowHandle { - #[deref] - #[deref_mut] - pub(crate) any_handle: AnyWindowHandle, - state_type: PhantomData V>, -} - -impl Debug for WindowHandle { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("WindowHandle") - .field("any_handle", &self.any_handle.id.as_u64()) - .finish() - } -} - -impl WindowHandle { - /// Creates a new handle from a window ID. - /// This does not check if the root type of the window is `V`. - pub fn new(id: WindowId) -> Self { - WindowHandle { - any_handle: AnyWindowHandle { - id, - state_type: TypeId::of::(), - root_entity_type_name: std::any::type_name::(), - }, - state_type: PhantomData, - } - } - - /// Get the root view out of this window. - /// - /// This will fail if the window is closed or if the root view's type does not match `V`. - #[cfg(any(test, feature = "test-support"))] - pub fn root(&self, cx: &mut C) -> Result> - where - C: AppContext, - { - cx.update_window(self.any_handle, |root_view, _, _| { - root_view - .downcast::() - .map_err(|_| anyhow!("the type of the window's root view has changed")) - })? - } - - /// Updates the root view of this window. - /// - /// This will fail if the window has been closed or if the root view's type does not match - pub fn update( - &self, - cx: &mut C, - update: impl FnOnce(&mut V, &mut Window, &mut Context) -> R, - ) -> Result - where - C: AppContext, - { - cx.update_window(self.any_handle, |root_view, window, cx| { - let view = root_view - .downcast::() - .map_err(|_| anyhow!("the type of the window's root view has changed"))?; - - Ok(view.update(cx, |view, cx| update(view, window, cx))) - })? - } - - /// Read the root view out of this window. - /// - /// This will fail if the window is closed or if the root view's type does not match `V`. - pub fn read<'a>(&self, cx: &'a App) -> Result<&'a V> { - let x = cx - .windows - .get(self.id) - .and_then(|window| { - window - .as_deref() - .and_then(|window| window.root.clone()) - .map(|root_view| root_view.downcast::()) - }) - .context("window not found")? - .map_err(|_| anyhow!("the type of the window's root view has changed"))?; - - Ok(x.read(cx)) - } - - /// Read the root view out of this window, with a callback - /// - /// This will fail if the window is closed or if the root view's type does not match `V`. - pub fn read_with(&self, cx: &C, read_with: impl FnOnce(&V, &App) -> R) -> Result - where - C: AppContext, - { - cx.read_window(self, |root_view, cx| read_with(root_view.read(cx), cx)) - } - - /// Read the root view pointer off of this window. - /// - /// This will fail if the window is closed or if the root view's type does not match `V`. - pub fn entity(&self, cx: &C) -> Result> - where - C: AppContext, - { - cx.read_window(self, |root_view, _cx| root_view) - } - - /// Check if this window is 'active'. - /// - /// Will return `None` if the window is closed or currently - /// borrowed. - pub fn is_active(&self, cx: &mut App) -> Option { - cx.update_window(self.any_handle, |_, window, _| window.is_window_active()) - .ok() - } -} - -impl Copy for WindowHandle {} - -impl Clone for WindowHandle { - fn clone(&self) -> Self { - *self - } -} - -impl PartialEq for WindowHandle { - fn eq(&self, other: &Self) -> bool { - self.any_handle == other.any_handle - } -} - -impl Eq for WindowHandle {} - -impl Hash for WindowHandle { - fn hash(&self, state: &mut H) { - self.any_handle.hash(state); - } -} - -impl From> for AnyWindowHandle { - fn from(val: WindowHandle) -> Self { - val.any_handle - } -} - -/// A handle to a window with any root view type, which can be downcast to a window with a specific root view type. -#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct AnyWindowHandle { - pub(crate) id: WindowId, - state_type: TypeId, - root_entity_type_name: &'static str, -} - -impl AnyWindowHandle { - /// Get the ID of this window. - pub fn window_id(&self) -> WindowId { - self.id - } - - /// Returns the name of the window's declared root entity type. - pub fn root_entity_type_name(&self) -> &'static str { - self.root_entity_type_name - } - - /// Attempt to convert this handle to a window handle with a specific root view type. - /// If the types do not match, this will return `None`. - pub fn downcast(&self) -> Option> { - if TypeId::of::() == self.state_type { - Some(WindowHandle { - any_handle: *self, - state_type: PhantomData, - }) - } else { - None - } - } - - /// Updates the state of the root view of this window. - /// - /// This will fail if the window has been closed. - pub fn update( - self, - cx: &mut C, - update: impl FnOnce(AnyView, &mut Window, &mut App) -> R, - ) -> Result - where - C: AppContext, - { - cx.update_window(self, update) - } - - /// Read the state of the root view of this window. - /// - /// This will fail if the window has been closed. - pub fn read(self, cx: &C, read: impl FnOnce(Entity, &App) -> R) -> Result - where - C: AppContext, - T: 'static, - { - let view = self - .downcast::() - .context("the type of the window's root view has changed")?; - - cx.read_window(&view, read) - } -} - -impl HasWindowHandle for Window { - fn window_handle(&self) -> Result, HandleError> { - self.platform_window.window_handle() - } -} - -impl HasDisplayHandle for Window { - fn display_handle( - &self, - ) -> std::result::Result, HandleError> { - self.platform_window.display_handle() - } -} - -/// An identifier for an [`Element`]. -/// -/// Can be constructed with a string, a number, or both, as well -/// as other internal representations. -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub enum ElementId { - /// The ID of a View element - View(EntityId), - /// An integer ID. - Integer(u64), - /// A string based ID. - Name(SharedString), - /// A UUID. - Uuid(Uuid), - /// An ID that's equated with a focus handle. - FocusHandle(FocusId), - /// A combination of a name and an integer. - NamedInteger(SharedString, u64), - /// A path. - Path(Arc), - /// A code location. - CodeLocation(core::panic::Location<'static>), - /// A labeled child of an element. - NamedChild(Arc, SharedString), - /// A byte array ID (used for text-anchors) - OpaqueId([u8; 20]), -} - -impl ElementId { - /// Constructs an `ElementId::NamedInteger` from a name and `usize`. - pub fn named_usize(name: impl Into, integer: usize) -> ElementId { - Self::NamedInteger(name.into(), integer as u64) - } -} - -impl Display for ElementId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ElementId::View(entity_id) => write!(f, "view-{}", entity_id)?, - ElementId::Integer(ix) => write!(f, "{}", ix)?, - ElementId::Name(name) => write!(f, "{}", name)?, - ElementId::FocusHandle(_) => write!(f, "FocusHandle")?, - ElementId::NamedInteger(s, i) => write!(f, "{}-{}", s, i)?, - ElementId::Uuid(uuid) => write!(f, "{}", uuid)?, - ElementId::Path(path) => write!(f, "{}", path.display())?, - ElementId::CodeLocation(location) => write!(f, "{}", location)?, - ElementId::NamedChild(id, name) => write!(f, "{}-{}", id, name)?, - ElementId::OpaqueId(opaque_id) => write!(f, "{:x?}", opaque_id)?, - } - - Ok(()) - } -} - -impl TryInto for ElementId { - type Error = anyhow::Error; - - fn try_into(self) -> anyhow::Result { - if let ElementId::Name(name) = self { - Ok(name) - } else { - anyhow::bail!("element id is not string") - } - } -} - -impl From for ElementId { - fn from(id: usize) -> Self { - ElementId::Integer(id as u64) - } -} - -impl From for ElementId { - fn from(id: i32) -> Self { - Self::Integer(id as u64) - } -} - -impl From for ElementId { - fn from(name: SharedString) -> Self { - ElementId::Name(name) - } -} - -impl From for ElementId { - fn from(name: String) -> Self { - ElementId::Name(name.into()) - } -} - -impl From> for ElementId { - fn from(name: Arc) -> Self { - ElementId::Name(name.into()) - } -} - -impl From> for ElementId { - fn from(path: Arc) -> Self { - ElementId::Path(path) - } -} - -impl From<&'static str> for ElementId { - fn from(name: &'static str) -> Self { - ElementId::Name(SharedString::new_static(name)) - } -} - -impl<'a> From<&'a FocusHandle> for ElementId { - fn from(handle: &'a FocusHandle) -> Self { - ElementId::FocusHandle(handle.id) - } -} - -impl From<(&'static str, EntityId)> for ElementId { - fn from((name, id): (&'static str, EntityId)) -> Self { - ElementId::NamedInteger(SharedString::new_static(name), id.as_u64()) - } -} - -impl From<(&'static str, usize)> for ElementId { - fn from((name, id): (&'static str, usize)) -> Self { - ElementId::NamedInteger(SharedString::new_static(name), id as u64) - } -} - -impl From<(SharedString, usize)> for ElementId { - fn from((name, id): (SharedString, usize)) -> Self { - ElementId::NamedInteger(name, id as u64) - } -} - -impl From<(&'static str, u64)> for ElementId { - fn from((name, id): (&'static str, u64)) -> Self { - ElementId::NamedInteger(SharedString::new_static(name), id) - } -} - -impl From for ElementId { - fn from(value: Uuid) -> Self { - Self::Uuid(value) - } -} - -impl From<(&'static str, u32)> for ElementId { - fn from((name, id): (&'static str, u32)) -> Self { - ElementId::NamedInteger(SharedString::new_static(name), u64::from(id)) - } -} - -impl> From<(ElementId, T)> for ElementId { - fn from((id, name): (ElementId, T)) -> Self { - ElementId::NamedChild(Arc::new(id), name.into()) - } -} - -impl From<&'static core::panic::Location<'static>> for ElementId { - fn from(location: &'static core::panic::Location<'static>) -> Self { - ElementId::CodeLocation(*location) - } -} - -impl From<[u8; 20]> for ElementId { - fn from(opaque_id: [u8; 20]) -> Self { - ElementId::OpaqueId(opaque_id) - } -} - -/// A rectangle to be rendered in the window at the given position and size. -/// Passed as an argument [`Window::paint_quad`]. -#[derive(Clone)] -pub struct PaintQuad { - /// The bounds of the quad within the window. - pub bounds: Bounds, - /// The radii of the quad's corners. - pub corner_radii: Corners, - /// The background color of the quad. - pub background: Background, - /// The widths of the quad's borders. - pub border_widths: Edges, - /// The color of the quad's borders. - pub border_color: Hsla, - /// The style of the quad's borders. - pub border_style: BorderStyle, -} - -impl PaintQuad { - /// Sets the corner radii of the quad. - pub fn corner_radii(self, corner_radii: impl Into>) -> Self { - PaintQuad { - corner_radii: corner_radii.into(), - ..self - } - } - - /// Sets the border widths of the quad. - pub fn border_widths(self, border_widths: impl Into>) -> Self { - PaintQuad { - border_widths: border_widths.into(), - ..self - } - } - - /// Sets the border color of the quad. - pub fn border_color(self, border_color: impl Into) -> Self { - PaintQuad { - border_color: border_color.into(), - ..self - } - } - - /// Sets the background color of the quad. - pub fn background(self, background: impl Into) -> Self { - PaintQuad { - background: background.into(), - ..self - } - } -} - -/// Creates a quad with the given parameters. -pub fn quad( - bounds: Bounds, - corner_radii: impl Into>, - background: impl Into, - border_widths: impl Into>, - border_color: impl Into, - border_style: BorderStyle, -) -> PaintQuad { - PaintQuad { - bounds, - corner_radii: corner_radii.into(), - background: background.into(), - border_widths: border_widths.into(), - border_color: border_color.into(), - border_style, - } -} - -/// Creates a filled quad with the given bounds and background color. -pub fn fill(bounds: impl Into>, background: impl Into) -> PaintQuad { - PaintQuad { - bounds: bounds.into(), - corner_radii: (0.).into(), - background: background.into(), - border_widths: (0.).into(), - border_color: transparent_black(), - border_style: BorderStyle::default(), - } -} - -/// Creates a rectangle outline with the given bounds, border color, and a 1px border width -pub fn outline( - bounds: impl Into>, - border_color: impl Into, - border_style: BorderStyle, -) -> PaintQuad { - PaintQuad { - bounds: bounds.into(), - corner_radii: (0.).into(), - background: transparent_black().into(), - border_widths: (1.).into(), - border_color: border_color.into(), - border_style, - } -} - -#[cfg(test)] -mod tests { - use std::{ - cell::{Cell, RefCell}, - path::PathBuf, - rc::Rc, - time::Duration, - }; - - use crate::{ - canvas, div, point, px, size, AnyWindowHandle, AppContext as _, Bounds, ContentMask, - Context, Corners, DispatchPhase, DragMoveEvent, Empty, ExternalDragPayload, ExternalPaths, - FileDragPaths, FileDropEvent, FocusHandle, InputEvent as _, InteractiveElement as _, - IntoElement, LongPressEvent, MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, - Pixels, Point, Render, RequestFrameOptions, StatefulInteractiveElement as _, Styled, - TestAppContext, TouchDragEvent, TouchEvent, TouchId, TouchPhase, Window, WindowAppearance, - WindowOptions, - }; - - struct EmptyView; - - #[test] - fn rounded_content_mask_intersection_keeps_the_original_curve() { - let outer = ContentMask { - bounds: Bounds::from_corners(point(px(0.), px(0.)), point(px(100.), px(100.))), - corner_radii: Corners::all(px(16.)), - ..Default::default() - }; - let inset = ContentMask { - bounds: Bounds::from_corners(point(px(4.), px(4.)), point(px(96.), px(96.))), - ..Default::default() - }; - - let intersection = outer.intersect(&inset); - assert_eq!(intersection.bounds.origin, point(px(4.), px(4.))); - assert_eq!(intersection.rounded_clips[0].bounds, outer.bounds); - assert!(intersection.contains(point(px(4.5), px(8.5)))); - } - - impl Render for EmptyView { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - } - } - - struct OpensWindowOnPaint { - opened: Rc>, - } - - impl Render for OpensWindowOnPaint { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let opened = self.opened.clone(); - div() - .size_full() - .child(canvas( - |_, _, _| {}, - move |_, _, _window, cx| { - if !opened.replace(true) { - cx.open_window(WindowOptions::default(), |_, cx| cx.new(|_| EmptyView)) - .unwrap(); - } - }, - )) - // Siblings painted after the canvas: their elements were - // allocated in the arena before the nested draw, so they detect - // a mid-draw arena clear when painted afterwards. - .child(div().child("after")) - } - } - - /// Opening a window synchronously draws it and requests an element arena - /// clear. When that happens from within another window's draw (here: from - /// an element's paint), the clear must be deferred until the outer draw - /// finishes, or the outer draw's arena-allocated elements would be freed - /// out from under it. - #[test] - fn test_window_opened_during_draw_defers_arena_clear() { - let mut cx = TestAppContext::single(); - - let opened = Rc::new(Cell::new(false)); - // add_window draws once, which runs the nested open_window mid-draw. - let window = cx.add_window({ - let opened = opened.clone(); - move |_, _| OpensWindowOnPaint { opened } - }); - - assert!(opened.get()); - assert_eq!(cx.windows().len(), 2); - - // The deferred clear must actually run once the outer draw unwinds: - // subsequent draws of both windows work against a fresh arena. - cx.update_window(window.into(), |_, window, cx| window.draw(cx).clear(cx)) - .unwrap(); - } - - /// Platforms that stop requesting frames for idle windows (currently web) - /// rely on the frame waker firing whenever frame demand arises; a demand - /// source that skips the waker shows up there as a window that silently - /// stops repainting until unrelated activity wakes it. - #[gpui::test] - fn test_frame_waker_fires_on_frame_demand(cx: &mut TestAppContext) { - let window = cx.add_window(|_, _| EmptyView); - let test_window = cx.test_window(window.into()); - - // Windows start dirty, and that can predate waker installation; - // installing the waker must deliver the pending wake or the first - // frame would never be requested. - assert!( - test_window.frame_wake_count() >= 1, - "opening a window must wake the frame source for the initial frame" - ); - - // Serve outstanding demand (present the frame drawn by `add_window`). - test_window.simulate_frame_request(RequestFrameOptions::default()); - - // An idle window must not wake on clean frames or plain updates, or - // the frame source could never stop. - let baseline = test_window.frame_wake_count(); - test_window.simulate_frame_request(RequestFrameOptions::default()); - window.update(cx, |_, _, _| {}).unwrap(); - assert_eq!( - test_window.frame_wake_count(), - baseline, - "clean frames and non-notifying updates must not wake the frame source" - ); - - // Notifying a view in an idle window is the core demand signal. - window.update(cx, |_, _, cx| cx.notify()).unwrap(); - assert!( - test_window.frame_wake_count() > baseline, - "notifying a view in an idle window must wake the frame source" - ); - - // Serving that demand returns to idle without further wakes. - test_window.simulate_frame_request(RequestFrameOptions::default()); - let baseline = test_window.frame_wake_count(); - test_window.simulate_frame_request(RequestFrameOptions::default()); - assert_eq!( - test_window.frame_wake_count(), - baseline, - "serving demand must return the window to idle" - ); - - // Next-frame callbacks create demand without dirtying the window. - window - .update(cx, |_, window, _| window.on_next_frame(|_, _| {})) - .unwrap(); - assert!( - test_window.frame_wake_count() > baseline, - "scheduling a next-frame callback in an idle window must wake the frame source" - ); - } - - /// A frame request that arrives while next-frame callbacks are pending - /// must never strand them: either the frame runs them, or (when the - /// inactive-window frame-rate throttle defers the frame) the waker fires - /// so another request is delivered. - #[gpui::test] - fn test_pending_next_frame_callbacks_are_not_stranded(cx: &mut TestAppContext) { - let window = cx.add_window(|_, _| EmptyView); - let test_window = cx.test_window(window.into()); - // Establish a recent last-frame time so the inactive-window throttle - // can engage on the next request. - test_window.simulate_frame_request(RequestFrameOptions::default()); - - let callback_ran = Rc::new(Cell::new(false)); - window - .update(cx, { - let callback_ran = callback_ran.clone(); - move |_, window, _| { - window.on_next_frame(move |_, _| callback_ran.set(true)); - } - }) - .unwrap(); - - let baseline = test_window.frame_wake_count(); - test_window.simulate_frame_request(RequestFrameOptions::default()); - // The test window is inactive, so this request throttles to ~30fps - // when it lands within the throttle interval of the previous frame - // (the common case here, but timing-dependent): the callback is - // deferred and the waker must re-arm the frame source. On a slow run - // the request instead lands outside the interval and runs the - // callback directly. - assert!( - test_window.frame_wake_count() > baseline || callback_ran.get(), - "a frame request with pending next-frame callbacks must either run them or re-arm the frame source" - ); - } - - #[gpui::test] - fn test_window_reports_no_raw_handle_instead_of_panicking(cx: &mut TestAppContext) { - use raw_window_handle::{HandleError, HasDisplayHandle as _, HasWindowHandle as _}; - - let window = cx.add_window(|_, _| EmptyView); - window - .update(cx, |_, window, _| { - assert!(matches!( - window.window_handle(), - Err(HandleError::NotSupported) - )); - assert!(matches!( - window.display_handle(), - Err(HandleError::NotSupported) - )); - }) - .unwrap(); - } - - #[gpui::test] - fn test_appearance_change_runs_after_app_update(cx: &mut TestAppContext) { - let window = cx.add_window(|_, _| EmptyView); - let observed_appearance = Rc::new(Cell::new(None)); - let _subscription = window - .update(cx, { - let observed_appearance = observed_appearance.clone(); - move |_, window, _| { - window.observe_window_appearance(move |window, _| { - observed_appearance.set(Some(window.appearance())); - }) - } - }) - .unwrap(); - let test_window = cx.test_window(window.into()); - - cx.update(|_| { - test_window.simulate_appearance_change(WindowAppearance::Dark); - assert_eq!(observed_appearance.get(), None); - }); - cx.run_until_parked(); - - assert_eq!(observed_appearance.get(), Some(WindowAppearance::Dark)); - } - - #[gpui::test] - fn queued_frame_callback_wakes_a_parked_render_loop(cx: &mut TestAppContext) { - let window = cx.add_window(|_, _| Empty); - let test_window = cx.test_window(window.into()); - - assert!(test_window.simulate_scheduled_frame()); - assert!(test_window.simulate_scheduled_frame()); - assert!(!test_window.frame_scheduled()); - - cx.update_window(window.into(), |_, window, _| { - window.active.set(true); - window.on_next_frame(|_, _| {}); - }) - .unwrap(); - assert!( - test_window.frame_scheduled(), - "queuing work on a parked window must wake the render loop" - ); - - assert!(test_window.simulate_scheduled_frame()); - assert!( - test_window.frame_scheduled(), - "presenting the frame must await one compositor callback" - ); - assert!(test_window.simulate_scheduled_frame()); - assert!(!test_window.frame_scheduled()); - } - - #[gpui::test] - fn pending_presentation_wakes_a_parked_render_loop(cx: &mut TestAppContext) { - let window = cx.add_window(|_, _| Empty); - let test_window = cx.test_window(window.into()); - - assert!(test_window.simulate_scheduled_frame()); - assert!(test_window.simulate_scheduled_frame()); - assert!(!test_window.frame_scheduled()); - - cx.update_window(window.into(), |_, window, cx| window.draw(cx).clear(cx)) - .unwrap(); - - assert!( - test_window.frame_scheduled(), - "a rendered scene awaiting presentation must wake the render loop" - ); - } - - #[gpui::test] - fn callback_queued_during_a_frame_requests_a_follow_up(cx: &mut TestAppContext) { - let window = cx.add_window(|_, _| Empty); - let test_window = cx.test_window(window.into()); - - let callback_ran = Rc::new(Cell::new(false)); - cx.update_window(window.into(), |_, window, _| { - // Inactive windows are frame-rate throttled, which would defer the - // ticks this test drives manually. - window.active.set(true); - let callback_ran = callback_ran.clone(); - window.on_next_frame(move |window, _| { - window.on_next_frame(move |_, _| callback_ran.set(true)); - }); - }) - .unwrap(); - - assert!(test_window.simulate_scheduled_frame()); - assert!(!callback_ran.get()); - assert!( - test_window.frame_scheduled(), - "a callback queued mid-frame must schedule a follow-up before the loop parks" - ); - - assert!(test_window.simulate_scheduled_frame()); - assert!(callback_ran.get()); - } - - struct RootView { - explicit_size: bool, - child_bounds: Rc>>, - } - - impl Render for RootView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - let child_bounds = self.child_bounds.clone(); - let root = div().flex().flex_col().child( - canvas( - move |bounds, _, _| child_bounds.set(bounds), - |_, _, _, _| {}, - ) - .size_full(), - ); - if self.explicit_size { - root.w(px(300.)).h(px(200.)) - } else { - root - } - } - } - - #[test] - fn auto_sized_window_root_fills_the_window() { - let mut cx = TestAppContext::single(); - let child_bounds = Rc::new(Cell::new(Bounds::default())); - let window = cx.add_window({ - let child_bounds = child_bounds.clone(); - move |_, _| RootView { - explicit_size: false, - child_bounds, - } - }); - - let viewport_size = cx - .update_window(window.into(), |_, window, cx| { - window.draw(cx).clear(cx); - window.viewport_size() - }) - .unwrap(); - - assert_eq!(child_bounds.get().size, viewport_size); - } - - #[test] - fn explicitly_sized_window_root_keeps_its_size() { - let mut cx = TestAppContext::single(); - let child_bounds = Rc::new(Cell::new(Bounds::default())); - let window = cx.add_window({ - let child_bounds = child_bounds.clone(); - move |_, _| RootView { - explicit_size: true, - child_bounds, - } - }); - - cx.update_window(window.into(), |_, window, cx| { - window.draw(cx).clear(cx); - }) - .unwrap(); - - assert_eq!(child_bounds.get().size, size(px(300.), px(200.))); - } - - struct FileDragView { - path: PathBuf, - observed_drag_moves: Rc>>>, - observed_drops: Rc>>, - } - - impl Render for FileDragView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - div() - .id("file-drag") - .size_full() - .on_drag(self.path.clone(), |_, _, _, cx| cx.new(|_| Empty)) - .external_drag_payload(|path: &PathBuf, _, _| { - Some(ExternalDragPayload::Files(FileDragPaths::new([( - path.clone(), - true, - )]))) - }) - .on_drag_move({ - let observed_drag_moves = self.observed_drag_moves.clone(); - move |event: &DragMoveEvent, _, _| { - observed_drag_moves.borrow_mut().push(event.event.position); - } - }) - .on_drop({ - let observed_drops = self.observed_drops.clone(); - move |path: &PathBuf, _, _| observed_drops.borrow_mut().push(path.clone()) - }) - } - } - - #[gpui::test] - fn file_drag_is_promoted_once_and_restored_in_source_window(cx: &mut TestAppContext) { - struct Drag { - window: AnyWindowHandle, - observed_drag_moves: Rc>>>, - observed_drops: Rc>>, - } - - fn start_drag(cx: &mut TestAppContext, path: PathBuf, platform_result: bool) -> Drag { - let observed_drag_moves = Rc::new(RefCell::new(Vec::new())); - let observed_drops = Rc::new(RefCell::new(Vec::new())); - let window: AnyWindowHandle = cx - .add_window({ - let observed_drag_moves = observed_drag_moves.clone(); - let observed_drops = observed_drops.clone(); - move |_, _| FileDragView { - path, - observed_drag_moves, - observed_drops, - } - }) - .into(); - cx.test_window(window) - .set_start_external_drag_result(platform_result); - - let update_result = cx.update_window(window, |_, window, cx| { - window.draw(cx).clear(cx); - window.dispatch_event( - MouseDownEvent { - position: point(px(10.), px(10.)), - button: MouseButton::Left, - modifiers: Default::default(), - click_count: 1, - first_mouse: false, - } - .to_platform_input(), - cx, - ); - window.dispatch_event( - MouseMoveEvent { - position: point(px(20.), px(20.)), - pressed_button: Some(MouseButton::Left), - modifiers: Default::default(), - } - .to_platform_input(), - cx, - ); - assert!(cx.active_drag.is_some()); - }); - assert!( - update_result.is_ok(), - "failed to start drag: {update_result:?}" - ); - - assert!(cx.test_window(window).external_drag_files().is_empty()); - Drag { - window, - observed_drag_moves, - observed_drops, - } - } - - let successful_path = PathBuf::from("/tmp/successful-drag"); - let successful = start_drag(cx, successful_path.clone(), true); - let outside_position = point(px(-1.), px(20.)); - let update_result = cx.update_window(successful.window, |_, window, cx| { - window.dispatch_event( - MouseMoveEvent { - position: outside_position, - pressed_button: Some(MouseButton::Left), - modifiers: Default::default(), - } - .to_platform_input(), - cx, - ); - assert!(cx.active_drag.is_none()); - }); - assert!( - update_result.is_ok(), - "failed to promote drag: {update_result:?}" - ); - assert_eq!( - cx.test_window(successful.window).external_drag_files(), - [(successful_path.clone(), true)] - ); - // Views must still see the move that leaves the window, otherwise they never learn to tear - // down the drag state they built up while the pointer was inside. - assert_eq!( - successful.observed_drag_moves.borrow().last(), - Some(&outside_position) - ); - - let destination: AnyWindowHandle = cx.add_window(|_, _| EmptyView).into(); - let reentry_position = point(px(30.), px(30.)); - let external_paths = || ExternalPaths([successful_path.clone()].into_iter().collect()); - let update_result = cx.update_window(destination, |_, window, cx| { - window.dispatch_event( - FileDropEvent::Entered { - position: reentry_position, - paths: external_paths(), - } - .to_platform_input(), - cx, - ); - assert!(cx - .active_drag - .as_ref() - .is_some_and(|drag| drag.value.downcast_ref::().is_some())); - window.dispatch_event(FileDropEvent::Exited.to_platform_input(), cx); - assert!(cx.active_drag.is_none()); - }); - assert!( - update_result.is_ok(), - "failed to handle drag in destination window: {update_result:?}" - ); - - let update_result = cx.update_window(successful.window, |_, window, cx| { - window.dispatch_event( - FileDropEvent::Entered { - position: reentry_position, - paths: external_paths(), - } - .to_platform_input(), - cx, - ); - assert!(cx - .active_drag - .as_ref() - .is_some_and(|drag| drag.value.downcast_ref::().is_some())); - assert_eq!( - successful.observed_drag_moves.borrow().last(), - Some(&reentry_position) - ); - - window.dispatch_event(FileDropEvent::Exited.to_platform_input(), cx); - assert!(cx.active_drag.is_none()); - - window.dispatch_event( - FileDropEvent::Entered { - position: reentry_position, - paths: external_paths(), - } - .to_platform_input(), - cx, - ); - assert!(cx - .active_drag - .as_ref() - .is_some_and(|drag| drag.value.downcast_ref::().is_some())); - - window.dispatch_event( - FileDropEvent::Submit { - position: reentry_position, - } - .to_platform_input(), - cx, - ); - assert_eq!( - successful.observed_drops.borrow().as_slice(), - std::slice::from_ref(&successful_path) - ); - assert!(cx.active_drag.is_none()); - - window.dispatch_event(FileDropEvent::Exited.to_platform_input(), cx); - assert!(cx.active_drag.is_none()); - window.dispatch_event(FileDropEvent::Ended.to_platform_input(), cx); - assert!(cx.active_drag.is_none()); - - window.dispatch_event( - FileDropEvent::Entered { - position: reentry_position, - paths: external_paths(), - } - .to_platform_input(), - cx, - ); - assert!(cx - .active_drag - .as_ref() - .is_some_and(|drag| drag.value.downcast_ref::().is_some())); - window.dispatch_event(FileDropEvent::Exited.to_platform_input(), cx); - }); - assert!( - update_result.is_ok(), - "failed to restore drag in source window: {update_result:?}" - ); - - let cancelled_path = PathBuf::from("/tmp/cancelled-drag"); - let cancelled = start_drag(cx, cancelled_path.clone(), true); - let update_result = cx.update_window(cancelled.window, |_, window, cx| { - window.dispatch_event( - MouseMoveEvent { - position: outside_position, - pressed_button: Some(MouseButton::Left), - modifiers: Default::default(), - } - .to_platform_input(), - cx, - ); - assert!(cx.active_drag.is_none()); - - window.dispatch_event( - FileDropEvent::Entered { - position: reentry_position, - paths: ExternalPaths([cancelled_path].into_iter().collect()), - } - .to_platform_input(), - cx, - ); - assert!(cx - .active_drag - .as_ref() - .is_some_and(|drag| drag.value.downcast_ref::().is_some())); - assert!(cx.stop_active_drag(window)); - assert!(cx.active_drag.is_none()); - }); - assert!( - update_result.is_ok(), - "failed to cancel restored drag: {update_result:?}" - ); - assert!(!cx.update(|cx| cx.end_platform_drag(cancelled.window.window_id()))); - - let removed_path = PathBuf::from("/tmp/removed-window-drag"); - let removed = start_drag(cx, removed_path, true); - let removed_window_id = removed.window.window_id(); - let update_result = cx.update_window(removed.window, |_, window, cx| { - window.dispatch_event( - MouseMoveEvent { - position: outside_position, - pressed_button: Some(MouseButton::Left), - modifiers: Default::default(), - } - .to_platform_input(), - cx, - ); - assert!(cx.active_drag.is_none()); - window.remove_window(); - }); - assert!( - update_result.is_ok(), - "failed to remove drag source window: {update_result:?}" - ); - assert!(!cx.update(|cx| cx.end_platform_drag(removed_window_id))); - - let failed_path = PathBuf::from("/tmp/failed-drag"); - let failed = start_drag(cx, failed_path.clone(), false); - let update_result = cx.update_window(failed.window, |_, window, cx| { - for x_position in [-1., -2.] { - window.dispatch_event( - MouseMoveEvent { - position: point(px(x_position), px(20.)), - pressed_button: Some(MouseButton::Left), - modifiers: Default::default(), - } - .to_platform_input(), - cx, - ); - } - assert!(cx.active_drag.is_some()); - }); - assert!( - update_result.is_ok(), - "failed to retain drag after platform failure: {update_result:?}" - ); - assert_eq!( - cx.test_window(failed.window).external_drag_files(), - [(failed_path, true)] - ); - } - - struct FocusForwarder { - a: FocusHandle, - b: FocusHandle, - } - - impl Render for FocusForwarder { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - div() - .size_full() - .child(div().w(px(50.)).h(px(50.)).track_focus(&self.a)) - .child(div().w(px(50.)).h(px(50.)).track_focus(&self.b)) - } - } - - /// When a focus listener moves focus again (e.g. a dock forwarding focus to its - /// active panel), the resulting focus events must be dispatched without waiting - /// for an unrelated redraw of the window. - #[gpui::test] - fn test_focus_moved_by_focus_listener_is_dispatched(cx: &mut TestAppContext) { - let b_focus_count = Rc::new(Cell::new(0)); - let window = cx.add_window({ - let b_focus_count = b_focus_count.clone(); - move |window, cx| { - let a = cx.focus_handle(); - let b = cx.focus_handle(); - cx.on_focus(&a, window, |this: &mut FocusForwarder, window, cx| { - let b = this.b.clone(); - window.focus(&b, cx); - }) - .detach(); - cx.on_focus(&b, window, move |_, _, _| { - b_focus_count.set(b_focus_count.get() + 1); - }) - .detach(); - FocusForwarder { a, b } - } - }); - - window - .update(cx, |_, window, _| window.activate_window()) - .unwrap(); - cx.executor().run_until_parked(); - - window - .update(cx, |this, window, cx| { - let a = this.a.clone(); - window.focus(&a, cx); - }) - .unwrap(); - cx.executor().run_until_parked(); - - window - .update(cx, |this, window, _| { - assert!(this.b.is_focused(window)); - }) - .unwrap(); - assert_eq!(b_focus_count.get(), 1); - } - - #[gpui::test] - fn claimed_touch_drag_receives_movement_and_release(cx: &mut TestAppContext) { - let events = Rc::new(RefCell::new(Vec::new())); - let window = cx.add_window({ - let events = events.clone(); - move |_, _| TouchDragListener { events } - }); - let touch = TouchId(1); - - dispatch_touch(window, cx, touch, TouchPhase::Started, 10.); - dispatch_touch(window, cx, touch, TouchPhase::Moved, 30.); - dispatch_touch(window, cx, touch, TouchPhase::Ended, 40.); - - assert_eq!( - events.borrow().as_slice(), - [ - (TouchPhase::Started, px(10.)), - (TouchPhase::Moved, px(30.)), - (TouchPhase::Ended, px(40.)), - ] - ); - } - - struct TouchDragListener { - events: Rc>>, - } - - impl Render for TouchDragListener { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let events = self.events.clone(); - canvas( - |_, _, _| {}, - move |_, _, window, _| { - window.on_mouse_event(move |event: &TouchDragEvent, phase, window, _cx| { - if phase != DispatchPhase::Bubble { - return; - } - events.borrow_mut().push((event.phase, event.position.x)); - if event.phase == TouchPhase::Started { - window.prevent_default(); - } - }); - }, - ) - } - } - - #[gpui::test] - fn long_press_is_claimed_only_when_started_prevents_default(cx: &mut TestAppContext) { - for response in [ - LongPressResponse::PreventDefault, - LongPressResponse::StopPropagation, - LongPressResponse::None, - ] { - let phases = Rc::new(RefCell::new(Vec::new())); - let window = cx.add_window({ - let phases = phases.clone(); - move |_, _| LongPressListener { phases, response } - }); - dispatch_touch(window, cx, TouchId(1), TouchPhase::Started, 0.); - cx.executor().advance_clock(Duration::from_millis(501)); - cx.executor().run_until_parked(); - window - .update(cx, |_, window, _| { - assert_eq!( - window.long_press_capture.is_some(), - response == LongPressResponse::PreventDefault - ); - }) - .unwrap(); - dispatch_touch(window, cx, TouchId(1), TouchPhase::Moved, 2.); - dispatch_touch(window, cx, TouchId(1), TouchPhase::Ended, 2.); - window - .update(cx, |_, window, _| { - assert!(window.long_press_capture.is_none()); - }) - .unwrap(); - - let phases = phases.borrow(); - if response == LongPressResponse::PreventDefault { - assert_eq!( - phases.as_slice(), - [TouchPhase::Started, TouchPhase::Moved, TouchPhase::Ended] - ); - } else { - assert_eq!(phases.as_slice(), [TouchPhase::Started]); - } - } - } - - #[gpui::test] - fn stale_default_prevention_does_not_claim_long_press(cx: &mut TestAppContext) { - let phases = Rc::new(RefCell::new(Vec::new())); - let window = cx.add_window({ - let phases = phases.clone(); - move |_, _| LongPressListener { - phases, - response: LongPressResponse::None, - } - }); - window - .update(cx, |_, window, _| { - window.prevent_default(); - }) - .unwrap(); - - dispatch_touch(window, cx, TouchId(1), TouchPhase::Started, 0.); - cx.executor().advance_clock(Duration::from_millis(501)); - cx.executor().run_until_parked(); - dispatch_touch(window, cx, TouchId(1), TouchPhase::Moved, 2.); - - assert_eq!(phases.borrow().as_slice(), [TouchPhase::Started]); - } - - #[gpui::test] - fn resolved_touch_cancels_scheduled_long_press(cx: &mut TestAppContext) { - for (phase, position) in [ - (TouchPhase::Ended, 0.), - (TouchPhase::Cancelled, 0.), - (TouchPhase::Moved, 20.), - ] { - let phases = Rc::new(RefCell::new(Vec::new())); - let window = cx.add_window({ - let phases = phases.clone(); - move |_, _| LongPressListener { - phases, - response: LongPressResponse::PreventDefault, - } - }); - dispatch_touch(window, cx, TouchId(1), TouchPhase::Started, 0.); - dispatch_touch(window, cx, TouchId(1), phase, position); - cx.executor().advance_clock(Duration::from_millis(501)); - cx.executor().run_until_parked(); - - assert!(phases.borrow().is_empty(), "{phase:?} allowed long press"); - } - } - - #[gpui::test] - fn stale_long_press_timer_cannot_affect_replacement_touch(cx: &mut TestAppContext) { - let phases = Rc::new(RefCell::new(Vec::new())); - let window = cx.add_window({ - let phases = phases.clone(); - move |_, _| LongPressListener { - phases, - response: LongPressResponse::PreventDefault, - } - }); - let first_touch = TouchId(1); - dispatch_touch(window, cx, first_touch, TouchPhase::Started, 0.); - cx.executor().advance_clock(Duration::from_millis(250)); - dispatch_touch(window, cx, first_touch, TouchPhase::Cancelled, 0.); - dispatch_touch(window, cx, TouchId(2), TouchPhase::Started, 10.); - - cx.executor().advance_clock(Duration::from_millis(251)); - cx.executor().run_until_parked(); - assert!(phases.borrow().is_empty()); - - cx.executor().advance_clock(Duration::from_millis(250)); - cx.executor().run_until_parked(); - assert_eq!(phases.borrow().as_slice(), [TouchPhase::Started]); - } - - #[derive(Clone, Copy, PartialEq)] - enum LongPressResponse { - PreventDefault, - StopPropagation, - None, - } - - struct LongPressListener { - phases: Rc>>, - response: LongPressResponse, - } - - impl Render for LongPressListener { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let entity = cx.entity(); - let phases = self.phases.clone(); - let response = self.response; - canvas( - |_, _, _| {}, - move |_, _, window, _| { - window.on_mouse_event(move |event: &LongPressEvent, phase, window, cx| { - if phase != DispatchPhase::Bubble { - return; - } - phases.borrow_mut().push(event.phase); - match response { - LongPressResponse::PreventDefault => { - window.capture_long_press(&entity); - window.prevent_default(); - } - LongPressResponse::StopPropagation => cx.stop_propagation(), - LongPressResponse::None => {} - } - }); - }, - ) - } - } - - fn dispatch_touch( - window: crate::WindowHandle, - cx: &mut TestAppContext, - id: TouchId, - phase: TouchPhase, - x: f32, - ) { - window - .update(cx, |_, window, cx| { - window.dispatch_event( - TouchEvent { - id, - phase, - position: point(px(x), px(0.)), - predicted_position: None, - force: None, - } - .to_platform_input(), - cx, - ); - }) - .unwrap(); - } -} diff --git a/crates/gpui_pre_wgpu/Cargo.lock b/crates/gpui_pre_wgpu/Cargo.lock deleted file mode 100644 index f9cd39b..0000000 --- a/crates/gpui_pre_wgpu/Cargo.lock +++ /dev/null @@ -1,4715 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "accesskit" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" -dependencies = [ - "enumn", - "uuid", -] - -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "aligned" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" -dependencies = [ - "as-slice", -] - -[[package]] -name = "aligned-vec" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" -dependencies = [ - "equator", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" - -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" - -[[package]] -name = "arg_enum_proc_macro" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "as-slice" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" -dependencies = [ - "stable_deref_trait", -] - -[[package]] -name = "ash" -version = "0.38.0+1.3.281" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" -dependencies = [ - "libloading", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-compression" -version = "0.4.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0" -dependencies = [ - "compression-codecs", - "compression-core", - "futures-core", - "futures-io", - "pin-project-lite", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "atomic" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "av-scenechange" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" -dependencies = [ - "aligned", - "anyhow", - "arg_enum_proc_macro", - "arrayvec", - "log", - "num-rational", - "num-traits", - "pastey", - "rayon", - "thiserror 2.0.17", - "v_frame", - "y4m", -] - -[[package]] -name = "av1-grain" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" -dependencies = [ - "anyhow", - "arrayvec", - "log", - "nom", - "num-rational", - "v_frame", -] - -[[package]] -name = "avif-serialize" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link", -] - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.13.1", - "cexpr", - "clang-sys", - "itertools 0.11.0", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 2.1.1", - "shlex 1.3.0", - "syn 2.0.117", -] - -[[package]] -name = "bit-set" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" - -[[package]] -name = "bit_field" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" - -[[package]] -name = "bitstream-io" -version = "4.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" -dependencies = [ - "core2", -] - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2", -] - -[[package]] -name = "borsh" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" -dependencies = [ - "cfg_aliases", -] - -[[package]] -name = "built" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "bytemuck" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - -[[package]] -name = "bzip2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" -dependencies = [ - "libbz2-rs-sys", -] - -[[package]] -name = "cc" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex 2.0.1", -] - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "cgl" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" -dependencies = [ - "libc", -] - -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "codespan-reporting" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba7a06c0b31fff5ff2e1e7d37dbf940864e2a974b336e1a2938d10af6e8fb283" -dependencies = [ - "serde", - "termcolor", - "unicode-width", -] - -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - -[[package]] -name = "compression-codecs" -version = "0.4.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" -dependencies = [ - "bzip2", - "compression-core", - "flate2", - "memchr", -] - -[[package]] -name = "compression-core" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "core-foundation" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" -dependencies = [ - "bitflags 2.13.1", - "core-foundation", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.13.1", - "core-foundation", - "libc", -] - -[[package]] -name = "core-graphics2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4416167a69126e617f8d0a214af0e3c1dbdeffcb100ddf72dcd1a1ac9893c146" -dependencies = [ - "bitflags 2.13.1", - "block", - "cfg-if", - "core-foundation", - "libc", -] - -[[package]] -name = "core-text" -version = "21.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130" -dependencies = [ - "core-foundation", - "core-graphics", - "foreign-types", - "libc", -] - -[[package]] -name = "core-video" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139679cc63eb9504bdbe37e37874b0247136177655f0008588781e90863afa62" -dependencies = [ - "block", - "core-foundation", - "core-graphics2", - "io-surface", - "libc", - "metal", -] - -[[package]] -name = "core2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" -dependencies = [ - "memchr", -] - -[[package]] -name = "core_maths" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" -dependencies = [ - "libm", -] - -[[package]] -name = "cosmic-text" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be17b688510d934ce13f48a2beba700e11583e281e0fda99c22bb256a14eda73" -dependencies = [ - "bitflags 2.13.1", - "fontdb", - "harfrust", - "linebender_resource_handle", - "log", - "rangemap", - "rustc-hash 2.1.1", - "self_cell", - "skrifa 0.40.0", - "smol_str", - "swash", - "sys-locale", - "unicode-bidi", - "unicode-linebreak", - "unicode-script", - "unicode-segmentation", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "ctor" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d83cb7e7a873830708d6b02a78cd36a592c6fa14bf267b68725103b85c0d77f" -dependencies = [ - "link-section", - "linktime-proc-macro", -] - -[[package]] -name = "data-url" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.117", - "unicode-xid", -] - -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.48.0", -] - -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.13.1", - "objc2", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "dlib" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" -dependencies = [ - "libloading", -] - -[[package]] -name = "document-features" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" -dependencies = [ - "litrs", -] - -[[package]] -name = "dwrote" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b35532432acc8b19ceed096e35dfa088d3ea037fe4f3c085f1f97f33b4d02" -dependencies = [ - "lazy_static", - "libc", - "winapi", - "wio", -] - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "enumn" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "equator" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" -dependencies = [ - "equator-macro", -] - -[[package]] -name = "equator-macro" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "erased-serde" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "etagere" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" -dependencies = [ - "euclid", - "svg_fmt", -] - -[[package]] -name = "euclid" -version = "0.22.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" -dependencies = [ - "num-traits", -] - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "exr" -version = "1.74.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" -dependencies = [ - "bit_field", - "half", - "lebe", - "miniz_oxide", - "rayon-core", - "smallvec", - "zune-inflate", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "fax" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" -dependencies = [ - "fax_derive", -] - -[[package]] -name = "fax_derive" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" - -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - -[[package]] -name = "flate2" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" - -[[package]] -name = "float-ord" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" - -[[package]] -name = "float_next_after" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" - -[[package]] -name = "flume" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" -dependencies = [ - "fastrand", - "futures-core", - "futures-sink", - "spin 0.9.8", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "font-types" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "511e2c18a516c666d27867d2f9821f76e7d591f762e9fc41dd6cc5c90fe54b0b" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "font-types" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4d2d0cf79d38430cc9dc9aadec84774bff2e1ba30ae2bf6c16cfce9385a23" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "fontconfig-parser" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" -dependencies = [ - "roxmltree 0.20.0", -] - -[[package]] -name = "fontdb" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" -dependencies = [ - "fontconfig-parser", - "log", - "memmap2", - "slotmap", - "tinyvec", - "ttf-parser", -] - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "freetype-sys" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-concurrency" -version = "7.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6" -dependencies = [ - "fixedbitset", - "futures-core", - "futures-lite", - "pin-project", - "smallvec", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "gif" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" -dependencies = [ - "color_quant", - "weezl", -] - -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - -[[package]] -name = "gl_generator" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" -dependencies = [ - "khronos_api", - "log", - "xml-rs", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "glow" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" -dependencies = [ - "js-sys", - "slotmap", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "glutin_wgl_sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" -dependencies = [ - "gl_generator", -] - -[[package]] -name = "gpu-allocator" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" -dependencies = [ - "ash", - "hashbrown 0.16.1", - "log", - "presser", - "thiserror 2.0.17", - "windows", -] - -[[package]] -name = "gpu-descriptor" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" -dependencies = [ - "bitflags 2.13.1", - "gpu-descriptor-types", - "hashbrown 0.15.5", -] - -[[package]] -name = "gpu-descriptor-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" -dependencies = [ - "bitflags 2.13.1", -] - -[[package]] -name = "gpui-pre" -version = "0.3.3" -dependencies = [ - "accesskit", - "anyhow", - "async-channel", - "async-task", - "bindgen", - "bitflags 2.13.1", - "chrono", - "core-video", - "ctor", - "derive_more", - "etagere", - "futures", - "futures-concurrency", - "getrandom 0.3.4", - "gpui-pre-collections", - "gpui-pre-http-client", - "gpui-pre-macros", - "gpui-pre-refineable", - "gpui-pre-scheduler", - "gpui-pre-shared-string", - "gpui-pre-sum-tree", - "gpui-pre-util", - "gpui-pre-util-macros", - "gpui-pre-ztracing", - "heapless", - "image", - "inventory", - "itertools 0.14.0", - "log", - "lyon", - "num_cpus", - "parking", - "parking_lot", - "pin-project", - "pollster 0.4.0", - "postage", - "profiling", - "rand", - "raw-window-handle", - "regex", - "resvg", - "schemars", - "seahash", - "serde", - "serde_json", - "slotmap", - "smallvec", - "spin 0.10.0", - "strum", - "taffy", - "thiserror 2.0.17", - "tracing", - "ttf-parser", - "url", - "usvg", - "uuid", - "waker-fn", - "web-time", - "windows", -] - -[[package]] -name = "gpui-pre-collections" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89c8efa2e51e368c8538a7be1ea9a12127ca03abe6cc01d9d3474e9ac4f53016" -dependencies = [ - "gpui-pre-util", - "indexmap", - "rustc-hash 2.1.1", -] - -[[package]] -name = "gpui-pre-derive-refineable" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a098d319acc9f84bf159944f96c5ea43a4f4cd7ed759f984acbd15719495aa0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "gpui-pre-http-client" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3495a45a28cb800c8626d2053406114bcaca26d390a4d4b183365b5bb8fdaf02" -dependencies = [ - "anyhow", - "async-compression", - "bytes", - "derive_more", - "futures", - "http", - "http-body", - "log", - "parking_lot", - "serde", - "serde_json", - "serde_urlencoded", - "url", -] - -[[package]] -name = "gpui-pre-macros" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be2db7b5097b4d523b2bce933604bcc5acdaf679bb9a150e8299e6c07efc29c" -dependencies = [ - "heck", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "gpui-pre-perf" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1db0c046b93c2a29120f8ee4c04bc80a4d7d6117d1164ef349faface8943491" -dependencies = [ - "gpui-pre-collections", - "serde", - "serde_json", -] - -[[package]] -name = "gpui-pre-refineable" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864e2e54a3029481dae5b6aae3ba2905f2fb1dfe66ced913b68c9ff527f8e327" -dependencies = [ - "gpui-pre-derive-refineable", -] - -[[package]] -name = "gpui-pre-scheduler" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b58a78c4e0c032900704ea49922641c534cf8bbf1bf22eda6ba00a76e88c6c3" -dependencies = [ - "async-task", - "backtrace", - "chrono", - "flume", - "futures", - "parking_lot", - "rand", - "web-time", -] - -[[package]] -name = "gpui-pre-shared-string" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82fc99fe88e44173758a500522d3f4059a6541da4b471af720e3f60cf34a2bc3" -dependencies = [ - "schemars", - "serde", - "smol_str", -] - -[[package]] -name = "gpui-pre-sum-tree" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "002baed852f20cef1188d3d5e0f749025fc718e4d6cea026b9077a9a6c10d042" -dependencies = [ - "gpui-pre-ztracing", - "heapless", - "log", - "rayon", - "tracing", -] - -[[package]] -name = "gpui-pre-util" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fe779c4cb00929aafcb2b307cd1240588aebb5d1eb328c59b80f77c05a41fad" -dependencies = [ - "anyhow", - "log", - "which", -] - -[[package]] -name = "gpui-pre-util-macros" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f0ccc4bccb6a31786095d15fc9f40d6a4c6a295522e3460331dd7e929ff2f79" -dependencies = [ - "gpui-pre-perf", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "gpui-pre-wgpu" -version = "0.3.3" -dependencies = [ - "anyhow", - "bytemuck", - "cosmic-text", - "etagere", - "gpui-pre", - "gpui-pre-collections", - "gpui-pre-util", - "itertools 0.14.0", - "log", - "naga", - "parking_lot", - "profiling", - "raw-window-handle", - "smallvec", - "swash", - "unicode-bidi", - "unicode-segmentation", - "web-sys", - "wgpu", - "zed-font-kit", -] - -[[package]] -name = "gpui-pre-zlog" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1416ea5f018e3a8a1c8be266332c443583f28f8f87379a84ab1e77622173ac0" -dependencies = [ - "anyhow", - "chrono", - "gpui-pre-collections", - "log", -] - -[[package]] -name = "gpui-pre-ztracing" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cc64a25a3cc4e4f1d8acb00074d3925339dae657c020083735738ba8af96bf7" -dependencies = [ - "gpui-pre-zlog", - "gpui-pre-ztracing-macro", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "gpui-pre-ztracing-macro" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f3ea75672348f37d94472e579f5979ee19b744d0fa5aaadc2fe129ccafa80b" - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "num-traits", - "zerocopy", -] - -[[package]] -name = "harfrust" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f9f40651a03bc0f7316bd75267ff5767e93017ef3cfffe76c6aa7252cc5a31c" -dependencies = [ - "bitflags 2.13.1", - "bytemuck", - "core_maths", - "read-fonts 0.37.0", - "smallvec", -] - -[[package]] -name = "hash32" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" -dependencies = [ - "byteorder", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heapless" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" -dependencies = [ - "hash32", - "stable_deref_trait", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hexf-parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" - -[[package]] -name = "http" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" - -[[package]] -name = "icu_properties" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" - -[[package]] -name = "icu_provider" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "image" -version = "0.25.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" -dependencies = [ - "bytemuck", - "byteorder-lite", - "color_quant", - "exr", - "gif", - "image-webp", - "moxcms", - "num-traits", - "png 0.18.0", - "qoi", - "ravif", - "rayon", - "tiff", - "zune-core", - "zune-jpeg", -] - -[[package]] -name = "image-webp" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" -dependencies = [ - "byteorder-lite", - "quick-error", -] - -[[package]] -name = "imagesize" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" - -[[package]] -name = "imgref" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[package]] -name = "interpolate_name" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "inventory" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" -dependencies = [ - "rustversion", -] - -[[package]] -name = "io-surface" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "554b8c5d64ec09a3a520fe58e4d48a73e00ff32899cdcbe32a4877afd4968b8e" -dependencies = [ - "cgl", - "core-foundation", - "core-foundation-sys", - "leaky-cow", -] - -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" -dependencies = [ - "cfg-if", - "futures-util", - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "khronos-egl" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" -dependencies = [ - "libc", - "libloading", - "pkg-config", -] - -[[package]] -name = "khronos_api" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" - -[[package]] -name = "kurbo" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" -dependencies = [ - "arrayvec", - "euclid", - "polycool", - "smallvec", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leak" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd100e01f1154f2908dfa7d02219aeab25d0b9c7fa955164192e3245255a0c73" - -[[package]] -name = "leaky-cow" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a8225d44241fd324a8af2806ba635fc7c8a7e9a7de4d5cf3ef54e71f5926fc" -dependencies = [ - "leak", -] - -[[package]] -name = "lebe" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" - -[[package]] -name = "libbz2-rs-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "libfuzzer-sys" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" -dependencies = [ - "arbitrary", - "cc", -] - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "libredox" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" -dependencies = [ - "bitflags 2.13.1", - "libc", -] - -[[package]] -name = "linebender_resource_handle" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" - -[[package]] -name = "link-section" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee1a0d6e252afe82e7bc2db42fba60e02ddf3b1accaf8cb21d96e34ba61f3d4" - -[[package]] -name = "linktime-proc-macro" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "348d0075b1fc163b26d72a7f75fc5141daf2fd1bdf128d873cbaf6785d495bdf" - -[[package]] -name = "litemap" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" - -[[package]] -name = "litrs" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -dependencies = [ - "serde_core", - "value-bag", -] - -[[package]] -name = "loop9" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" -dependencies = [ - "imgref", -] - -[[package]] -name = "lyon" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbcb7d54d54c8937364c9d41902d066656817dce1e03a44e5533afebd1ef4352" -dependencies = [ - "lyon_algorithms", - "lyon_tessellation", -] - -[[package]] -name = "lyon_algorithms" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c0829e28c4f336396f250d850c3987e16ce6db057ffe047ce0dd54aab6b647" -dependencies = [ - "lyon_path", - "num-traits", -] - -[[package]] -name = "lyon_geom" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e16770d760c7848b0c1c2d209101e408207a65168109509f8483837a36cf2e7" -dependencies = [ - "arrayvec", - "euclid", - "num-traits", -] - -[[package]] -name = "lyon_path" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aeca86bcfd632a15984ba029b539ffb811e0a70bf55e814ef8b0f54f506fdeb" -dependencies = [ - "lyon_geom", - "num-traits", -] - -[[package]] -name = "lyon_tessellation" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f586142e1280335b1bc89539f7c97dd80f08fc43e9ab1b74ef0a42b04aa353" -dependencies = [ - "float_next_after", - "lyon_path", - "num-traits", -] - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - -[[package]] -name = "maybe-rayon" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" -dependencies = [ - "cfg-if", - "rayon", -] - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "memmap2" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" -dependencies = [ - "libc", -] - -[[package]] -name = "metal" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15" -dependencies = [ - "bitflags 2.13.1", - "block", - "core-graphics-types", - "foreign-types", - "log", - "objc", - "paste", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "moxcms" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" -dependencies = [ - "num-traits", - "pxfm", -] - -[[package]] -name = "naga" -version = "29.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2bf919621e7975acb27d881bae2fb993e0d45c8e0446e85e6272971e00dc8df" -dependencies = [ - "arrayvec", - "bit-set", - "bitflags 2.13.1", - "cfg-if", - "cfg_aliases", - "codespan-reporting", - "half", - "hashbrown 0.16.1", - "hexf-parse", - "indexmap", - "libm", - "log", - "num-traits", - "once_cell", - "rustc-hash 1.1.0", - "spirv", - "thiserror 2.0.17", - "unicode-ident", -] - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "noop_proc_macro" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num-bigint" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - -[[package]] -name = "objc2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" -dependencies = [ - "objc2-encode", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.13.1", - "dispatch2", - "objc2", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.13.1", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-metal" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" -dependencies = [ - "bitflags 2.13.1", - "block2", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" -dependencies = [ - "bitflags 2.13.1", - "objc2", - "objc2-core-foundation", - "objc2-foundation", - "objc2-metal", -] - -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "ordered-float" -version = "5.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c7c9e0d9b23589f26070720bac724174bfec1083e82f7854cdd0267518343c0" -dependencies = [ - "num-traits", -] - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pastey" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" - -[[package]] -name = "pathfinder_geometry" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" -dependencies = [ - "log", - "pathfinder_simd", -] - -[[package]] -name = "pathfinder_simd" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4500030c302e4af1d423f36f3b958d1aecb6c04184356ed5a833bf6b60435777" -dependencies = [ - "rustc_version", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pico-args" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" - -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "png" -version = "0.17.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "png" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" -dependencies = [ - "bitflags 2.13.1", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "pollster" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" - -[[package]] -name = "pollster" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" - -[[package]] -name = "polycool" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "portable-atomic" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" - -[[package]] -name = "portable-atomic-util" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "postage" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" -dependencies = [ - "atomic", - "crossbeam-queue", - "futures", - "log", - "parking_lot", - "pin-project", - "pollster 0.2.5", - "static_assertions", - "thiserror 1.0.69", -] - -[[package]] -name = "potential_utf" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" -dependencies = [ - "zerovec", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "presser" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - -[[package]] -name = "proc-macro-crate" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "profiling" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" -dependencies = [ - "profiling-procmacros", -] - -[[package]] -name = "profiling-procmacros" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" -dependencies = [ - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pxfm" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" -dependencies = [ - "num-traits", -] - -[[package]] -name = "qoi" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "range-alloc" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" - -[[package]] -name = "rangemap" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" - -[[package]] -name = "rav1e" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" -dependencies = [ - "aligned-vec", - "arbitrary", - "arg_enum_proc_macro", - "arrayvec", - "av-scenechange", - "av1-grain", - "bitstream-io", - "built", - "cfg-if", - "interpolate_name", - "itertools 0.14.0", - "libc", - "libfuzzer-sys", - "log", - "maybe-rayon", - "new_debug_unreachable", - "noop_proc_macro", - "num-derive", - "num-traits", - "paste", - "profiling", - "rand", - "rand_chacha", - "simd_helpers", - "thiserror 2.0.17", - "v_frame", - "wasm-bindgen", -] - -[[package]] -name = "ravif" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" -dependencies = [ - "avif-serialize", - "imgref", - "loop9", - "quick-error", - "rav1e", - "rayon", - "rgb", -] - -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - -[[package]] -name = "raw-window-metal" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" -dependencies = [ - "objc2", - "objc2-core-foundation", - "objc2-foundation", - "objc2-quartz-core", -] - -[[package]] -name = "rayon" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "read-fonts" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" -dependencies = [ - "bytemuck", - "font-types 0.10.0", -] - -[[package]] -name = "read-fonts" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" -dependencies = [ - "bytemuck", - "core_maths", - "font-types 0.11.0", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.13.1", -] - -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.16", - "libredox", - "thiserror 1.0.69", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "renderdoc-sys" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" - -[[package]] -name = "resvg" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b563218631706d614e23059436526d005b50ab5f2d506b55a17eb65c5eb83419" -dependencies = [ - "gif", - "image-webp", - "log", - "pico-args", - "rgb", - "svgtypes", - "tiny-skia", - "usvg", - "zune-jpeg", -] - -[[package]] -name = "rgb" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "roxmltree" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" - -[[package]] -name = "roxmltree" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" -dependencies = [ - "memchr", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "rustybuzz" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" -dependencies = [ - "bitflags 2.13.1", - "bytemuck", - "core_maths", - "log", - "smallvec", - "ttf-parser", - "unicode-bidi-mirroring", - "unicode-ccc", - "unicode-properties", - "unicode-script", -] - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schemars" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" -dependencies = [ - "dyn-clone", - "indexmap", - "ref-cast", - "schemars_derive", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.117", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - -[[package]] -name = "self_cell" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_fmt" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_json" -version = "1.0.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "indexmap", - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "simd_helpers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" -dependencies = [ - "quote", -] - -[[package]] -name = "simplecss" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" -dependencies = [ - "log", -] - -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - -[[package]] -name = "skrifa" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" -dependencies = [ - "bytemuck", - "read-fonts 0.35.0", -] - -[[package]] -name = "skrifa" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" -dependencies = [ - "bytemuck", - "read-fonts 0.37.0", -] - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "slotmap" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" -dependencies = [ - "version_check", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "smol_str" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" -dependencies = [ - "borsh", - "serde_core", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spin" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spirv" -version = "0.4.0+sdk-1.4.341.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" -dependencies = [ - "bitflags 2.13.1", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strict-num" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" -dependencies = [ - "float-cmp", -] - -[[package]] -name = "strum" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "sval" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d94c4464e595f0284970fd9c7e9013804d035d4a61ab74b113242c874c05814d" - -[[package]] -name = "sval_buffer" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0f46e34b20a39e6a2bf02b926983149b3af6609fd1ee8a6e63f6f340f3e2164" -dependencies = [ - "sval", - "sval_ref", -] - -[[package]] -name = "sval_dynamic" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d0970e53c92ab5381d3b2db1828da8af945954d4234225f6dd9c3afbcef3f5" -dependencies = [ - "sval", -] - -[[package]] -name = "sval_fmt" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43e5e6e1613e1e7fc2e1a9fdd709622e54c122ceb067a60d170d75efd491a839" -dependencies = [ - "itoa", - "ryu", - "sval", -] - -[[package]] -name = "sval_json" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aec382f7bfa6e367b23c9611f129b94eb7daaf3d8fae45a8d0a0211eb4d4c8e6" -dependencies = [ - "itoa", - "ryu", - "sval", -] - -[[package]] -name = "sval_nested" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3049d0f99ce6297f8f7d9953b35a0103b7584d8f638de40e64edb7105fa578ae" -dependencies = [ - "sval", - "sval_buffer", - "sval_ref", -] - -[[package]] -name = "sval_ref" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f88913e77506085c0a8bf6912bb6558591a960faf5317df6c1d9b227224ca6e1" -dependencies = [ - "sval", -] - -[[package]] -name = "sval_serde" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f579fd7254f4be6cd7b450034f856b78523404655848789c451bacc6aa8b387d" -dependencies = [ - "serde_core", - "sval", - "sval_nested", -] - -[[package]] -name = "svg_fmt" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" - -[[package]] -name = "svgtypes" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d" -dependencies = [ - "kurbo", - "siphasher", -] - -[[package]] -name = "swash" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47846491253e976bdd07d0f9cc24b7daf24720d11309302ccbbc6e6b6e53550a" -dependencies = [ - "skrifa 0.37.0", - "yazi", - "zeno", -] - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "sys-locale" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" -dependencies = [ - "libc", -] - -[[package]] -name = "taffy" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c034e05f6ee85a12daa63863c2245797715075c70649947aa0da54f3f2ab1d0f" -dependencies = [ - "arrayvec", - "serde", - "slotmap", - "smallvec", -] - -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" -dependencies = [ - "thiserror-impl 2.0.17", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "tiff" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" -dependencies = [ - "fax", - "flate2", - "half", - "quick-error", - "weezl", - "zune-jpeg", -] - -[[package]] -name = "tiny-skia" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" -dependencies = [ - "arrayref", - "arrayvec", - "bytemuck", - "cfg-if", - "log", - "png 0.17.16", - "tiny-skia-path", -] - -[[package]] -name = "tiny-skia-path" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" -dependencies = [ - "arrayref", - "bytemuck", - "strict-num", -] - -[[package]] -name = "tinystr" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "toml_datetime" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.23.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" -dependencies = [ - "winnow", -] - -[[package]] -name = "tracing" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tracing-core" -version = "0.1.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "ttf-parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" -dependencies = [ - "core_maths", -] - -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - -[[package]] -name = "unicode-bidi-mirroring" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" - -[[package]] -name = "unicode-ccc" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-linebreak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" - -[[package]] -name = "unicode-properties" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" - -[[package]] -name = "unicode-script" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" - -[[package]] -name = "unicode-segmentation" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" - -[[package]] -name = "unicode-vo" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "usvg" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e419dff010bb12512b0ae9e3d2f318dfbdf0167fde7eb05465134d4e8756076f" -dependencies = [ - "base64", - "data-url", - "flate2", - "fontdb", - "imagesize", - "kurbo", - "log", - "pico-args", - "roxmltree 0.21.1", - "rustybuzz", - "simplecss", - "siphasher", - "strict-num", - "svgtypes", - "tiny-skia-path", - "unicode-bidi", - "unicode-script", - "unicode-vo", - "xmlwriter", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" -dependencies = [ - "getrandom 0.3.4", - "js-sys", - "serde", - "sha1_smol", - "wasm-bindgen", -] - -[[package]] -name = "v_frame" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" -dependencies = [ - "aligned-vec", - "num-traits", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "value-bag" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" -dependencies = [ - "value-bag-serde1", - "value-bag-sval2", -] - -[[package]] -name = "value-bag-serde1" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16530907bfe2999a1773ca5900a65101e092c70f642f25cc23ca0c43573262c5" -dependencies = [ - "erased-serde", - "serde_core", - "serde_fmt", -] - -[[package]] -name = "value-bag-sval2" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d00ae130edd690eaa877e4f40605d534790d1cf1d651e7685bd6a144521b251f" -dependencies = [ - "sval", - "sval_buffer", - "sval_dynamic", - "sval_fmt", - "sval_json", - "sval_ref", - "sval_serde", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "waker-fn" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.70" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.117", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wayland-sys" -version = "0.31.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" -dependencies = [ - "dlib", - "log", - "once_cell", - "pkg-config", -] - -[[package]] -name = "web-sys" -version = "0.3.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "weezl" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" - -[[package]] -name = "wgpu" -version = "29.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76e8840e1ba2881d4cbb18d2147627a56af426ff064c0401eb0c8410c6325d07" -dependencies = [ - "arrayvec", - "bitflags 2.13.1", - "bytemuck", - "cfg-if", - "cfg_aliases", - "document-features", - "hashbrown 0.16.1", - "js-sys", - "log", - "naga", - "parking_lot", - "portable-atomic", - "profiling", - "raw-window-handle", - "smallvec", - "static_assertions", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "wgpu-core", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-core" -version = "29.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f519832254e56965a9940c4af57dcb75f702b6f6fa4a0b172f685395843a4d7" -dependencies = [ - "arrayvec", - "bit-set", - "bit-vec", - "bitflags 2.13.1", - "bytemuck", - "cfg_aliases", - "document-features", - "hashbrown 0.16.1", - "indexmap", - "log", - "naga", - "once_cell", - "parking_lot", - "portable-atomic", - "profiling", - "raw-window-handle", - "rustc-hash 1.1.0", - "smallvec", - "thiserror 2.0.17", - "wgpu-core-deps-apple", - "wgpu-core-deps-emscripten", - "wgpu-core-deps-wasm", - "wgpu-core-deps-windows-linux-android", - "wgpu-hal", - "wgpu-naga-bridge", - "wgpu-types", -] - -[[package]] -name = "wgpu-core-deps-apple" -version = "29.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e39e26c4c0e07589e67d18546cf79ff45383659fc72fca4dd293358a0347f3" -dependencies = [ - "wgpu-hal", -] - -[[package]] -name = "wgpu-core-deps-emscripten" -version = "29.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01e09be551dc939498bdd5f6b2c66e55ab275dad25825267a08605a80fc9f0af" -dependencies = [ - "wgpu-hal", -] - -[[package]] -name = "wgpu-core-deps-wasm" -version = "29.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1fb1798be2a912497d4c224f72d39bb0cb34af50e8bcc29865bc339c943059" -dependencies = [ - "wgpu-hal", -] - -[[package]] -name = "wgpu-core-deps-windows-linux-android" -version = "29.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e592c1bbef6ad047647ae6e666ebd8cee7a32bb4544d9700ec96cbf73230257" -dependencies = [ - "wgpu-hal", -] - -[[package]] -name = "wgpu-hal" -version = "29.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ace1c17727311c22a46e4e3faf56ea6de81af99dcc839bdfb54857b94d448d" -dependencies = [ - "android_system_properties", - "arrayvec", - "ash", - "bit-set", - "bitflags 2.13.1", - "block2", - "bytemuck", - "cfg-if", - "cfg_aliases", - "glow", - "glutin_wgl_sys", - "gpu-allocator", - "gpu-descriptor", - "hashbrown 0.16.1", - "js-sys", - "khronos-egl", - "libc", - "libloading", - "log", - "naga", - "ndk-sys", - "objc2", - "objc2-core-foundation", - "objc2-foundation", - "objc2-metal", - "objc2-quartz-core", - "once_cell", - "ordered-float", - "parking_lot", - "portable-atomic", - "portable-atomic-util", - "profiling", - "range-alloc", - "raw-window-handle", - "raw-window-metal", - "renderdoc-sys", - "smallvec", - "thiserror 2.0.17", - "wasm-bindgen", - "wayland-sys", - "web-sys", - "wgpu-naga-bridge", - "wgpu-types", - "windows", - "windows-core", - "windows-result", -] - -[[package]] -name = "wgpu-naga-bridge" -version = "29.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95226013f547544b223281cd16a4fb549aa9dcb562adbda0faae4c73ffbbc161" -dependencies = [ - "naga", - "wgpu-types", -] - -[[package]] -name = "wgpu-types" -version = "29.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84bf84cd9ca8ca45e2b223a3868f1adf9bfc0c66aeac212e76ee7e40fdadf8f5" -dependencies = [ - "bitflags 2.13.1", - "bytemuck", - "js-sys", - "log", - "raw-window-handle", - "web-sys", -] - -[[package]] -name = "which" -version = "8.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3ef584124b911bcc3875c2f1472e80f24361ceb789bd1c62b3e9a3df9ff43c" -dependencies = [ - "libc", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core", - "windows-link", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core", - "windows-link", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "winnow" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" -dependencies = [ - "memchr", -] - -[[package]] -name = "wio" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" -dependencies = [ - "winapi", -] - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "writeable" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" - -[[package]] -name = "xml-rs" -version = "0.8.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" - -[[package]] -name = "xmlwriter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" - -[[package]] -name = "y4m" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" - -[[package]] -name = "yazi" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" - -[[package]] -name = "yeslogic-fontconfig-sys" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503a066b4c037c440169d995b869046827dbc71263f6e8f3be6d77d4f3229dbd" -dependencies = [ - "dlib", - "once_cell", - "pkg-config", -] - -[[package]] -name = "yoke" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zed-font-kit" -version = "0.14.1-zed" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3898e450f36f852edda72e3f985c34426042c4951790b23b107f93394f9bff5" -dependencies = [ - "bitflags 2.13.1", - "byteorder", - "core-foundation", - "core-graphics", - "core-text", - "dirs", - "dwrote", - "float-ord", - "freetype-sys", - "lazy_static", - "libc", - "log", - "pathfinder_geometry", - "pathfinder_simd", - "walkdir", - "winapi", - "yeslogic-fontconfig-sys", -] - -[[package]] -name = "zeno" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" - -[[package]] -name = "zerocopy" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f212a141d820099d57ffafb9569be9617a6f27d3dc881fbee8fb56642f917a9" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" - -[[package]] -name = "zune-core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" - -[[package]] -name = "zune-inflate" -version = "0.2.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "zune-jpeg" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" -dependencies = [ - "zune-core", -] diff --git a/crates/gpui_pre_wgpu/Cargo.toml b/crates/gpui_pre_wgpu/Cargo.toml deleted file mode 100644 index 0bfc6d7..0000000 --- a/crates/gpui_pre_wgpu/Cargo.toml +++ /dev/null @@ -1,150 +0,0 @@ -# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO -# -# When uploading crates to the registry Cargo will automatically -# "normalize" Cargo.toml files for maximal compatibility -# with all versions of Cargo and also rewrite `path` dependencies -# to registry (e.g., crates.io) dependencies. -# -# If you are reading this file be aware that the original Cargo.toml -# will likely look very different (and much more reasonable). -# See Cargo.toml.orig for the original contents. - -[package] -edition = "2024" -name = "gpui-pre-wgpu" -version = "0.3.3" -build = false -publish = true -autolib = false -autobins = false -autoexamples = false -autotests = false -autobenches = false -description = "Zed's `gpui_wgpu` crate (gpui-pre snapshot of zed@5b055fa)" -readme = false -license = "Apache-2.0" -repository = "https://github.com/zed-industries/zed" -resolver = "2" - -[package.metadata.gpui-pre] -zed-crate = "gpui_wgpu" -zed-version = "0.1.0" -zed-rev = "5b055fa789a8b8d38ac951a6e0cde272f66b4495" - -[features] -default = [] -font-kit = ["dep:font-kit"] - -[lib] -name = "gpui_wgpu" -path = "src/gpui_wgpu.rs" - -[[bench]] -name = "layout_line" -path = "benches/layout_line.rs" -harness = false - -[dependencies.anyhow] -version = "1.0.86" - -[dependencies.bytemuck] -version = "1" - -[dependencies.collections] -version = "=0.3.3" -package = "gpui-pre-collections" - -[dependencies.cosmic-text] -version = "0.19.0" - -[dependencies.etagere] -version = "0.2" - -[dependencies.font-kit] -version = "0.14.1-zed" -optional = true -package = "zed-font-kit" - -[dependencies.gpui] -version = "=0.3.3" -default-features = false -package = "gpui-pre" - -[dependencies.gpui_util] -version = "=0.3.3" -package = "gpui-pre-util" - -[dependencies.itertools] -version = "0.14.0" - -[dependencies.log] -version = "0.4.16" -features = [ - "kv_unstable_serde", - "serde", -] - -[dependencies.parking_lot] -version = "0.12.1" - -[dependencies.profiling] -version = "1" - -[dependencies.raw-window-handle] -version = "0.6" - -[dependencies.smallvec] -version = "1.6" -features = [ - "union", - "const_new", -] - -[dependencies.swash] -version = "0.2.6" - -[dependencies.unicode-bidi] -version = "0.3.18" -features = ["hardcoded-data"] -default-features = false - -[dependencies.unicode-segmentation] -version = "1.10" - -[dependencies.wgpu] -version = "29.0.4" - -[dev-dependencies.naga] -version = "29.0.4" -features = ["wgsl-in"] - -[target.'cfg(target_family = "wasm")'.dependencies.web-sys] -version = "0.3" -features = ["HtmlCanvasElement"] - -[target.'cfg(target_family = "wasm")'.dependencies.wgpu] -version = "29.0.4" -features = ["webgl"] - -[lints.clippy] -dbg_macro = "deny" -declare_interior_mutable_const = "deny" -disallowed_methods = "deny" -large_enum_variant = "allow" -let_underscore_future = "allow" -nonminimal_bool = "allow" -redundant_clone = "deny" -single_range_in_vec_init = "allow" -todo = "deny" -too_many_arguments = "allow" -type_complexity = "allow" - -[lints.clippy.style] -level = "allow" -priority = -1 - -[lints.rust.unexpected_cfgs] -level = "allow" -priority = 0 - -[workspace] diff --git a/crates/gpui_pre_wgpu/LICENSE-APACHE b/crates/gpui_pre_wgpu/LICENSE-APACHE deleted file mode 100644 index 461a0fe..0000000 --- a/crates/gpui_pre_wgpu/LICENSE-APACHE +++ /dev/null @@ -1,222 +0,0 @@ -Copyright 2022 - 2025 Zed Industries, Inc. - - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - - http://www.apache.org/licenses/LICENSE-2.0 - - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - - - -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 diff --git a/crates/gpui_pre_wgpu/benches/layout_line.rs b/crates/gpui_pre_wgpu/benches/layout_line.rs deleted file mode 100644 index 6d66a7a..0000000 --- a/crates/gpui_pre_wgpu/benches/layout_line.rs +++ /dev/null @@ -1,104 +0,0 @@ -use criterion::{Criterion, criterion_group, criterion_main}; -use gpui::{FontFallbacks, FontRun, PlatformTextSystem, font, px}; -use gpui_wgpu::CosmicTextSystem; -use std::borrow::Cow; - -const LILEX: &[u8] = include_bytes!("../../../assets/fonts/lilex/Lilex-Regular.ttf"); -const IBM_PLEX: &[u8] = - include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf"); - -// ~4 000 chars of typical ASCII code text, as a single display line. -// -// `layout_line` is handed one line at a time, already split on `\n` by its -// callers, so the newlines are replaced rather than kept: leaving them in would -// make this measure the multi-paragraph path instead of the common one, since -// `\n` is itself a bidi paragraph separator. -fn code_text() -> String { - concat!( - " fn compute_run_spans(\n", - " text: &str,\n", - " run_offset: usize,\n", - " run_len: usize,\n", - " primary: FontId,\n", - " fallback_chain: &[(FontId, SharedString)],\n", - " covers: &impl Fn(FontId, char) -> bool,\n", - " ) -> SmallVec<[RunSpan; 4]> {\n", - " let mut spans = SmallVec::new();\n", - " let run_end = run_offset + run_len;\n", - " if run_end <= run_offset { return spans; }\n", - " let run_text = &text[run_offset..run_end];\n", - " let mut span_start = run_offset;\n", - " let mut span_slot: Option = None;\n", - " for (ch_idx, ch) in run_text.char_indices() {\n", - " let abs = run_offset + ch_idx;\n", - " let next = pick_covering_slot(ch, span_slot, primary, fallback_chain, covers);\n", - " if next == span_slot { continue; }\n", - " if abs > span_start {\n", - " spans.push(RunSpan { start: span_start, end: abs, slot: span_slot });\n", - " }\n", - " span_start = abs;\n", - " span_slot = next;\n", - " }\n", - " spans\n", - " }\n", - ) - .repeat(8) // ~3 800 chars - .replace('\n', " ") -} - -fn bench_layout_line(c: &mut Criterion) { - let system = CosmicTextSystem::new_without_system_fonts("Lilex"); - system - .add_fonts(vec![Cow::Borrowed(LILEX), Cow::Borrowed(IBM_PLEX)]) - .unwrap(); - - let font_id_no_fallback = system.font_id(&font("Lilex")).unwrap(); - - let font_id_with_fallback = { - let mut f = font("Lilex"); - f.fallbacks = Some(FontFallbacks::from_fonts(vec!["IBM Plex Sans".to_string()])); - system.font_id(&f).unwrap() - }; - - let text = code_text(); - - // Same text, but with a bidi paragraph separator (U+001C) and RTL content - // forcing the per-paragraph shaping path. - let text_mixed_direction = text.clone() + "\u{001c}\u{05d0}\u{05d1}"; - assert!( - !text.contains('\n'), - "fast-path corpus must contain no separator" - ); - - let runs_no_fallback = vec![FontRun { - len: text.len(), - font_id: font_id_no_fallback, - }]; - let runs_with_fallback = vec![FontRun { - len: text.len(), - font_id: font_id_with_fallback, - }]; - let runs_mixed_direction = vec![FontRun { - len: text_mixed_direction.len(), - font_id: font_id_no_fallback, - }]; - - let mut group = c.benchmark_group("layout_line"); - - group.bench_function("no_fallback", |b| { - b.iter(|| system.layout_line(&text, px(14.0), &runs_no_fallback)) - }); - - group.bench_function("with_fallback_ascii", |b| { - b.iter(|| system.layout_line(&text, px(14.0), &runs_with_fallback)) - }); - - group.bench_function("mixed_direction_paragraphs", |b| { - b.iter(|| system.layout_line(&text_mixed_direction, px(14.0), &runs_mixed_direction)) - }); - - group.finish(); -} - -criterion_group!(benches, bench_layout_line); -criterion_main!(benches); diff --git a/crates/gpui_pre_wgpu/src/cosmic_text_system.rs b/crates/gpui_pre_wgpu/src/cosmic_text_system.rs deleted file mode 100644 index e84d924..0000000 --- a/crates/gpui_pre_wgpu/src/cosmic_text_system.rs +++ /dev/null @@ -1,1454 +0,0 @@ -use anyhow::{Context as _, Ok, Result}; -use collections::HashMap; -use cosmic_text::{ - Attrs, AttrsList, Ellipsize, Family, Font as CosmicTextFont, - FontFeatures as CosmicFontFeatures, FontSystem, ShapeBuffer, ShapeLine, Stretch, Style, Weight, -}; -use gpui::{ - Bounds, DevicePixels, Font, FontFallbacks, FontFeatures, FontId, FontMetrics, FontRun, GlyphId, - IsZero as _, LineLayout, Pixels, PlatformTextSystem, RenderGlyphParams, SUBPIXEL_VARIANTS_X, - SUBPIXEL_VARIANTS_Y, ShapedGlyph, ShapedRun, SharedString, Size, TextRenderingMode, point, - size, -}; - -use itertools::Itertools; -use parking_lot::RwLock; -use smallvec::SmallVec; -use std::{borrow::Cow, ops::Range, sync::Arc}; -use swash::{ - scale::{Render, ScaleContext, Source, StrikeWith}, - zeno::{Format, Vector}, -}; -use unicode_segmentation::UnicodeSegmentation; - -pub struct CosmicTextSystem(RwLock); - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -struct FontKey { - family: SharedString, - features: FontFeatures, - fallbacks: Option, -} - -impl FontKey { - fn new(family: SharedString, features: FontFeatures, fallbacks: Option) -> Self { - Self { - family, - features, - fallbacks, - } - } -} - -struct CosmicTextSystemState { - font_system: FontSystem, - scratch: ShapeBuffer, - swash_scale_context: ScaleContext, - pending_glyph_images: HashMap, - /// Contains all already loaded fonts, including all faces. Indexed by `FontId`. - loaded_fonts: Vec, - /// Caches the `FontId`s associated with a specific family to avoid iterating the font database - /// for every font face in a family. - font_ids_by_family_cache: HashMap>, - system_font_fallback: String, -} - -struct LoadedFont { - font: Arc, - features: CosmicFontFeatures, - is_known_emoji_font: bool, - /// resolved at load time so `layout_line` shares one chain across faces. - /// `Arc` keeps clone cheap on the per-run hot path. - user_fallback_chain: Arc<[(FontId, SharedString)]>, -} - -struct FontMatchProperties { - primary_family_name: SharedString, - stretch: Stretch, - style: Style, - weight: Weight, - features: CosmicFontFeatures, - fallback_chain: Arc<[(FontId, SharedString)]>, -} - -impl FontMatchProperties { - fn attributes<'a>(&'a self, font_id: FontId, family_name: &'a str) -> Attrs<'a> { - Attrs::new() - .metadata(font_id.0) - .family(Family::Name(family_name)) - .stretch(self.stretch) - .style(self.style) - .weight(self.weight) - .font_features(self.features.clone()) - } -} - -impl CosmicTextSystem { - pub fn new(system_font_fallback: &str) -> Self { - let font_system = FontSystem::new(); - - Self(RwLock::new(CosmicTextSystemState { - font_system, - scratch: ShapeBuffer::default(), - swash_scale_context: ScaleContext::new(), - pending_glyph_images: HashMap::default(), - loaded_fonts: Vec::new(), - font_ids_by_family_cache: HashMap::default(), - system_font_fallback: system_font_fallback.to_string(), - })) - } - - pub fn new_without_system_fonts(system_font_fallback: &str) -> Self { - let font_system = FontSystem::new_with_locale_and_db( - "en-US".to_string(), - cosmic_text::fontdb::Database::new(), - ); - - Self(RwLock::new(CosmicTextSystemState { - font_system, - scratch: ShapeBuffer::default(), - swash_scale_context: ScaleContext::new(), - pending_glyph_images: HashMap::default(), - loaded_fonts: Vec::new(), - font_ids_by_family_cache: HashMap::default(), - system_font_fallback: system_font_fallback.to_string(), - })) - } -} - -impl PlatformTextSystem for CosmicTextSystem { - fn add_fonts(&self, fonts: Vec>) -> Result<()> { - self.0.write().add_fonts(fonts) - } - - fn all_font_names(&self) -> Vec { - let mut result = self - .0 - .read() - .font_system - .db() - .faces() - .filter_map(|face| face.families.first().map(|family| family.0.clone())) - .collect_vec(); - result.sort_unstable(); - result.dedup(); - result - } - - fn font_id(&self, font: &Font) -> Result { - let mut state = self.0.write(); - let key = FontKey::new( - font.family.clone(), - font.features.clone(), - font.fallbacks.clone(), - ); - let candidates = if let Some(font_ids) = state.font_ids_by_family_cache.get(&key) { - font_ids.as_slice() - } else { - let font_ids = - state.load_family(&font.family, &font.features, font.fallbacks.as_ref())?; - state.font_ids_by_family_cache.insert(key.clone(), font_ids); - state.font_ids_by_family_cache[&key].as_ref() - }; - - let ix = find_best_match(font, candidates, &state)?; - - Ok(candidates[ix]) - } - - fn prewarm_fonts(&self, font_ids: &[FontId]) { - self.0.write().prewarm_fonts(font_ids); - } - - fn font_metrics(&self, font_id: FontId) -> FontMetrics { - let metrics = self - .0 - .read() - .loaded_font(font_id) - .font - .as_swash() - .metrics(&[]); - - FontMetrics { - units_per_em: metrics.units_per_em as u32, - ascent: metrics.ascent, - descent: -metrics.descent, - line_gap: metrics.leading, - underline_position: metrics.underline_offset, - underline_thickness: metrics.stroke_size, - cap_height: metrics.cap_height, - x_height: metrics.x_height, - bounding_box: Bounds { - origin: point(0.0, 0.0), - size: size(metrics.max_width, metrics.ascent + metrics.descent), - }, - } - } - - fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { - let lock = self.0.read(); - let glyph_metrics = lock.loaded_font(font_id).font.as_swash().glyph_metrics(&[]); - let glyph_id = glyph_id.0 as u16; - Ok(Bounds { - origin: point(0.0, 0.0), - size: size( - glyph_metrics.advance_width(glyph_id), - glyph_metrics.advance_height(glyph_id), - ), - }) - } - - fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { - self.0.read().advance(font_id, glyph_id) - } - - fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { - self.0.read().glyph_for_char(font_id, ch) - } - - fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result> { - self.0.write().raster_bounds(params) - } - - fn rasterize_glyph( - &self, - params: &RenderGlyphParams, - raster_bounds: Bounds, - ) -> Result<(Size, Vec)> { - self.0.write().rasterize_glyph(params, raster_bounds) - } - - fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout { - self.0.write().layout_line(text, font_size, runs) - } - - fn recommended_rendering_mode( - &self, - _font_id: FontId, - _font_size: Pixels, - ) -> TextRenderingMode { - TextRenderingMode::Subpixel - } -} - -impl CosmicTextSystemState { - fn loaded_font(&self, font_id: FontId) -> &LoadedFont { - &self.loaded_fonts[font_id.0] - } - - fn font_match_properties(&self, font_id: FontId) -> Option { - let loaded_font = self.loaded_font(font_id); - let Some(face) = self.font_system.db().face(loaded_font.font.id()) else { - log::warn!("font face not found in database for font_id {:?}", font_id); - return None; - }; - let Some(first_family) = face.families.first() else { - log::warn!("font face has no family names for font_id {:?}", font_id); - return None; - }; - - Some(FontMatchProperties { - primary_family_name: first_family.0.clone().into(), - stretch: face.stretch, - style: face.style, - weight: face.weight, - features: loaded_font.features.clone(), - fallback_chain: Arc::clone(&loaded_font.user_fallback_chain), - }) - } - - fn prewarm_fonts(&mut self, font_ids: &[FontId]) { - for &font_id in font_ids { - let Some(properties) = self.font_match_properties(font_id) else { - continue; - }; - let primary_attributes = - properties.attributes(font_id, &properties.primary_family_name); - self.font_system.get_font_matches(&primary_attributes); - - for (fallback_id, fallback_name) in &*properties.fallback_chain { - let fallback_attributes = properties.attributes(*fallback_id, fallback_name); - self.font_system.get_font_matches(&fallback_attributes); - } - } - } - - #[profiling::function] - fn add_fonts(&mut self, fonts: Vec>) -> Result<()> { - let db = self.font_system.db_mut(); - for bytes in fonts { - db.load_font_source(cosmic_text::fontdb::Source::Binary(Arc::new(bytes))); - } - Ok(()) - } - - #[profiling::function] - fn load_family( - &mut self, - name: &str, - features: &FontFeatures, - fallbacks: Option<&FontFallbacks>, - ) -> Result> { - // recurse with `fallbacks = None` so a fallback family cannot pull in - // another chain. missing fallback families are dropped so a typo in - // settings still lets the primary family load. - let user_fallback_chain: Arc<[(FontId, SharedString)]> = match fallbacks { - Some(fallbacks) if !fallbacks.fallback_list().is_empty() => { - let mut chain: Vec<(FontId, SharedString)> = Vec::new(); - for fallback_name in fallbacks.fallback_list() { - let fb_key = FontKey::new( - SharedString::from(fallback_name.clone()), - features.clone(), - None, - ); - let fb_ids = if let Some(cached) = self.font_ids_by_family_cache.get(&fb_key) { - cached.clone() - } else { - let loaded = self.load_family(fallback_name, features, None)?; - self.font_ids_by_family_cache - .insert(fb_key.clone(), loaded.clone()); - loaded - }; - let Some(&fb_id) = fb_ids.first() else { - continue; - }; - let db_id = self.loaded_fonts[fb_id.0].font.id(); - if let Some(face) = self.font_system.db().face(db_id) - && let Some(family) = face.families.first() - { - chain.push((fb_id, SharedString::from(family.0.clone()))); - } - } - Arc::from(chain) - } - _ => Arc::from(Vec::new()), - }; - - let name = gpui::font_name_with_fallbacks(name, &self.system_font_fallback); - - let families = self - .font_system - .db() - .faces() - .filter(|face| face.families.iter().any(|family| *name == family.0)) - .map(|face| (face.id, face.post_script_name.clone())) - .collect::>(); - - let cosmic_features = cosmic_font_features(features)?; - - let mut loaded_font_ids = SmallVec::new(); - for (font_id, postscript_name) in families { - let font = self - .font_system - .get_font(font_id, cosmic_text::Weight::NORMAL) - .context("Could not load font")?; - - // HACK: To let the storybook run and render Windows caption icons. We should actually do better font fallback. - let allowed_bad_font_names = [ - "SegoeFluentIcons", // NOTE: Segoe fluent icons postscript name is inconsistent - "Segoe Fluent Icons", - ]; - - if font.as_swash().charmap().map('m') == 0 - && !allowed_bad_font_names.contains(&postscript_name.as_str()) - { - self.font_system.db_mut().remove_face(font.id()); - continue; - }; - - let font_id = FontId(self.loaded_fonts.len()); - loaded_font_ids.push(font_id); - self.loaded_fonts.push(LoadedFont { - font, - features: cosmic_features.clone(), - is_known_emoji_font: check_is_known_emoji_font(&postscript_name), - user_fallback_chain: Arc::clone(&user_fallback_chain), - }); - } - - Ok(loaded_font_ids) - } - - fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { - let glyph_metrics = self.loaded_font(font_id).font.as_swash().glyph_metrics(&[]); - Ok(Size { - width: glyph_metrics.advance_width(glyph_id.0 as u16), - height: glyph_metrics.advance_height(glyph_id.0 as u16), - }) - } - - fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { - let glyph_id = self.loaded_font(font_id).font.as_swash().charmap().map(ch); - if glyph_id == 0 { - None - } else { - Some(GlyphId(glyph_id.into())) - } - } - - fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result> { - let image = self.render_glyph_image(params)?; - let bounds = Bounds { - origin: point(image.placement.left.into(), (-image.placement.top).into()), - size: size(image.placement.width.into(), image.placement.height.into()), - }; - if !bounds.is_zero() { - self.pending_glyph_images.insert(params.clone(), image); - } - Ok(bounds) - } - - #[profiling::function] - fn rasterize_glyph( - &mut self, - params: &RenderGlyphParams, - glyph_bounds: Bounds, - ) -> Result<(Size, Vec)> { - if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 { - anyhow::bail!("glyph bounds are empty"); - } - - let mut image = match self.pending_glyph_images.remove(params) { - Some(image) => image, - None => self.render_glyph_image(params)?, - }; - let bitmap_size = glyph_bounds.size; - match image.content { - swash::scale::image::Content::Color | swash::scale::image::Content::SubpixelMask => { - // Convert from RGBA to BGRA. - for pixel in image.data.chunks_exact_mut(4) { - pixel.swap(0, 2); - } - Ok((bitmap_size, image.data)) - } - swash::scale::image::Content::Mask => { - if params.subpixel_rendering { - // We must always return RGBA data when subpixel rendering is requested. - let expanded = image.data.iter().flat_map(|&a| [a, a, a, a]).collect(); - Ok((bitmap_size, expanded)) - } else { - Ok((bitmap_size, image.data)) - } - } - } - } - - fn render_glyph_image( - &mut self, - params: &RenderGlyphParams, - ) -> Result { - let loaded_font = &self.loaded_fonts[params.font_id.0]; - let font_ref = loaded_font.font.as_swash(); - let pixel_size = f32::from(params.font_size); - - let subpixel_offset = Vector::new( - params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor, - params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor, - ); - - let mut scaler = self - .swash_scale_context - .builder(font_ref) - .size(pixel_size * params.scale_factor) - .hint(true) - .build(); - - let sources: &[Source] = if params.is_emoji { - &[ - Source::ColorOutline(0), - Source::ColorBitmap(StrikeWith::BestFit), - Source::Outline, - ] - } else { - &[Source::Bitmap(StrikeWith::ExactSize), Source::Outline] - }; - - let mut renderer = Render::new(sources); - if params.subpixel_rendering { - // There seems to be a bug in Swash where the B and R values are swapped. - renderer - .format(Format::subpixel_bgra()) - .offset(subpixel_offset); - } else { - renderer.format(Format::Alpha).offset(subpixel_offset); - } - - let glyph_id: u16 = params.glyph_id.0.try_into()?; - renderer - .render(&mut scaler, glyph_id) - .with_context(|| format!("unable to render glyph via swash for {params:?}")) - } - - /// This is used when cosmic_text has chosen a fallback font instead of using the requested - /// font, typically to handle some unicode characters. When this happens, `loaded_fonts` may not - /// yet have an entry for this fallback font, and so one is added. - /// - /// Note that callers shouldn't use this `FontId` somewhere that will retrieve the corresponding - /// `LoadedFont.features`, as it will have an arbitrarily chosen or empty value. The only - /// current use of this field is for the *input* of `layout_line`, and so it's fine to use - /// `font_id_for_cosmic_id` when computing the *output* of `layout_line`. - fn font_id_for_cosmic_id(&mut self, id: cosmic_text::fontdb::ID) -> Result { - if let Some(ix) = self - .loaded_fonts - .iter() - .position(|loaded_font| loaded_font.font.id() == id) - { - Ok(FontId(ix)) - } else { - let font = self - .font_system - .get_font(id, cosmic_text::Weight::NORMAL) - .context("failed to get fallback font from cosmic-text font system")?; - let face = self - .font_system - .db() - .face(id) - .context("fallback font face not found in cosmic-text database")?; - - let font_id = FontId(self.loaded_fonts.len()); - self.loaded_fonts.push(LoadedFont { - font, - features: CosmicFontFeatures::new(), - is_known_emoji_font: check_is_known_emoji_font(&face.post_script_name), - user_fallback_chain: Arc::from(Vec::new()), - }); - - Ok(font_id) - } - } - - #[profiling::function] - fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { - if contains_paragraph_separator(text) { - self.layout_line_with_separators(text, font_size, font_runs) - } else { - self.layout_line_no_separators(text, font_size, font_runs) - } - } - - fn layout_line_with_separators( - &mut self, - text: &str, - font_size: Pixels, - font_runs: &[FontRun], - ) -> LineLayout { - let mut layout = LineLayout { - font_size, - len: text.len(), - ..Default::default() - }; - let mut paragraph_start = 0; - - for (separator_start, separator) in text - .char_indices() - .filter(|(_, character)| is_paragraph_separator(*character)) - { - let separator_end = separator_start + separator.len_utf8(); - self.shape_segment( - text, - paragraph_start..separator_start, - font_size, - font_runs, - &mut layout, - ); - self.shape_segment( - text, - separator_start..separator_end, - font_size, - font_runs, - &mut layout, - ); - paragraph_start = separator_end; - } - - self.shape_segment( - text, - paragraph_start..text.len(), - font_size, - font_runs, - &mut layout, - ); - - layout - } - - fn shape_segment( - &mut self, - text: &str, - range: Range, - font_size: Pixels, - font_runs: &[FontRun], - layout: &mut LineLayout, - ) { - if range.is_empty() { - return; - } - - let segment_font_runs = clip_font_runs(font_runs, range.clone()); - let segment = - self.layout_line_no_separators(&text[range.clone()], font_size, &segment_font_runs); - - let mut segment_runs = segment.runs; - for run in &mut segment_runs { - for glyph in &mut run.glyphs { - glyph.index += range.start; - glyph.position.x += layout.width; - } - } - - for mut run in segment_runs { - if let Some(same_run) = layout - .runs - .last_mut() - .filter(|last| last.font_id == run.font_id) - { - same_run.glyphs.append(&mut run.glyphs); - } else { - layout.runs.push(run); - } - } - - layout.width += segment.width; - layout.ascent = layout.ascent.max(segment.ascent); - layout.descent = layout.descent.max(segment.descent); - } - - fn layout_line_no_separators( - &mut self, - text: &str, - font_size: Pixels, - font_runs: &[FontRun], - ) -> LineLayout { - let mut attrs_list = AttrsList::new(&Attrs::new()); - let mut offs = 0; - for run in font_runs { - let run_end = offs + run.len; - - let Some(properties) = self.font_match_properties(run.font_id) else { - offs = run_end; - continue; - }; - - let primary_attrs = properties.attributes(run.font_id, &properties.primary_family_name); - let fallback_attrs: SmallVec<[Attrs<'_>; 4]> = properties - .fallback_chain - .iter() - .map(|(font_id, family_name)| properties.attributes(*font_id, family_name)) - .collect(); - - let spans = if properties.fallback_chain.is_empty() { - let mut spans = SmallVec::<[RunSpan; 4]>::new(); - spans.push(RunSpan { - start: offs, - end: run_end, - slot: None, - font_id: run.font_id, - }); - spans - } else { - let loaded_fonts = &self.loaded_fonts; - let covers = |id: FontId, ch: char| charmap_covers(loaded_fonts, id, ch); - compute_run_spans( - text, - offs, - run.len, - run.font_id, - &properties.fallback_chain, - &covers, - ) - }; - - for span in spans { - let attrs = match span.slot { - None => &primary_attrs, - Some(ix) => &fallback_attrs[ix], - }; - attrs_list.add_span(span.start..span.end, attrs); - } - offs = run_end; - } - - let line = ShapeLine::new( - &mut self.font_system, - text, - &attrs_list, - cosmic_text::Shaping::Advanced, - 4, - ); - let mut layout_lines = Vec::with_capacity(1); - line.layout_to_buffer( - &mut self.scratch, - f32::from(font_size), - None, // We do our own wrapping - cosmic_text::Wrap::None, - Ellipsize::None, - None, - &mut layout_lines, - None, - cosmic_text::Hinting::Disabled, - ); - - let Some(layout) = layout_lines.first() else { - return LineLayout { - font_size, - width: Pixels::ZERO, - ascent: Pixels::ZERO, - descent: Pixels::ZERO, - runs: Vec::new(), - len: text.len(), - }; - }; - - let mut runs: Vec = Vec::new(); - for glyph in &layout.glyphs { - let mut font_id = FontId(glyph.metadata); - let mut loaded_font = self.loaded_font(font_id); - if loaded_font.font.id() != glyph.font_id { - match self.font_id_for_cosmic_id(glyph.font_id) { - std::result::Result::Ok(resolved_id) => { - font_id = resolved_id; - loaded_font = self.loaded_font(font_id); - } - Err(error) => { - log::warn!( - "failed to resolve cosmic font id {:?}: {error:#}", - glyph.font_id - ); - continue; - } - } - } - let is_emoji = loaded_font.is_known_emoji_font; - - // HACK: Prevent crash caused by variation selectors. - if glyph.glyph_id == 3 && is_emoji { - continue; - } - - let shaped_glyph = ShapedGlyph { - id: GlyphId(glyph.glyph_id as u32), - position: point(glyph.x.into(), glyph.y.into()), - index: glyph.start, - is_emoji, - }; - - if let Some(last_run) = runs - .last_mut() - .filter(|last_run| last_run.font_id == font_id) - { - last_run.glyphs.push(shaped_glyph); - } else { - runs.push(ShapedRun { - font_id, - glyphs: vec![shaped_glyph], - }); - } - } - - LineLayout { - font_size, - width: layout.w.into(), - ascent: layout.max_ascent.into(), - descent: layout.max_descent.into(), - runs, - len: text.len(), - } - } -} - -#[inline(always)] -fn is_paragraph_separator(character: char) -> bool { - unicode_bidi::bidi_class(character) == unicode_bidi::BidiClass::B -} - -fn contains_paragraph_separator(text: &str) -> bool { - if text - .bytes() - .any(|byte| matches!(byte, b'\n' | b'\r' | 0x1c | 0x1d | 0x1e)) - { - return true; - } - - !text.is_ascii() && text.chars().any(is_paragraph_separator) -} - -fn clip_font_runs(font_runs: &[FontRun], range: Range) -> SmallVec<[FontRun; 4]> { - let mut clipped = SmallVec::new(); - let mut offs = 0; - for run in font_runs { - let run_start = offs; - offs += run.len; - if offs <= range.start { - continue; - } - if run_start >= range.end { - break; - } - let start = run_start.max(range.start); - let end = offs.min(range.end); - if start < end { - clipped.push(FontRun { - len: end - start, - font_id: run.font_id, - }); - } - } - clipped -} - -#[cfg(feature = "font-kit")] -fn find_best_match( - font: &Font, - candidates: &[FontId], - state: &CosmicTextSystemState, -) -> Result { - let candidate_properties = candidates - .iter() - .map(|font_id| { - let database_id = state.loaded_font(*font_id).font.id(); - let face_info = state - .font_system - .db() - .face(database_id) - .context("font face not found in database")?; - Ok(face_info_into_properties(face_info)) - }) - .collect::>>()?; - - let ix = - font_kit::matching::find_best_match(&candidate_properties, &font_into_properties(font)) - .context("requested font family contains no font matching the other parameters")?; - - Ok(ix) -} - -#[cfg(not(feature = "font-kit"))] -fn find_best_match( - font: &Font, - candidates: &[FontId], - state: &CosmicTextSystemState, -) -> Result { - if candidates.is_empty() { - anyhow::bail!("requested font family contains no font matching the other parameters"); - } - if candidates.len() == 1 { - return Ok(0); - } - - let target_weight = font.weight.0; - let target_italic = matches!( - font.style, - gpui::FontStyle::Italic | gpui::FontStyle::Oblique - ); - - let mut best_index = 0; - let mut best_score = u32::MAX; - - for (index, font_id) in candidates.iter().enumerate() { - let database_id = state.loaded_font(*font_id).font.id(); - let face_info = state - .font_system - .db() - .face(database_id) - .context("font face not found in database")?; - - let is_italic = matches!( - face_info.style, - cosmic_text::Style::Italic | cosmic_text::Style::Oblique - ); - let style_penalty: u32 = if is_italic == target_italic { 0 } else { 1000 }; - let weight_diff = (face_info.weight.0 as i32 - target_weight as i32).unsigned_abs(); - let score = style_penalty + weight_diff; - - if score < best_score { - best_score = score; - best_index = index; - } - } - - Ok(best_index) -} - -/// one contiguous slice of a `FontRun` that maps to a single slot. `slot` is -/// `None` for the primary font and `Some(ix)` for `fallback_chain[ix]`. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct RunSpan { - start: usize, - end: usize, - slot: Option, - font_id: FontId, -} - -/// walks `text[run_offset..run_offset + run_len]` and groups codepoints into -/// spans. inheriting codepoints stay in the current span so shaping clusters -/// like emoji zwj sequences and combining marks are not torn apart. -fn compute_run_spans( - text: &str, - run_offset: usize, - run_len: usize, - primary: FontId, - fallback_chain: &[(FontId, SharedString)], - covers: &impl Fn(FontId, char) -> bool, -) -> SmallVec<[RunSpan; 4]> { - let mut spans = SmallVec::new(); - let run_end = run_offset + run_len; - if run_end <= run_offset { - return spans; - } - if fallback_chain.is_empty() { - spans.push(RunSpan { - start: run_offset, - end: run_end, - slot: None, - font_id: primary, - }); - return spans; - } - let run_text = &text[run_offset..run_end]; - let mut span_start = run_offset; - let mut span_slot: Option = None; - let mut span_font_id = primary; - for (grapheme_idx, grapheme) in run_text.grapheme_indices(true) { - let abs = run_offset + grapheme_idx; - let ch = grapheme.chars().next().unwrap_or('\0'); - let next_slot = pick_covering_slot(ch, span_slot, primary, fallback_chain, covers); - if next_slot == span_slot { - continue; - } - if abs > span_start { - spans.push(RunSpan { - start: span_start, - end: abs, - slot: span_slot, - font_id: span_font_id, - }); - } - span_start = abs; - span_slot = next_slot; - span_font_id = slot_font_id(next_slot, primary, fallback_chain); - } - if span_start < run_end { - spans.push(RunSpan { - start: span_start, - end: run_end, - slot: span_slot, - font_id: span_font_id, - }); - } - spans -} - -fn slot_font_id( - slot: Option, - primary: FontId, - fallback_chain: &[(FontId, SharedString)], -) -> FontId { - match slot { - None => primary, - Some(ix) => fallback_chain[ix].0, - } -} - -fn pick_covering_slot( - ch: char, - current: Option, - primary: FontId, - fallback_chain: &[(FontId, SharedString)], - covers: &impl Fn(FontId, char) -> bool, -) -> Option { - if (ch as u32) <= 0x7F { - return None; - } - if covers(primary, ch) { - return None; - } - let current_id = slot_font_id(current, primary, fallback_chain); - if covers(current_id, ch) { - return current; - } - - fallback_chain - .iter() - .position(|(fb_id, _)| covers(*fb_id, ch)) -} - -fn charmap_covers(loaded_fonts: &[LoadedFont], id: FontId, ch: char) -> bool { - loaded_fonts - .get(id.0) - .is_some_and(|loaded| loaded.font.as_swash().charmap().map(ch) != 0) -} - -fn cosmic_font_features(features: &FontFeatures) -> Result { - let mut result = CosmicFontFeatures::new(); - for feature in features.0.iter() { - let name_bytes: [u8; 4] = feature - .0 - .as_bytes() - .try_into() - .context("Incorrect feature flag format")?; - - let tag = cosmic_text::FeatureTag::new(&name_bytes); - - result.set(tag, feature.1); - } - Ok(result) -} - -#[cfg(feature = "font-kit")] -fn font_into_properties(font: &gpui::Font) -> font_kit::properties::Properties { - font_kit::properties::Properties { - style: match font.style { - gpui::FontStyle::Normal => font_kit::properties::Style::Normal, - gpui::FontStyle::Italic => font_kit::properties::Style::Italic, - gpui::FontStyle::Oblique => font_kit::properties::Style::Oblique, - }, - weight: font_kit::properties::Weight(font.weight.0), - stretch: Default::default(), - } -} - -#[cfg(feature = "font-kit")] -fn face_info_into_properties( - face_info: &cosmic_text::fontdb::FaceInfo, -) -> font_kit::properties::Properties { - font_kit::properties::Properties { - style: match face_info.style { - cosmic_text::Style::Normal => font_kit::properties::Style::Normal, - cosmic_text::Style::Italic => font_kit::properties::Style::Italic, - cosmic_text::Style::Oblique => font_kit::properties::Style::Oblique, - }, - weight: font_kit::properties::Weight(face_info.weight.0.into()), - stretch: match face_info.stretch { - cosmic_text::Stretch::Condensed => font_kit::properties::Stretch::CONDENSED, - cosmic_text::Stretch::Expanded => font_kit::properties::Stretch::EXPANDED, - cosmic_text::Stretch::ExtraCondensed => font_kit::properties::Stretch::EXTRA_CONDENSED, - cosmic_text::Stretch::ExtraExpanded => font_kit::properties::Stretch::EXTRA_EXPANDED, - cosmic_text::Stretch::Normal => font_kit::properties::Stretch::NORMAL, - cosmic_text::Stretch::SemiCondensed => font_kit::properties::Stretch::SEMI_CONDENSED, - cosmic_text::Stretch::SemiExpanded => font_kit::properties::Stretch::SEMI_EXPANDED, - cosmic_text::Stretch::UltraCondensed => font_kit::properties::Stretch::ULTRA_CONDENSED, - cosmic_text::Stretch::UltraExpanded => font_kit::properties::Stretch::ULTRA_EXPANDED, - }, - } -} - -fn check_is_known_emoji_font(postscript_name: &str) -> bool { - // TODO: Include other common emoji fonts - postscript_name == "NotoColorEmoji" -} - -#[cfg(test)] -mod tests { - use super::*; - - fn fid(i: usize) -> FontId { - FontId(i) - } - - fn chain(ids: &[usize]) -> SmallVec<[(FontId, SharedString); 4]> { - ids.iter() - .map(|&i| (fid(i), SharedString::from(format!("fb{i}")))) - .collect() - } - - fn span(start: usize, end: usize, slot: Option, font_id: FontId) -> RunSpan { - RunSpan { - start, - end, - slot, - font_id, - } - } - - const IBM_PLEX: &[u8] = - include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf"); - - /// Every code point of `Bidi_Class=B`, each of which starts a new bidi - /// paragraph and so can split one line into mixed-direction paragraphs. - const SEPARATORS: &[char] = &[ - '\u{000a}', '\u{000d}', '\u{001c}', '\u{001d}', '\u{001e}', '\u{0085}', '\u{2029}', - ]; - - fn text_system() -> Result { - let text_system = CosmicTextSystem::new_without_system_fonts("IBM Plex Sans"); - text_system.add_fonts(vec![Cow::Borrowed(IBM_PLEX)])?; - Ok(text_system) - } - - fn layout_text(text_system: &CosmicTextSystem, text: &str) -> Result { - let font_id = text_system.font_id(&gpui::font("IBM Plex Sans"))?; - let runs = [FontRun { - len: text.len(), - font_id, - }]; - Ok(text_system.layout_line(text, gpui::px(14.0), &runs)) - } - - /// Mirrors the original crash: mixed-direction text reaching the shaper - /// through `shape_text`, which only splits lines on `\n`. - #[test] - fn shape_text_with_mixed_direction_paragraphs() -> Result<()> { - let platform_text_system = Arc::new(text_system()?); - let text_system = Arc::new(gpui::TextSystem::new(platform_text_system)); - let window_text_system = gpui::WindowTextSystem::new(text_system); - - let text: SharedString = "first line\n\u{05d0}\u{001c}A".into(); - let runs = [gpui::TextRun { - len: text.len(), - font: gpui::font("IBM Plex Sans"), - ..Default::default() - }]; - - let lines = window_text_system.shape_text(text, gpui::px(14.0), &runs, None, None)?; - - assert_eq!(lines.len(), 2); - assert_eq!(lines[1].len(), "\u{05d0}\u{001c}A".len()); - assert!(lines[1].width() > Pixels::ZERO); - Ok(()) - } - - #[test] - fn layout_line_with_mixed_direction_paragraphs() -> Result<()> { - let text_system = text_system()?; - - for separator in SEPARATORS { - for text in [ - format!("\u{05d0}{separator}A"), - format!("A{separator}\u{05d0}"), - ] { - let layout = layout_text(&text_system, &text)?; - - assert_eq!(layout.len, text.len(), "{text:?}"); - assert!(layout.width > Pixels::ZERO, "{text:?}"); - assert!( - layout.runs.iter().any(|run| !run.glyphs.is_empty()), - "{text:?}" - ); - } - } - - Ok(()) - } - - #[test] - fn layout_line_with_separators_at_line_edges() -> Result<()> { - let text_system = text_system()?; - - for text in [ - "\u{001c}", - "\u{001c}\u{001c}", - "\u{001c}\u{05d0}", - "\u{05d0}\u{001c}", - "\u{05d0}\u{001c}\u{001c}A", - "\u{001c}\u{05d0}\u{001c}A\u{001c}", - ] { - let layout = layout_text(&text_system, text)?; - assert_eq!(layout.len, text.len(), "{text:?}"); - } - - Ok(()) - } - - /// Glyph indices must stay absolute and positions ordered across segment - /// boundaries, otherwise cursor placement and hit testing desync. Uses - /// single-direction text so visual order matches logical order. - #[test] - fn layout_line_keeps_indices_and_positions_ordered_across_paragraphs() -> Result<()> { - let text_system = text_system()?; - let text = "ab\u{001c}cd\u{2029}ef"; - let layout = layout_text(&text_system, text)?; - - let glyphs: Vec<_> = layout.runs.iter().flat_map(|run| &run.glyphs).collect(); - assert!(!glyphs.is_empty()); - - for glyph in &glyphs { - assert!(glyph.index < text.len(), "{:?}", glyph.index); - assert!(text.is_char_boundary(glyph.index), "{:?}", glyph.index); - } - for pair in glyphs.windows(2) { - assert!(pair[0].index < pair[1].index); - assert!(pair[0].position.x <= pair[1].position.x); - } - - // Every segment contributes width, so the whole line is wider than its - // leading paragraph alone. - assert!(layout.width > layout_text(&text_system, "ab")?.width); - Ok(()) - } - - /// A font run boundary that does not line up with a paragraph boundary must - /// still be clipped to the right segments. - #[test] - fn layout_line_with_font_run_straddling_a_separator() -> Result<()> { - let text_system = text_system()?; - let font_id = text_system.font_id(&gpui::font("IBM Plex Sans"))?; - let text = "ab\u{001c}\u{05d0}\u{05d1}"; - - // The run boundary falls inside the trailing RTL paragraph. - let runs = [ - FontRun { - len: "ab\u{001c}\u{05d0}".len(), - font_id, - }, - FontRun { - len: "\u{05d1}".len(), - font_id, - }, - ]; - let layout = text_system.layout_line(text, gpui::px(14.0), &runs); - - assert_eq!(layout.len, text.len()); - assert!(layout.width > Pixels::ZERO); - Ok(()) - } - - /// Lines with no separator take the fast path and must be shaped exactly as - /// they were before paragraph splitting existed. - #[test] - fn layout_line_without_separators_takes_fast_path() -> Result<()> { - let text_system = text_system()?; - - for text in [ - "hello world", - "\u{05d0}\u{05d1}\u{05d2}", - "mixed \u{05d0}\u{05d1}", - ] { - assert!(!contains_paragraph_separator(text), "{text:?}"); - let layout = layout_text(&text_system, text)?; - assert_eq!(layout.len, text.len(), "{text:?}"); - assert!(layout.width > Pixels::ZERO, "{text:?}"); - } - - Ok(()) - } - - #[test] - fn paragraph_separator_detection() { - for separator in SEPARATORS { - assert!(is_paragraph_separator(*separator), "{separator:?}"); - assert!(contains_paragraph_separator(&format!("a{separator}b"))); - } - - for text in [ - "", - "plain ascii", - "\u{05d0}", - "tab\there", - "emoji \u{1f600}", - ] { - assert!(!contains_paragraph_separator(text), "{text:?}"); - } - } - - #[test] - fn font_runs_are_clipped_to_segment() { - let runs = [ - FontRun { - len: 3, - font_id: fid(1), - }, - FontRun { - len: 4, - font_id: fid(2), - }, - ]; - - assert_eq!(clip_font_runs(&runs, 0..7).as_slice(), &runs); - assert_eq!( - clip_font_runs(&runs, 2..5).as_slice(), - &[ - FontRun { - len: 1, - font_id: fid(1) - }, - FontRun { - len: 2, - font_id: fid(2) - }, - ] - ); - assert_eq!( - clip_font_runs(&runs, 3..7).as_slice(), - &[FontRun { - len: 4, - font_id: fid(2) - }] - ); - assert!(clip_font_runs(&runs, 5..5).is_empty()); - } - - #[test] - fn primary_wins_over_current_fallback_when_primary_covers() { - let primary = fid(0); - let fb = chain(&[1, 2]); - let covers = |id: FontId, _: char| id == fid(0) || id == fid(1); - assert_eq!( - pick_covering_slot('a', Some(0), primary, &fb, &covers), - None - ); - } - - #[test] - fn primary_preferred_over_fallback_when_both_cover() { - let primary = fid(0); - let fb = chain(&[1]); - let covers = |_: FontId, _: char| true; - assert_eq!(pick_covering_slot('a', None, primary, &fb, &covers), None); - } - - #[test] - fn falls_through_chain_in_order() { - let primary = fid(0); - let fb = chain(&[1, 2, 3]); - // only fallback 2 at index 1 covers. - let covers = |id: FontId, _: char| id == fid(2); - assert_eq!( - pick_covering_slot('字', None, primary, &fb, &covers), - Some(1) - ); - } - - #[test] - fn no_coverage_returns_primary() { - let primary = fid(0); - let fb = chain(&[1, 2]); - let covers = |_: FontId, _: char| false; - // nothing covers. return `None` so the `cosmic-text` built in script - // fallback can take over during shaping. - assert_eq!( - pick_covering_slot('\u{1F600}', Some(1), primary, &fb, &covers), - None - ); - } - - #[test] - fn empty_chain_always_returns_primary() { - let primary = fid(0); - let fb: SmallVec<[(FontId, SharedString); 4]> = SmallVec::new(); - let covers = |_: FontId, _: char| false; - assert_eq!(pick_covering_slot('a', None, primary, &fb, &covers), None); - } - - #[test] - fn slot_font_id_resolution() { - let primary = fid(7); - let fb = chain(&[10, 20]); - assert_eq!(slot_font_id(None, primary, &fb), fid(7)); - assert_eq!(slot_font_id(Some(0), primary, &fb), fid(10)); - assert_eq!(slot_font_id(Some(1), primary, &fb), fid(20)); - } - - #[test] - fn run_spans_with_no_chain_emit_one_primary_span() { - let primary = fid(0); - let fb: SmallVec<[(FontId, SharedString); 4]> = SmallVec::new(); - let covers = |_: FontId, _: char| false; - let text = "hello"; - let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers); - assert_eq!(spans.as_slice(), &[span(0, text.len(), None, primary)]); - } - - #[test] - fn run_spans_use_byte_offsets_for_multibyte_chars() { - let primary = fid(0); - let fb = chain(&[1]); - // primary covers ascii. fallback covers cjk. - let covers = |id: FontId, ch: char| { - if id == primary { - ch.is_ascii() - } else { - !ch.is_ascii() - } - }; - let text = "a字b"; - let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers); - // '字' is 3 bytes so split is at 1 then 4. - assert_eq!( - spans.as_slice(), - &[ - span(0, 1, None, primary), - span(1, 4, Some(0), fid(1)), - span(4, 5, None, primary), - ] - ); - } - - #[test] - fn run_spans_respect_run_offset() { - let primary = fid(0); - let fb = chain(&[1]); - let covers = |id: FontId, ch: char| { - if id == primary { - ch.is_ascii() - } else { - !ch.is_ascii() - } - }; - // outer text has a prefix that is not part of this run. - let text = "xx字y"; - let run_offset = 2; - let run_len = text.len() - run_offset; - let spans = compute_run_spans(text, run_offset, run_len, primary, &fb, &covers); - assert_eq!( - spans.as_slice(), - &[span(2, 5, Some(0), fid(1)), span(5, 6, None, primary)] - ); - } - - #[test] - fn run_spans_keep_combining_marks_with_base_in_fallback() { - let primary = fid(0); - let fb = chain(&[1]); - // primary covers ascii only. fallback covers the base char. - // combining mark must stay in the fallback span even when fallback - // does not advertise coverage of it. - let covers = |id: FontId, ch: char| { - if id == primary { - ch.is_ascii() - } else { - ch == '\u{0905}' - } - }; - // \u{0905} devanagari short a + \u{0902} candrabindu mark. - let text = "\u{0905}\u{0902}"; - let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers); - assert_eq!(spans.as_slice(), &[span(0, text.len(), Some(0), fid(1))]); - } - - #[test] - fn run_spans_keep_zwj_inside_emoji_cluster() { - let primary = fid(0); - let fb = chain(&[1]); - // only fallback covers the emoji codepoints. zwj must not split. - let covers = |id: FontId, ch: char| id == fid(1) && ch != '\u{200D}'; - // family zwj sequence woman zwj girl. - let text = "\u{1F469}\u{200D}\u{1F467}"; - let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers); - assert_eq!(spans.as_slice(), &[span(0, text.len(), Some(0), fid(1))]); - } - - #[test] - fn run_spans_collapse_adjacent_same_slot() { - let primary = fid(0); - let fb = chain(&[1]); - let covers = |id: FontId, ch: char| { - if id == primary { - ch.is_ascii() - } else { - !ch.is_ascii() - } - }; - let text = "字字字"; - let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers); - assert_eq!(spans.as_slice(), &[span(0, text.len(), Some(0), fid(1))]); - } - - #[test] - fn run_spans_empty_run_returns_no_spans() { - let primary = fid(0); - let fb = chain(&[1]); - let covers = |_: FontId, _: char| true; - let spans = compute_run_spans("anything", 3, 0, primary, &fb, &covers); - assert!(spans.is_empty()); - } -} diff --git a/crates/gpui_pre_wgpu/src/gpui_wgpu.rs b/crates/gpui_pre_wgpu/src/gpui_wgpu.rs deleted file mode 100644 index 3557d6b..0000000 --- a/crates/gpui_pre_wgpu/src/gpui_wgpu.rs +++ /dev/null @@ -1,11 +0,0 @@ -mod cosmic_text_system; -mod shaders; -mod wgpu_atlas; -mod wgpu_context; -mod wgpu_renderer; - -pub use cosmic_text_system::*; -pub use wgpu; -pub use wgpu_atlas::*; -pub use wgpu_context::*; -pub use wgpu_renderer::{GpuContext, WgpuRenderer, WgpuSurfaceConfig}; diff --git a/crates/gpui_pre_wgpu/src/shaders.rs b/crates/gpui_pre_wgpu/src/shaders.rs deleted file mode 100644 index 3d4a12a..0000000 --- a/crates/gpui_pre_wgpu/src/shaders.rs +++ /dev/null @@ -1,54 +0,0 @@ -/// Shader variant for backends with storage buffer support: the shared shader -/// logic plus the storage-buffer instance transport. -pub(crate) const STORAGE_BUFFER_SHADERS: &str = concat!( - include_str!("shaders.wgsl"), - include_str!("shaders_storage.wgsl"), -); - -/// Shader variant for WebGL2, which has no storage buffers: the shared shader -/// logic plus the texture-based instance transport. -pub(crate) const WEBGL_SHADERS: &str = concat!( - include_str!("shaders.wgsl"), - include_str!("shaders_webgl.wgsl"), -); - -/// Subpixel text rendering requires dual-source blending, which WebGL2 lacks, so -/// this variant only ever runs with the storage-buffer transport. The `enable` -/// directive must precede all declarations. -pub(crate) const SUBPIXEL_SHADERS: &str = concat!( - "enable dual_source_blending;\n", - include_str!("shaders.wgsl"), - include_str!("shaders_storage.wgsl"), - include_str!("shaders_subpixel.wgsl"), -); - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn webgl_shader_is_valid_wgsl_without_storage_buffers() { - assert!(!WEBGL_SHADERS.contains("var cyan -> green -> yellow -> red). -fn heat_map_color(value: f32, minValue: f32, maxValue: f32, position: vec2) -> vec4 { - // Normalize value to 0-1 range - let t = clamp((value - minValue) / (maxValue - minValue), 0.0, 1.0); - - // Heat map color calculation - let r = t * t; - let g = 4.0 * t * (1.0 - t); - let b = (1.0 - t) * (1.0 - t); - let heat_color = vec3(r, g, b); - - // Create a checkerboard pattern (black and white) - let sum = floor(position.x / 3) + floor(position.y / 3); - let is_odd = fract(sum * 0.5); // 0.0 for even, 0.5 for odd - let checker_value = is_odd * 2.0; // 0.0 for even, 1.0 for odd - let checker_color = vec3(checker_value); - - // Determine if value is in range (1.0 if in range, 0.0 if out of range) - let in_range = step(minValue, value) * step(value, maxValue); - - // Mix checkerboard and heat map based on whether value is in range - let final_color = mix(checker_color, heat_color, in_range); - - return vec4(final_color, 1.0); -} - -*/ - -// Contrast and gamma correction adapted from https://github.com/microsoft/terminal/blob/1283c0f5b99a2961673249fa77c6b986efb5086c/src/renderer/atlas/dwrite.hlsl -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -fn color_brightness(color: vec3) -> f32 { - // REC. 601 luminance coefficients for perceived brightness - return dot(color, vec3(0.30, 0.59, 0.11)); -} - -fn light_on_dark_contrast(enhancedContrast: f32, color: vec3) -> f32 { - let brightness = color_brightness(color); - let multiplier = saturate(4.0 * (0.75 - brightness)); - return enhancedContrast * multiplier; -} - -fn enhance_contrast(alpha: f32, k: f32) -> f32 { - return alpha * (k + 1.0) / (alpha * k + 1.0); -} - -fn enhance_contrast3(alpha: vec3, k: f32) -> vec3 { - return alpha * (k + 1.0) / (alpha * k + 1.0); -} - -fn apply_alpha_correction(a: f32, b: f32, g: vec4) -> f32 { - let brightness_adjustment = g.x * b + g.y; - let correction = brightness_adjustment * a + (g.z * b + g.w); - return a + a * (1.0 - a) * correction; -} - -fn apply_alpha_correction3(a: vec3, b: vec3, g: vec4) -> vec3 { - let brightness_adjustment = g.x * b + g.y; - let correction = brightness_adjustment * a + (g.z * b + g.w); - return a + a * (1.0 - a) * correction; -} - -fn apply_contrast_and_gamma_correction(sample: f32, color: vec3, enhanced_contrast_factor: f32, gamma_ratios: vec4) -> f32 { - let enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color); - let brightness = color_brightness(color); - - let contrasted = enhance_contrast(sample, enhanced_contrast); - return apply_alpha_correction(contrasted, brightness, gamma_ratios); -} - -fn apply_contrast_and_gamma_correction3(sample: vec3, color: vec3, enhanced_contrast_factor: f32, gamma_ratios: vec4) -> vec3 { - let enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color); - - let contrasted = enhance_contrast3(sample, enhanced_contrast); - return apply_alpha_correction3(contrasted, color, gamma_ratios); -} - -struct GlobalParams { - viewport_size: vec2, - premultiplied_alpha: u32, - pad: u32, -} - -struct GammaParams { - gamma_ratios: vec4, - grayscale_enhanced_contrast: f32, - subpixel_enhanced_contrast: f32, - is_bgr: u32, - pad: u32, -} - -@group(0) @binding(0) var globals: GlobalParams; -@group(0) @binding(1) var gamma_params: GammaParams; -@group(2) @binding(0) var t_sprite: texture_2d; -@group(2) @binding(1) var s_sprite: sampler; - -const M_PI_F: f32 = 3.1415926; -const GRAYSCALE_FACTORS: vec3 = vec3(0.2126, 0.7152, 0.0722); - -struct Bounds { - origin: vec2, - size: vec2, -} - -struct Corners { - top_left: f32, - top_right: f32, - bottom_right: f32, - bottom_left: f32, -} - -struct ContentMask { - bounds: Bounds, - corner_radii: Corners, - clip_index: u32, - clip_padding: u32, -} - -struct RoundedClip { - bounds: Bounds, - radii_x: Corners, - radii_y: Corners, - parent: u32, - padding: u32, -} - -struct Edges { - top: f32, - right: f32, - bottom: f32, - left: f32, -} - -struct Hsla { - h: f32, - s: f32, - l: f32, - a: f32, -} - -struct LinearColorStop { - color: Hsla, - percentage: f32, -} - -struct Background { - // 0u is Solid - // 1u is LinearGradient - // 2u is PatternSlash - // 3u is Checkerboard - tag: u32, - // 0u is sRGB linear color - // 1u is Oklab color - color_space: u32, - solid: Hsla, - gradient_angle_or_pattern_height: f32, - colors: array, - pad: u32, -} - -struct AtlasTextureId { - index: u32, - kind: u32, -} - -struct AtlasBounds { - origin: vec2, - size: vec2, -} - -struct AtlasTile { - texture_id: AtlasTextureId, - tile_id: u32, - padding: u32, - bounds: AtlasBounds, -} - -struct TransformationMatrix { - rotation_scale: mat2x2, - translation: vec2, -} - -fn to_device_position_impl(position: vec2) -> vec4 { - let device_position = position / globals.viewport_size * vec2(2.0, -2.0) + vec2(-1.0, 1.0); - return vec4(device_position, 0.0, 1.0); -} - -fn to_device_position(unit_vertex: vec2, bounds: Bounds) -> vec4 { - let position = unit_vertex * vec2(bounds.size) + bounds.origin; - return to_device_position_impl(position); -} - -fn to_device_position_transformed(unit_vertex: vec2, bounds: Bounds, transform: TransformationMatrix) -> vec4 { - let position = unit_vertex * vec2(bounds.size) + bounds.origin; - //Note: Rust side stores it as row-major, so transposing here - let transformed = transpose(transform.rotation_scale) * position + transform.translation; - return to_device_position_impl(transformed); -} - -fn to_tile_position(unit_vertex: vec2, tile: AtlasTile) -> vec2 { - let atlas_size = vec2(textureDimensions(t_sprite, 0)); - return (vec2(tile.bounds.origin) + unit_vertex * vec2(tile.bounds.size)) / atlas_size; -} - -fn distance_from_clip_rect_impl(position: vec2, mask: ContentMask) -> vec4 { - let tl = position - mask.bounds.origin; - let br = mask.bounds.origin + mask.bounds.size - position; - return vec4(tl.x, br.x, tl.y, br.y); -} - -fn distance_from_clip_bounds_impl(position: vec2, clip_bounds: Bounds) -> vec4 { - let tl = position - clip_bounds.origin; - let br = clip_bounds.origin + clip_bounds.size - position; - return vec4(tl.x, br.x, tl.y, br.y); -} - -fn distance_from_clip_rect(unit_vertex: vec2, bounds: Bounds, mask: ContentMask) -> vec4 { - let position = unit_vertex * vec2(bounds.size) + bounds.origin; - return distance_from_clip_rect_impl(position, mask); -} - -fn distance_from_clip_rect_transformed(unit_vertex: vec2, bounds: Bounds, mask: ContentMask, transform: TransformationMatrix) -> vec4 { - let position = unit_vertex * vec2(bounds.size) + bounds.origin; - let transformed = transpose(transform.rotation_scale) * position + transform.translation; - return distance_from_clip_rect_impl(transformed, mask); -} - -// Signed distance near an ellipse boundary; exact for circles and on the axes. -fn clip_corner_distance(point: vec2, radii: vec2) -> f32 { - if (any(radii <= vec2(0.0)) || any(point >= radii)) { - return -1e20; - } - let p = point - radii; - let k0 = length(p / radii); - let k1 = length(p / (radii * radii)); - if (k1 == 0.0) { return -min(radii.x, radii.y); } - return k0 * (k0 - 1.0) / k1; -} - -fn rounded_clip_distance(position: vec2, clip: RoundedClip) -> f32 { - let tl = position - clip.bounds.origin; - let br = clip.bounds.size - tl; - var distance = max(max(-tl.x, -tl.y), max(-br.x, -br.y)); - distance = max(distance, clip_corner_distance(tl, vec2(clip.radii_x.top_left, clip.radii_y.top_left))); - distance = max(distance, clip_corner_distance(vec2(br.x, tl.y), vec2(clip.radii_x.top_right, clip.radii_y.top_right))); - distance = max(distance, clip_corner_distance(br, vec2(clip.radii_x.bottom_right, clip.radii_y.bottom_right))); - distance = max(distance, clip_corner_distance(vec2(tl.x, br.y), vec2(clip.radii_x.bottom_left, clip.radii_y.bottom_left))); - return distance; -} - -fn content_mask_coverage(position: vec2, mask_bounds: vec4, mask_radii: vec4, clip_index: u32) -> f32 { - var distance = -1e20; - if (clip_index == 0u) { - let radii = Corners(mask_radii.x, mask_radii.y, mask_radii.z, mask_radii.w); - distance = rounded_clip_distance(position, RoundedClip(Bounds(mask_bounds.xy, mask_bounds.zw), radii, radii, 0u, 0u)); - } else { - var index = clip_index; - loop { - let clip = load_clip(index - 1u); - distance = max(distance, rounded_clip_distance(position, clip)); - index = clip.parent; - if (index == 0u) { break; } - } - } - return saturate(0.5 - distance); -} - -// https://gamedev.stackexchange.com/questions/92015/optimized-linear-to-srgb-glsl -fn srgb_to_linear(srgb: vec3) -> vec3 { - let cutoff = srgb < vec3(0.04045); - let higher = pow((srgb + vec3(0.055)) / vec3(1.055), vec3(2.4)); - let lower = srgb / vec3(12.92); - return select(higher, lower, cutoff); -} - -fn srgb_to_linear_component(a: f32) -> f32 { - let cutoff = a < 0.04045; - let higher = pow((a + 0.055) / 1.055, 2.4); - let lower = a / 12.92; - return select(higher, lower, cutoff); -} - -fn linear_to_srgb(linear: vec3) -> vec3 { - let cutoff = linear < vec3(0.0031308); - let higher = vec3(1.055) * pow(linear, vec3(1.0 / 2.4)) - vec3(0.055); - let lower = linear * vec3(12.92); - return select(higher, lower, cutoff); -} - -/// Convert a linear color to sRGBA space. -fn linear_to_srgba(color: vec4) -> vec4 { - return vec4(linear_to_srgb(color.rgb), color.a); -} - -/// Convert a sRGBA color to linear space. -fn srgba_to_linear(color: vec4) -> vec4 { - return vec4(srgb_to_linear(color.rgb), color.a); -} - -/// Hsla to linear RGBA conversion. -fn hsla_to_rgba(hsla: Hsla) -> vec4 { - let h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range - let s = hsla.s; - let l = hsla.l; - let a = hsla.a; - - let c = (1.0 - abs(2.0 * l - 1.0)) * s; - let x = c * (1.0 - abs(h % 2.0 - 1.0)); - let m = l - c / 2.0; - var color = vec3(m); - - if (h >= 0.0 && h < 1.0) { - color.r += c; - color.g += x; - } else if (h >= 1.0 && h < 2.0) { - color.r += x; - color.g += c; - } else if (h >= 2.0 && h < 3.0) { - color.g += c; - color.b += x; - } else if (h >= 3.0 && h < 4.0) { - color.g += x; - color.b += c; - } else if (h >= 4.0 && h < 5.0) { - color.r += x; - color.b += c; - } else { - color.r += c; - color.b += x; - } - - return vec4(color, a); -} - -/// Convert a linear sRGB to Oklab space. -/// Reference: https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab -fn linear_srgb_to_oklab(color: vec4) -> vec4 { - let l = 0.4122214708 * color.r + 0.5363325363 * color.g + 0.0514459929 * color.b; - let m = 0.2119034982 * color.r + 0.6806995451 * color.g + 0.1073969566 * color.b; - let s = 0.0883024619 * color.r + 0.2817188376 * color.g + 0.6299787005 * color.b; - - let l_ = pow(l, 1.0 / 3.0); - let m_ = pow(m, 1.0 / 3.0); - let s_ = pow(s, 1.0 / 3.0); - - return vec4( - 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_, - 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_, - 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_, - color.a - ); -} - -/// Convert an Oklab color to linear sRGB space. -fn oklab_to_linear_srgb(color: vec4) -> vec4 { - let l_ = color.r + 0.3963377774 * color.g + 0.2158037573 * color.b; - let m_ = color.r - 0.1055613458 * color.g - 0.0638541728 * color.b; - let s_ = color.r - 0.0894841775 * color.g - 1.2914855480 * color.b; - - let l = l_ * l_ * l_; - let m = m_ * m_ * m_; - let s = s_ * s_ * s_; - - return vec4( - 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, - -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, - -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s, - color.a - ); -} - -fn over(below: vec4, above: vec4) -> vec4 { - let alpha = above.a + below.a * (1.0 - above.a); - let color = (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha; - return vec4(color, alpha); -} - -// A standard gaussian function, used for weighting samples -fn gaussian(x: f32, sigma: f32) -> f32{ - return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * M_PI_F) * sigma); -} - -// This approximates the error function, needed for the gaussian integral -fn erf(v: vec2) -> vec2 { - let s = sign(v); - let a = abs(v); - let r1 = 1.0 + (0.278393 + (0.230389 + (0.000972 + 0.078108 * a) * a) * a) * a; - let r2 = r1 * r1; - return s - s / (r2 * r2); -} - -fn blur_along_x(x: f32, y: f32, sigma: f32, corner: f32, half_size: vec2) -> f32 { - let delta = min(half_size.y - corner - abs(y), 0.0); - let curved = half_size.x - corner + sqrt(max(0.0, corner * corner - delta * delta)); - let integral = 0.5 + 0.5 * erf((x + vec2(-curved, curved)) * (sqrt(0.5) / sigma)); - return integral.y - integral.x; -} - -// Selects corner radius based on quadrant. -fn pick_corner_radius(center_to_point: vec2, radii: Corners) -> f32 { - if (center_to_point.x < 0.0) { - if (center_to_point.y < 0.0) { - return radii.top_left; - } else { - return radii.bottom_left; - } - } else { - if (center_to_point.y < 0.0) { - return radii.top_right; - } else { - return radii.bottom_right; - } - } -} - -// Signed distance of the point to the quad's border - positive outside the -// border, and negative inside. -// -// See comments on similar code using `quad_sdf_impl` in `fs_quad` for -// explanation. -fn quad_sdf(point: vec2, bounds: Bounds, corner_radii: Corners) -> f32 { - let half_size = bounds.size / 2.0; - let center = bounds.origin + half_size; - let center_to_point = point - center; - let corner_radius = pick_corner_radius(center_to_point, corner_radii); - let corner_to_point = abs(center_to_point) - half_size; - let corner_center_to_point = corner_to_point + corner_radius; - return quad_sdf_impl(corner_center_to_point, corner_radius); -} - -fn quad_sdf_impl(corner_center_to_point: vec2, corner_radius: f32) -> f32 { - if (corner_radius == 0.0) { - // Fast path for unrounded corners. - return max(corner_center_to_point.x, corner_center_to_point.y); - } else { - // Signed distance of the point from a quad that is inset by corner_radius. - // It is negative inside this quad, and positive outside. - let signed_distance_to_inset_quad = - // 0 inside the inset quad, and positive outside. - length(max(vec2(0.0), corner_center_to_point)) + - // 0 outside the inset quad, and negative inside. - min(0.0, max(corner_center_to_point.x, corner_center_to_point.y)); - - return signed_distance_to_inset_quad - corner_radius; - } -} - -// Abstract away the final color transformation based on the -// target alpha compositing mode. -fn blend_color(color: vec4, alpha_factor: f32) -> vec4 { - let alpha = color.a * alpha_factor; - let multiplier = select(1.0, alpha, globals.premultiplied_alpha != 0u); - return vec4(color.rgb * multiplier, alpha); -} - - -struct GradientColor { - solid: vec4, - color0: vec4, - color1: vec4, -} - -fn prepare_gradient_color(tag: u32, color_space: u32, - solid: Hsla, colors: array) -> GradientColor { - var result = GradientColor(); - - if (tag == 0u || tag == 2u || tag == 3u) { - result.solid = hsla_to_rgba(solid); - } else if (tag == 1u) { - // The hsla_to_rgba is returns a linear sRGB color - result.color0 = hsla_to_rgba(colors[0].color); - result.color1 = hsla_to_rgba(colors[1].color); - - // Prepare color space in vertex for avoid conversion - // in fragment shader for performance reasons - if (color_space == 0u) { - // sRGB - result.color0 = linear_to_srgba(result.color0); - result.color1 = linear_to_srgba(result.color1); - } else if (color_space == 1u) { - // Oklab - result.color0 = linear_srgb_to_oklab(result.color0); - result.color1 = linear_srgb_to_oklab(result.color1); - } - } - - return result; -} - -fn gradient_color(background: Background, position: vec2, bounds: Bounds, - solid_color: vec4, color0: vec4, color1: vec4) -> vec4 { - var background_color = vec4(0.0); - - switch (background.tag) { - default: { - return solid_color; - } - case 1u: { - // Linear gradient background. - // -90 degrees to match the CSS gradient angle. - let angle = background.gradient_angle_or_pattern_height; - let radians = (angle % 360.0 - 90.0) * M_PI_F / 180.0; - var direction = vec2(cos(radians), sin(radians)); - let stop0_percentage = background.colors[0].percentage; - let stop1_percentage = background.colors[1].percentage; - - // Expand the short side to be the same as the long side - if (bounds.size.x > bounds.size.y) { - direction.y *= bounds.size.y / bounds.size.x; - } else { - direction.x *= bounds.size.x / bounds.size.y; - } - - // Get the t value for the linear gradient with the color stop percentages. - let half_size = bounds.size / 2.0; - let center = bounds.origin + half_size; - let center_to_point = position - center; - var t = dot(center_to_point, direction) / length(direction); - // Check the direct to determine the use x or y - if (abs(direction.x) > abs(direction.y)) { - t = (t + half_size.x) / bounds.size.x; - } else { - t = (t + half_size.y) / bounds.size.y; - } - - // Adjust t based on the stop percentages - t = (t - stop0_percentage) / (stop1_percentage - stop0_percentage); - t = clamp(t, 0.0, 1.0); - - switch (background.color_space) { - default: { - background_color = srgba_to_linear(mix(color0, color1, t)); - } - case 1u: { - let oklab_color = mix(color0, color1, t); - background_color = oklab_to_linear_srgb(oklab_color); - } - } - } - case 2u: { - // pattern slash - let gradient_angle_or_pattern_height = background.gradient_angle_or_pattern_height; - let pattern_width = (gradient_angle_or_pattern_height / 65535.0f) / 255.0f; - let pattern_interval = (gradient_angle_or_pattern_height % 65535.0f) / 255.0f; - let pattern_height = pattern_width + pattern_interval; - let stripe_angle = M_PI_F / 4.0; - let pattern_period = pattern_height * sin(stripe_angle); - let rotation = mat2x2( - cos(stripe_angle), -sin(stripe_angle), - sin(stripe_angle), cos(stripe_angle) - ); - let relative_position = position - bounds.origin; - let rotated_point = rotation * relative_position; - let pattern = rotated_point.x % pattern_period; - let distance = min(pattern, pattern_period - pattern) - pattern_period * (pattern_width / pattern_height) / 2.0f; - background_color = solid_color; - background_color.a *= saturate(0.5 - distance); - } - case 3u: { - // checkerboard - let size = background.gradient_angle_or_pattern_height; - let relative_position = position - bounds.origin; - - let x_index = floor(relative_position.x / size); - let y_index = floor(relative_position.y / size); - let should_be_colored = (x_index + y_index) % 2.0; - - background_color = solid_color; - background_color.a *= saturate(should_be_colored); - } - } - - return background_color; -} - -// --- quads --- // - -struct Quad { - order: u32, - border_style: u32, - bounds: Bounds, - content_mask: ContentMask, - background: Background, - border_color: Hsla, - corner_radii: Corners, - border_widths: Edges, -} - -struct QuadVarying { - @builtin(position) position: vec4, - @location(0) @interpolate(flat) border_color: vec4, - @location(1) @interpolate(flat) quad_id: u32, - // TODO: use `clip_distance` once Naga supports it - @location(2) clip_distances: vec4, - @location(3) @interpolate(flat) background_solid: vec4, - @location(4) @interpolate(flat) background_color0: vec4, - @location(5) @interpolate(flat) background_color1: vec4, - @location(6) @interpolate(flat) clip_mask_bounds: vec4, - @location(7) @interpolate(flat) clip_mask_radii: vec4, - @location(8) @interpolate(flat) clip_index: u32, -} - -@vertex -fn vs_quad(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> QuadVarying { - let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); - let quad = load_quad(instance_id); - - var out = QuadVarying(); - out.position = to_device_position(unit_vertex, quad.bounds); - - let gradient = prepare_gradient_color( - quad.background.tag, - quad.background.color_space, - quad.background.solid, - quad.background.colors - ); - out.background_solid = gradient.solid; - out.background_color0 = gradient.color0; - out.background_color1 = gradient.color1; - out.border_color = hsla_to_rgba(quad.border_color); - out.quad_id = instance_id; - out.clip_mask_bounds = vec4(quad.content_mask.bounds.origin, quad.content_mask.bounds.size); - out.clip_index = quad.content_mask.clip_index; - out.clip_mask_radii = vec4( - quad.content_mask.corner_radii.top_left, - quad.content_mask.corner_radii.top_right, - quad.content_mask.corner_radii.bottom_right, - quad.content_mask.corner_radii.bottom_left, - ); - out.clip_distances = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask); - return out; -} - -@fragment -fn fs_quad(input: QuadVarying) -> @location(0) vec4 { - let coverage = content_mask_coverage(input.position.xy, input.clip_mask_bounds, input.clip_mask_radii, input.clip_index); - // Alpha clip first, since we don't have `clip_distance`. - if (any(input.clip_distances < vec4(0.0)) - || coverage <= 0.0) { - return vec4(0.0); - } - - let quad = load_quad(input.quad_id); - - let background_color = gradient_color(quad.background, input.position.xy, quad.bounds, - input.background_solid, input.background_color0, input.background_color1); - - let unrounded = quad.corner_radii.top_left == 0.0 && - quad.corner_radii.bottom_left == 0.0 && - quad.corner_radii.top_right == 0.0 && - quad.corner_radii.bottom_right == 0.0; - - // Fast path when the quad is not rounded and doesn't have any border - if (quad.border_widths.top == 0.0 && - quad.border_widths.left == 0.0 && - quad.border_widths.right == 0.0 && - quad.border_widths.bottom == 0.0 && - unrounded) { - return blend_color(background_color, coverage); - } - - let size = quad.bounds.size; - let half_size = size / 2.0; - let point = input.position.xy - quad.bounds.origin; - let center_to_point = point - half_size; - - // Signed distance field threshold for inclusion of pixels. 0.5 is the - // minimum distance between the center of the pixel and the edge. - let antialias_threshold = 0.5; - - // Radius of the nearest corner - let corner_radius = pick_corner_radius(center_to_point, quad.corner_radii); - - // Width of the nearest borders - let border = vec2( - select( - quad.border_widths.right, - quad.border_widths.left, - center_to_point.x < 0.0), - select( - quad.border_widths.bottom, - quad.border_widths.top, - center_to_point.y < 0.0)); - - // 0-width borders are reduced so that `inner_sdf >= antialias_threshold`. - // The purpose of this is to not draw antialiasing pixels in this case. - let reduced_border = - vec2(select(border.x, -antialias_threshold, border.x == 0.0), - select(border.y, -antialias_threshold, border.y == 0.0)); - - // Vector from the corner of the quad bounds to the point, after mirroring - // the point into the bottom right quadrant. Both components are <= 0. - let corner_to_point = abs(center_to_point) - half_size; - - // Vector from the point to the center of the rounded corner's circle, also - // mirrored into bottom right quadrant. - let corner_center_to_point = corner_to_point + corner_radius; - - // Whether the nearest point on the border is rounded - let is_near_rounded_corner = - corner_center_to_point.x >= 0 && - corner_center_to_point.y >= 0; - - // Vector from straight border inner corner to point. - let straight_border_inner_corner_to_point = corner_to_point + reduced_border; - - // Whether the point is beyond the inner edge of the straight border. - let is_beyond_inner_straight_border = - straight_border_inner_corner_to_point.x > 0 || - straight_border_inner_corner_to_point.y > 0; - - // Whether the point is far enough inside the quad, such that the pixels are - // not affected by the straight border. - let is_within_inner_straight_border = - straight_border_inner_corner_to_point.x < -antialias_threshold && - straight_border_inner_corner_to_point.y < -antialias_threshold; - - // Fast path for points that must be part of the background. - // - // This could be optimized further for large rounded corners by including - // points in an inscribed rectangle, or some other quick linear check. - // However, that might negatively impact performance in the case of - // reasonable sizes for rounded corners. - if (is_within_inner_straight_border && !is_near_rounded_corner) { - return blend_color(background_color, coverage); - } - - // Signed distance of the point to the outside edge of the quad's border. It - // is positive outside this edge, and negative inside. - let outer_sdf = quad_sdf_impl(corner_center_to_point, corner_radius); - - // Approximate signed distance of the point to the inside edge of the quad's - // border. It is negative outside this edge (within the border), and - // positive inside. - // - // This is not always an accurate signed distance: - // * The rounded portions with varying border width use an approximation of - // nearest-point-on-ellipse. - // * When it is quickly known to be outside the edge, -1.0 is used. - var inner_sdf = 0.0; - if (corner_center_to_point.x <= 0 || corner_center_to_point.y <= 0) { - // Fast paths for straight borders. - inner_sdf = -max(straight_border_inner_corner_to_point.x, - straight_border_inner_corner_to_point.y); - } else if (is_beyond_inner_straight_border) { - // Fast path for points that must be outside the inner edge. - inner_sdf = -1.0; - } else if (reduced_border.x == reduced_border.y) { - // Fast path for circular inner edge. - inner_sdf = -(outer_sdf + reduced_border.x); - } else { - let ellipse_radii = max(vec2(0.0), corner_radius - reduced_border); - inner_sdf = quarter_ellipse_sdf(corner_center_to_point, ellipse_radii); - } - - // Negative when inside the border - let border_sdf = max(inner_sdf, outer_sdf); - - var color = background_color; - if (border_sdf < antialias_threshold) { - var border_color = input.border_color; - - // Dashed border logic when border_style == 1 - if (quad.border_style == 1) { - // Position along the perimeter in "dash space", where each dash - // period has length 1 - var t = 0.0; - - // Total number of dash periods, so that the dash spacing can be - // adjusted to evenly divide it - var max_t = 0.0; - - // Border width is proportional to dash size. This is the behavior - // used by browsers, but also avoids dashes from different segments - // overlapping when dash size is smaller than the border width. - // - // Dash pattern: (2 * border width) dash, (1 * border width) gap - let dash_length_per_width = 2.0; - let dash_gap_per_width = 1.0; - let dash_period_per_width = dash_length_per_width + dash_gap_per_width; - - // Since the dash size is determined by border width, the density of - // dashes varies. Multiplying a pixel distance by this returns a - // position in dash space - it has units (dash period / pixels). So - // a dash velocity of (1 / 10) is 1 dash every 10 pixels. - var dash_velocity = 0.0; - - // Dividing this by the border width gives the dash velocity - let dv_numerator = 1.0 / dash_period_per_width; - - if (unrounded) { - // When corners aren't rounded, the dashes are separately laid - // out on each straight line, rather than around the whole - // perimeter. This way each line starts and ends with a dash. - let is_horizontal = - corner_center_to_point.x < - corner_center_to_point.y; - - // When applying dashed borders to just some, not all, the sides. - // The way we chose border widths above sometimes comes with a 0 width value. - // So we choose again to avoid division by zero. - // TODO: A better solution exists taking a look at the whole file. - // this does not fix single dashed borders at the corners - let dashed_border = vec2( - max( - quad.border_widths.bottom, - quad.border_widths.top, - ), - max( - quad.border_widths.right, - quad.border_widths.left, - ) - ); - - let border_width = select(dashed_border.y, dashed_border.x, is_horizontal); - dash_velocity = dv_numerator / border_width; - t = select(point.y, point.x, is_horizontal) * dash_velocity; - max_t = select(size.y, size.x, is_horizontal) * dash_velocity; - } else { - // When corners are rounded, the dashes are laid out clockwise - // around the whole perimeter. - - let r_tr = quad.corner_radii.top_right; - let r_br = quad.corner_radii.bottom_right; - let r_bl = quad.corner_radii.bottom_left; - let r_tl = quad.corner_radii.top_left; - - let w_t = quad.border_widths.top; - let w_r = quad.border_widths.right; - let w_b = quad.border_widths.bottom; - let w_l = quad.border_widths.left; - - // Straight side dash velocities - let dv_t = select(dv_numerator / w_t, 0.0, w_t <= 0.0); - let dv_r = select(dv_numerator / w_r, 0.0, w_r <= 0.0); - let dv_b = select(dv_numerator / w_b, 0.0, w_b <= 0.0); - let dv_l = select(dv_numerator / w_l, 0.0, w_l <= 0.0); - - // Straight side lengths in dash space - let s_t = (size.x - r_tl - r_tr) * dv_t; - let s_r = (size.y - r_tr - r_br) * dv_r; - let s_b = (size.x - r_br - r_bl) * dv_b; - let s_l = (size.y - r_bl - r_tl) * dv_l; - - let corner_dash_velocity_tr = corner_dash_velocity(dv_t, dv_r); - let corner_dash_velocity_br = corner_dash_velocity(dv_b, dv_r); - let corner_dash_velocity_bl = corner_dash_velocity(dv_b, dv_l); - let corner_dash_velocity_tl = corner_dash_velocity(dv_t, dv_l); - - // Corner lengths in dash space - let c_tr = r_tr * (M_PI_F / 2.0) * corner_dash_velocity_tr; - let c_br = r_br * (M_PI_F / 2.0) * corner_dash_velocity_br; - let c_bl = r_bl * (M_PI_F / 2.0) * corner_dash_velocity_bl; - let c_tl = r_tl * (M_PI_F / 2.0) * corner_dash_velocity_tl; - - // Cumulative dash space upto each segment - let upto_tr = s_t; - let upto_r = upto_tr + c_tr; - let upto_br = upto_r + s_r; - let upto_b = upto_br + c_br; - let upto_bl = upto_b + s_b; - let upto_l = upto_bl + c_bl; - let upto_tl = upto_l + s_l; - max_t = upto_tl + c_tl; - - if (is_near_rounded_corner) { - let radians = atan2(corner_center_to_point.y, - corner_center_to_point.x); - let corner_t = radians * corner_radius; - - if (center_to_point.x >= 0.0) { - if (center_to_point.y < 0.0) { - dash_velocity = corner_dash_velocity_tr; - // Subtracted because radians is pi/2 to 0 when - // going clockwise around the top right corner, - // since the y axis has been flipped - t = upto_r - corner_t * dash_velocity; - } else { - dash_velocity = corner_dash_velocity_br; - // Added because radians is 0 to pi/2 when going - // clockwise around the bottom-right corner - t = upto_br + corner_t * dash_velocity; - } - } else { - if (center_to_point.y >= 0.0) { - dash_velocity = corner_dash_velocity_bl; - // Subtracted because radians is pi/2 to 0 when - // going clockwise around the bottom-left corner, - // since the x axis has been flipped - t = upto_l - corner_t * dash_velocity; - } else { - dash_velocity = corner_dash_velocity_tl; - // Added because radians is 0 to pi/2 when going - // clockwise around the top-left corner, since both - // axis were flipped - t = upto_tl + corner_t * dash_velocity; - } - } - } else { - // Straight borders - let is_horizontal = - corner_center_to_point.x < - corner_center_to_point.y; - if (is_horizontal) { - if (center_to_point.y < 0.0) { - dash_velocity = dv_t; - t = (point.x - r_tl) * dash_velocity; - } else { - dash_velocity = dv_b; - t = upto_bl - (point.x - r_bl) * dash_velocity; - } - } else { - if (center_to_point.x < 0.0) { - dash_velocity = dv_l; - t = upto_tl - (point.y - r_tl) * dash_velocity; - } else { - dash_velocity = dv_r; - t = upto_r + (point.y - r_tr) * dash_velocity; - } - } - } - } - - let dash_length = dash_length_per_width / dash_period_per_width; - let desired_dash_gap = dash_gap_per_width / dash_period_per_width; - - // Straight borders should start and end with a dash, so max_t is - // reduced to cause this. - max_t -= select(0.0, dash_length, unrounded); - if (max_t >= 1.0) { - // Adjust dash gap to evenly divide max_t. - let dash_count = floor(max_t); - let dash_period = max_t / dash_count; - border_color.a *= dash_alpha( - t, - dash_period, - dash_length, - dash_velocity, - antialias_threshold); - } else if (unrounded) { - // When there isn't enough space for the full gap between the - // two start / end dashes of a straight border, reduce gap to - // make them fit. - let dash_gap = max_t - dash_length; - if (dash_gap > 0.0) { - let dash_period = dash_length + dash_gap; - border_color.a *= dash_alpha( - t, - dash_period, - dash_length, - dash_velocity, - antialias_threshold); - } - } - } - - // Blend the border on top of the background and then linearly interpolate - // between the two as we slide inside the background. - let blended_border = over(background_color, border_color); - color = mix(background_color, blended_border, - saturate(antialias_threshold - inner_sdf)); - } - - return blend_color(color, min(coverage, saturate(antialias_threshold - outer_sdf))); -} - -// Returns the dash velocity of a corner given the dash velocity of the two -// sides, by returning the slower velocity (larger dashes). -// -// Since 0 is used for dash velocity when the border width is 0 (instead of -// +inf), this returns the other dash velocity in that case. -// -// An alternative to this might be to appropriately interpolate the dash -// velocity around the corner, but that seems overcomplicated. -fn corner_dash_velocity(dv1: f32, dv2: f32) -> f32 { - if (dv1 == 0.0) { - return dv2; - } else if (dv2 == 0.0) { - return dv1; - } else { - return min(dv1, dv2); - } -} - -// Returns alpha used to render antialiased dashes. -// `t` is within the dash when `fmod(t, period) < length`. -fn dash_alpha(t: f32, period: f32, length: f32, dash_velocity: f32, antialias_threshold: f32) -> f32 { - let half_period = period / 2; - let half_length = length / 2; - // Value in [-half_period, half_period]. - // The dash is in [-half_length, half_length]. - let centered = fmod(t + half_period - half_length, period) - half_period; - // Signed distance for the dash, negative values are inside the dash. - let signed_distance = abs(centered) - half_length; - // Antialiased alpha based on the signed distance. - return saturate(antialias_threshold - signed_distance / dash_velocity); -} - -// This approximates distance to the nearest point to a quarter ellipse in a way -// that is sufficient for anti-aliasing when the ellipse is not very eccentric. -// The components of `point` are expected to be positive. -// -// Negative on the outside and positive on the inside. -fn quarter_ellipse_sdf(point: vec2, radii: vec2) -> f32 { - // Scale the space to treat the ellipse like a unit circle. - let circle_vec = point / radii; - let unit_circle_sdf = length(circle_vec) - 1.0; - // Approximate up-scaling of the length by using the average of the radii. - // - // TODO: A better solution would be to use the gradient of the implicit - // function for an ellipse to approximate a scaling factor. - return unit_circle_sdf * (radii.x + radii.y) * -0.5; -} - -// Modulus that has the same sign as `a`. -fn fmod(a: f32, b: f32) -> f32 { - return a - b * trunc(a / b); -} - -// --- shadows --- // - -struct Shadow { - order: u32, - blur_radius: f32, - // The shadow rect for drop shadows; the "hole" rect for inset shadows. - bounds: Bounds, - corner_radii: Corners, - content_mask: ContentMask, - color: Hsla, - // Only consulted when `inset == 1u`: the element's own bounds, used as a rounded-rect - // clip so the shadow never escapes the element. - element_bounds: Bounds, - element_corner_radii: Corners, - // 0 = drop shadow, 1 = inset shadow. - inset: u32, - pad: u32, // align to 8 bytes -} - -struct ShadowVarying { - @builtin(position) position: vec4, - @location(0) @interpolate(flat) color: vec4, - @location(1) @interpolate(flat) shadow_id: u32, - //TODO: use `clip_distance` once Naga supports it - @location(3) clip_distances: vec4, - @location(4) @interpolate(flat) clip_mask_bounds: vec4, - @location(5) @interpolate(flat) clip_mask_radii: vec4, - @location(6) @interpolate(flat) clip_index: u32, -} - -@vertex -fn vs_shadow(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> ShadowVarying { - let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); - var shadow = load_shadow(instance_id); - - var geometry: Bounds; - if (shadow.inset != 0u) { - geometry = shadow.element_bounds; - } else { - // Leave room for the gaussian tail outside the shadow rect. - let margin = 3.0 * shadow.blur_radius; - geometry = shadow.bounds; - geometry.origin -= vec2(margin); - geometry.size += 2.0 * vec2(margin); - } - - var out = ShadowVarying(); - out.position = to_device_position(unit_vertex, geometry); - out.color = hsla_to_rgba(shadow.color); - out.shadow_id = instance_id; - out.clip_mask_bounds = vec4(shadow.content_mask.bounds.origin, shadow.content_mask.bounds.size); - out.clip_index = shadow.content_mask.clip_index; - out.clip_mask_radii = vec4( - shadow.content_mask.corner_radii.top_left, - shadow.content_mask.corner_radii.top_right, - shadow.content_mask.corner_radii.bottom_right, - shadow.content_mask.corner_radii.bottom_left, - ); - out.clip_distances = distance_from_clip_rect(unit_vertex, geometry, shadow.content_mask); - return out; -} - -@fragment -fn fs_shadow(input: ShadowVarying) -> @location(0) vec4 { - let coverage = content_mask_coverage(input.position.xy, input.clip_mask_bounds, input.clip_mask_radii, input.clip_index); - // Alpha clip first, since we don't have `clip_distance`. - if (any(input.clip_distances < vec4(0.0)) - || coverage <= 0.0) { - return vec4(0.0); - } - - let shadow = load_shadow(input.shadow_id); - let half_size = shadow.bounds.size / 2.0; - let center = shadow.bounds.origin + half_size; - let center_to_point = input.position.xy - center; - - let corner_radius = pick_corner_radius(center_to_point, shadow.corner_radii); - - var alpha: f32; - if (shadow.blur_radius == 0.0) { - let distance = quad_sdf(input.position.xy, shadow.bounds, shadow.corner_radii); - alpha = saturate(0.5 - distance); - } else { - // The signal is only non-zero in a limited range, so don't waste samples - let low = center_to_point.y - half_size.y; - let high = center_to_point.y + half_size.y; - let start = clamp(-3.0 * shadow.blur_radius, low, high); - let end = clamp(3.0 * shadow.blur_radius, low, high); - - // Accumulate samples (we can get away with surprisingly few samples) - let step = (end - start) / 4.0; - var y = start + step * 0.5; - alpha = 0.0; - for (var i = 0; i < 4; i += 1) { - let blur = blur_along_x(center_to_point.x, center_to_point.y - y, - shadow.blur_radius, corner_radius, half_size); - alpha += blur * gaussian(y, shadow.blur_radius) * step; - y += step; - } - } - - if (shadow.inset != 0u) { - // The inset shadow is the complement of the (blurred) hole rect, clipped to the element. - // `saturate(0.5 - d)` gives a 1-pixel antialiased edge: d <= -0.5 -> 1, d >= 0.5 -> 0. - alpha = 1.0 - alpha; - let element_distance = quad_sdf(input.position.xy, shadow.element_bounds, - shadow.element_corner_radii); - alpha *= saturate(0.5 - element_distance); - } - - return blend_color(input.color, alpha * coverage); -} - -// --- path rasterization --- // - -struct PathRasterizationVertex { - xy_position: vec2, - st_position: vec2, - color: Background, - bounds: Bounds, - content_mask: ContentMask, -} - - - -struct PathRasterizationVarying { - @builtin(position) position: vec4, - @location(0) st_position: vec2, - @location(1) @interpolate(flat) vertex_id: u32, - //TODO: use `clip_distance` once Naga supports it - @location(3) clip_distances: vec4, - @location(4) @interpolate(flat) clip_mask_bounds: vec4, - @location(5) @interpolate(flat) clip_mask_radii: vec4, - @location(6) @interpolate(flat) clip_index: u32, -} - -@vertex -fn vs_path_rasterization(@builtin(vertex_index) vertex_id: u32) -> PathRasterizationVarying { - let v = load_path_vertex(vertex_id); - - var out = PathRasterizationVarying(); - out.position = to_device_position_impl(v.xy_position); - out.st_position = v.st_position; - out.vertex_id = vertex_id; - out.clip_distances = distance_from_clip_bounds_impl(v.xy_position, v.bounds); - out.clip_mask_bounds = vec4(v.content_mask.bounds.origin, v.content_mask.bounds.size); - out.clip_index = v.content_mask.clip_index; - out.clip_mask_radii = vec4( - v.content_mask.corner_radii.top_left, - v.content_mask.corner_radii.top_right, - v.content_mask.corner_radii.bottom_right, - v.content_mask.corner_radii.bottom_left, - ); - return out; -} - -@fragment -fn fs_path_rasterization(input: PathRasterizationVarying) -> @location(0) vec4 { - let coverage = content_mask_coverage(input.position.xy, input.clip_mask_bounds, input.clip_mask_radii, input.clip_index); - let dx = dpdx(input.st_position); - let dy = dpdy(input.st_position); - if (any(input.clip_distances < vec4(0.0)) - || coverage <= 0.0) { - return vec4(0.0); - } - - let v = load_path_vertex(input.vertex_id); - let background = v.color; - let bounds = v.bounds; - - var alpha: f32; - if (length(vec2(dx.x, dy.x)) < 0.001) { - // If the gradient is too small, return a solid color. - alpha = 1.0; - } else { - let gradient = 2.0 * input.st_position.xx * vec2(dx.x, dy.x) - vec2(dx.y, dy.y); - let f = input.st_position.x * input.st_position.x - input.st_position.y; - let distance = f / length(gradient); - alpha = saturate(0.5 - distance); - } - let prepared_gradient = prepare_gradient_color( - background.tag, - background.color_space, - background.solid, - background.colors, - ); - let color = gradient_color(background, input.position.xy, bounds, - prepared_gradient.solid, prepared_gradient.color0, prepared_gradient.color1); - return vec4(color.rgb * color.a * alpha, color.a * alpha) * coverage; -} - -// --- paths --- // - -struct PathSprite { - bounds: Bounds, -} - - -struct PathVarying { - @builtin(position) position: vec4, - @location(0) texture_coords: vec2, -} - -@vertex -fn vs_path(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> PathVarying { - let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); - let sprite = load_path_sprite(instance_id); - // Don't apply content mask because it was already accounted for when rasterizing the path. - let device_position = to_device_position(unit_vertex, sprite.bounds); - // For screen-space intermediate texture, convert screen position to texture coordinates - let screen_position = sprite.bounds.origin + unit_vertex * sprite.bounds.size; - let texture_coords = screen_position / globals.viewport_size; - - var out = PathVarying(); - out.position = device_position; - out.texture_coords = texture_coords; - - return out; -} - -@fragment -fn fs_path(input: PathVarying) -> @location(0) vec4 { - let sample = textureSample(t_sprite, s_sprite, input.texture_coords); - return sample; -} - -// --- underlines --- // - -struct Underline { - order: u32, - pad: u32, - bounds: Bounds, - content_mask: ContentMask, - color: Hsla, - thickness: f32, - wavy: u32, -} - - -struct UnderlineVarying { - @builtin(position) position: vec4, - @location(0) @interpolate(flat) color: vec4, - @location(1) @interpolate(flat) underline_id: u32, - //TODO: use `clip_distance` once Naga supports it - @location(3) clip_distances: vec4, - @location(4) @interpolate(flat) clip_mask_bounds: vec4, - @location(5) @interpolate(flat) clip_mask_radii: vec4, - @location(6) @interpolate(flat) clip_index: u32, -} - -@vertex -fn vs_underline(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> UnderlineVarying { - let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); - let underline = load_underline(instance_id); - - var out = UnderlineVarying(); - out.position = to_device_position(unit_vertex, underline.bounds); - out.color = hsla_to_rgba(underline.color); - out.underline_id = instance_id; - out.clip_mask_bounds = vec4(underline.content_mask.bounds.origin, underline.content_mask.bounds.size); - out.clip_index = underline.content_mask.clip_index; - out.clip_mask_radii = vec4( - underline.content_mask.corner_radii.top_left, - underline.content_mask.corner_radii.top_right, - underline.content_mask.corner_radii.bottom_right, - underline.content_mask.corner_radii.bottom_left, - ); - out.clip_distances = distance_from_clip_rect(unit_vertex, underline.bounds, underline.content_mask); - return out; -} - -@fragment -fn fs_underline(input: UnderlineVarying) -> @location(0) vec4 { - let coverage = content_mask_coverage(input.position.xy, input.clip_mask_bounds, input.clip_mask_radii, input.clip_index); - const WAVE_FREQUENCY: f32 = 2.0; - const WAVE_HEIGHT_RATIO: f32 = 0.8; - - // Alpha clip first, since we don't have `clip_distance`. - if (any(input.clip_distances < vec4(0.0)) - || coverage <= 0.0) { - return vec4(0.0); - } - - let underline = load_underline(input.underline_id); - if (underline.wavy == 0u) - { - return blend_color(input.color, input.color.a * coverage); - } - - let half_thickness = underline.thickness * 0.5; - - let st = (input.position.xy - underline.bounds.origin) / underline.bounds.size.y - vec2(0.0, 0.5); - let frequency = M_PI_F * WAVE_FREQUENCY * underline.thickness / underline.bounds.size.y; - let amplitude = (underline.thickness * WAVE_HEIGHT_RATIO) / underline.bounds.size.y; - - let sine = sin(st.x * frequency) * amplitude; - let dSine = cos(st.x * frequency) * amplitude * frequency; - let distance = (st.y - sine) / sqrt(1.0 + dSine * dSine); - let distance_in_pixels = distance * underline.bounds.size.y; - let distance_from_top_border = distance_in_pixels - half_thickness; - let distance_from_bottom_border = distance_in_pixels + half_thickness; - let alpha = saturate(0.5 - max(-distance_from_bottom_border, distance_from_top_border)); - return blend_color(input.color, alpha * input.color.a * coverage); -} - -// --- monochrome sprites --- // - -struct MonochromeSprite { - order: u32, - pad: u32, - bounds: Bounds, - content_mask: ContentMask, - color: Hsla, - tile: AtlasTile, - transformation: TransformationMatrix, -} - - -struct MonoSpriteVarying { - @builtin(position) position: vec4, - @location(0) tile_position: vec2, - @location(1) @interpolate(flat) color: vec4, - @location(3) clip_distances: vec4, - @location(4) @interpolate(flat) clip_mask_bounds: vec4, - @location(5) @interpolate(flat) clip_mask_radii: vec4, - @location(6) @interpolate(flat) clip_index: u32, -} - -@vertex -fn vs_mono_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> MonoSpriteVarying { - let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); - let sprite = load_mono_sprite(instance_id); - - var out = MonoSpriteVarying(); - out.position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation); - - out.tile_position = to_tile_position(unit_vertex, sprite.tile); - out.color = hsla_to_rgba(sprite.color); - out.clip_mask_bounds = vec4(sprite.content_mask.bounds.origin, sprite.content_mask.bounds.size); - out.clip_index = sprite.content_mask.clip_index; - out.clip_mask_radii = vec4( - sprite.content_mask.corner_radii.top_left, - sprite.content_mask.corner_radii.top_right, - sprite.content_mask.corner_radii.bottom_right, - sprite.content_mask.corner_radii.bottom_left, - ); - out.clip_distances = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation); - return out; -} - -@fragment -fn fs_mono_sprite(input: MonoSpriteVarying) -> @location(0) vec4 { - let coverage = content_mask_coverage(input.position.xy, input.clip_mask_bounds, input.clip_mask_radii, input.clip_index); - let sample = textureSample(t_sprite, s_sprite, input.tile_position).r; - let alpha_corrected = apply_contrast_and_gamma_correction(sample, input.color.rgb, gamma_params.grayscale_enhanced_contrast, gamma_params.gamma_ratios); - - // Alpha clip after using the derivatives. - if (any(input.clip_distances < vec4(0.0)) - || coverage <= 0.0) { - return vec4(0.0); - } - - return blend_color(input.color, alpha_corrected * coverage); -} - -// --- polychrome sprites --- // - -struct PolychromeSprite { - order: u32, - pad: u32, - grayscale: u32, - opacity: f32, - bounds: Bounds, - content_mask: ContentMask, - corner_radii: Corners, - tile: AtlasTile, -} - - -struct PolySpriteVarying { - @builtin(position) position: vec4, - @location(0) tile_position: vec2, - @location(1) @interpolate(flat) sprite_id: u32, - @location(3) clip_distances: vec4, - @location(4) @interpolate(flat) clip_mask_bounds: vec4, - @location(5) @interpolate(flat) clip_mask_radii: vec4, - @location(6) @interpolate(flat) clip_index: u32, -} - -@vertex -fn vs_poly_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> PolySpriteVarying { - let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); - let sprite = load_poly_sprite(instance_id); - - var out = PolySpriteVarying(); - out.position = to_device_position(unit_vertex, sprite.bounds); - out.tile_position = to_tile_position(unit_vertex, sprite.tile); - out.sprite_id = instance_id; - out.clip_mask_bounds = vec4(sprite.content_mask.bounds.origin, sprite.content_mask.bounds.size); - out.clip_index = sprite.content_mask.clip_index; - out.clip_mask_radii = vec4( - sprite.content_mask.corner_radii.top_left, - sprite.content_mask.corner_radii.top_right, - sprite.content_mask.corner_radii.bottom_right, - sprite.content_mask.corner_radii.bottom_left, - ); - out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask); - return out; -} - -@fragment -fn fs_poly_sprite(input: PolySpriteVarying) -> @location(0) vec4 { - let coverage = content_mask_coverage(input.position.xy, input.clip_mask_bounds, input.clip_mask_radii, input.clip_index); - let sample = textureSample(t_sprite, s_sprite, input.tile_position); - // Alpha clip after using the derivatives. - if (any(input.clip_distances < vec4(0.0)) - || coverage <= 0.0) { - return vec4(0.0); - } - - let sprite = load_poly_sprite(input.sprite_id); - let distance = quad_sdf(input.position.xy, sprite.bounds, sprite.corner_radii); - - var color = sample; - if (sprite.grayscale != 0u) { - let grayscale = dot(color.rgb, GRAYSCALE_FACTORS); - color = vec4(vec3(grayscale), sample.a); - } - return blend_color(color, sprite.opacity * min(coverage, saturate(0.5 - distance))); -} - -// --- surfaces --- // - -struct SurfaceParams { - bounds: Bounds, - @size(48) content_mask: ContentMask, -} - -@group(1) @binding(0) var surface_locals: SurfaceParams; -@group(1) @binding(1) var t_y: texture_2d; -@group(1) @binding(2) var t_cb_cr: texture_2d; -@group(1) @binding(3) var s_surface: sampler; - -const ycbcr_to_RGB = mat4x4( - vec4( 1.0000f, 1.0000f, 1.0000f, 0.0), - vec4( 0.0000f, -0.3441f, 1.7720f, 0.0), - vec4( 1.4020f, -0.7141f, 0.0000f, 0.0), - vec4(-0.7010f, 0.5291f, -0.8860f, 1.0), -); - -struct SurfaceVarying { - @builtin(position) position: vec4, - @location(0) texture_position: vec2, - @location(3) clip_distances: vec4, - @location(4) @interpolate(flat) clip_mask_bounds: vec4, - @location(5) @interpolate(flat) clip_mask_radii: vec4, - @location(6) @interpolate(flat) clip_index: u32, -} - -@vertex -fn vs_surface(@builtin(vertex_index) vertex_id: u32) -> SurfaceVarying { - let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); - - var out = SurfaceVarying(); - out.position = to_device_position(unit_vertex, surface_locals.bounds); - out.texture_position = unit_vertex; - out.clip_mask_bounds = vec4(surface_locals.content_mask.bounds.origin, surface_locals.content_mask.bounds.size); - out.clip_index = surface_locals.content_mask.clip_index; - out.clip_mask_radii = vec4( - surface_locals.content_mask.corner_radii.top_left, - surface_locals.content_mask.corner_radii.top_right, - surface_locals.content_mask.corner_radii.bottom_right, - surface_locals.content_mask.corner_radii.bottom_left, - ); - out.clip_distances = distance_from_clip_rect(unit_vertex, surface_locals.bounds, surface_locals.content_mask); - return out; -} - -@fragment -fn fs_surface(input: SurfaceVarying) -> @location(0) vec4 { - let coverage = content_mask_coverage(input.position.xy, input.clip_mask_bounds, input.clip_mask_radii, input.clip_index); - // Alpha clip after using the derivatives. - if (any(input.clip_distances < vec4(0.0)) - || coverage <= 0.0) { - return vec4(0.0); - } - - let y_cb_cr = vec4( - textureSampleLevel(t_y, s_surface, input.texture_position, 0.0).r, - textureSampleLevel(t_cb_cr, s_surface, input.texture_position, 0.0).rg, - 1.0); - - return blend_color(ycbcr_to_RGB * y_cb_cr, coverage); -} diff --git a/crates/gpui_pre_wgpu/src/shaders_storage.wgsl b/crates/gpui_pre_wgpu/src/shaders_storage.wgsl deleted file mode 100644 index 6f74e0e..0000000 --- a/crates/gpui_pre_wgpu/src/shaders_storage.wgsl +++ /dev/null @@ -1,48 +0,0 @@ -// Storage-buffer instance transport. Concatenated after `shaders.wgsl` on backends -// with storage buffer support; `shaders_webgl.wgsl` is the WebGL2 counterpart that -// decodes the same records from a texture instead. -// -// All buffers share `@group(1) @binding(0)` because each pipeline binds only the -// buffer its own entry points read. - -@group(1) @binding(0) var b_quads: array; -@group(1) @binding(0) var b_shadows: array; -@group(1) @binding(0) var b_path_vertices: array; -@group(1) @binding(0) var b_path_sprites: array; -@group(1) @binding(0) var b_underlines: array; -@group(1) @binding(0) var b_mono_sprites: array; -@group(1) @binding(0) var b_poly_sprites: array; - -fn load_quad(instance_id: u32) -> Quad { - return b_quads[instance_id]; -} - -fn load_shadow(instance_id: u32) -> Shadow { - return b_shadows[instance_id]; -} - -fn load_path_vertex(vertex_id: u32) -> PathRasterizationVertex { - return b_path_vertices[vertex_id]; -} - -fn load_path_sprite(instance_id: u32) -> PathSprite { - return b_path_sprites[instance_id]; -} - -fn load_underline(instance_id: u32) -> Underline { - return b_underlines[instance_id]; -} - -fn load_mono_sprite(instance_id: u32) -> MonochromeSprite { - return b_mono_sprites[instance_id]; -} - -fn load_poly_sprite(instance_id: u32) -> PolychromeSprite { - return b_poly_sprites[instance_id]; -} - -@group(3) @binding(0) var b_clips: array; - -fn load_clip(index: u32) -> RoundedClip { - return b_clips[index]; -} diff --git a/crates/gpui_pre_wgpu/src/shaders_subpixel.wgsl b/crates/gpui_pre_wgpu/src/shaders_subpixel.wgsl deleted file mode 100644 index 9121384..0000000 --- a/crates/gpui_pre_wgpu/src/shaders_subpixel.wgsl +++ /dev/null @@ -1,69 +0,0 @@ -// --- subpixel sprites --- // - -struct SubpixelSprite { - order: u32, - pad: u32, - bounds: Bounds, - content_mask: ContentMask, - color: Hsla, - tile: AtlasTile, - transformation: TransformationMatrix, -} -@group(1) @binding(0) var b_subpixel_sprites: array; - -struct SubpixelSpriteOutput { - @builtin(position) position: vec4, - @location(0) tile_position: vec2, - @location(1) @interpolate(flat) color: vec4, - @location(3) clip_distances: vec4, - @location(4) @interpolate(flat) clip_mask_bounds: vec4, - @location(5) @interpolate(flat) clip_mask_radii: vec4, - @location(6) @interpolate(flat) clip_index: u32, -} - -struct SubpixelSpriteFragmentOutput { - @location(0) @blend_src(0) foreground: vec4, - @location(0) @blend_src(1) alpha: vec4, -} - -@vertex -fn vs_subpixel_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> SubpixelSpriteOutput { - let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); - let sprite = b_subpixel_sprites[instance_id]; - - var out = SubpixelSpriteOutput(); - out.position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation); - out.tile_position = to_tile_position(unit_vertex, sprite.tile); - out.color = hsla_to_rgba(sprite.color); - out.clip_mask_bounds = vec4(sprite.content_mask.bounds.origin, sprite.content_mask.bounds.size); - out.clip_index = sprite.content_mask.clip_index; - out.clip_mask_radii = vec4( - sprite.content_mask.corner_radii.top_left, - sprite.content_mask.corner_radii.top_right, - sprite.content_mask.corner_radii.bottom_right, - sprite.content_mask.corner_radii.bottom_left, - ); - out.clip_distances = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation); - return out; -} - -@fragment -fn fs_subpixel_sprite(input: SubpixelSpriteOutput) -> SubpixelSpriteFragmentOutput { - let coverage = content_mask_coverage(input.position.xy, input.clip_mask_bounds, input.clip_mask_radii, input.clip_index); - var sample = textureSample(t_sprite, s_sprite, input.tile_position).rgb; - if (gamma_params.is_bgr != 0u) { - sample = sample.bgr; - } - let alpha_corrected = apply_contrast_and_gamma_correction3(sample, input.color.rgb, gamma_params.subpixel_enhanced_contrast, gamma_params.gamma_ratios); - - // Alpha clip after using the derivatives. - if (any(input.clip_distances < vec4(0.0)) - || coverage <= 0.0) { - return SubpixelSpriteFragmentOutput(vec4(0.0), vec4(0.0)); - } - - var out = SubpixelSpriteFragmentOutput(); - out.foreground = vec4(input.color.rgb, 1.0); - out.alpha = vec4(input.color.a * alpha_corrected * coverage, coverage); - return out; -} diff --git a/crates/gpui_pre_wgpu/src/shaders_webgl.wgsl b/crates/gpui_pre_wgpu/src/shaders_webgl.wgsl deleted file mode 100644 index d36d078..0000000 --- a/crates/gpui_pre_wgpu/src/shaders_webgl.wgsl +++ /dev/null @@ -1,265 +0,0 @@ -@group(1) @binding(0) var t_instances: texture_2d; -@group(3) @binding(0) var t_clips: texture_2d; - -// Each texel of `t_instances` packs four 32-bit words of instance data. Records -// are read strictly front to back, so all readers share a cursor that keeps the -// most recently fetched texel and only touches the texture again when the next -// word crosses a texel boundary. This fetches each texel exactly once per -// record load instead of once per word. The `read_*` functions must therefore -// consume their words in struct declaration order; skipping or re-reading words -// would still return correct data (the cursor re-fetches whenever the texel -// index changes) but would forfeit the single-fetch-per-texel guarantee. -struct InstanceCursor { - word_index: u32, - texel_index: u32, - texel: vec4, - width: u32, -} - -fn fetch_instance_texel(texel_index: u32, width: u32) -> vec4 { - let coordinate = vec2( - i32(texel_index % width), - i32(texel_index / width), - ); - return textureLoad(t_instances, coordinate, 0); -} - -fn instance_cursor(word_index: u32) -> InstanceCursor { - let width = textureDimensions(t_instances).x; - let texel_index = word_index / 4u; - return InstanceCursor( - word_index, - texel_index, - fetch_instance_texel(texel_index, width), - width, - ); -} - -fn read_word(cursor: ptr) -> u32 { - let word_index = (*cursor).word_index; - let texel_index = word_index / 4u; - if texel_index != (*cursor).texel_index { - (*cursor).texel = fetch_instance_texel(texel_index, (*cursor).width); - (*cursor).texel_index = texel_index; - } - (*cursor).word_index = word_index + 1u; - return (*cursor).texel[word_index % 4u]; -} - -fn read_f32(cursor: ptr) -> f32 { - return bitcast(read_word(cursor)); -} - -fn read_i32(cursor: ptr) -> i32 { - return bitcast(read_word(cursor)); -} - -fn read_vec2_f32(cursor: ptr) -> vec2 { - return vec2(read_f32(cursor), read_f32(cursor)); -} - -fn read_vec2_i32(cursor: ptr) -> vec2 { - return vec2(read_i32(cursor), read_i32(cursor)); -} - -fn read_hsla(cursor: ptr) -> Hsla { - return Hsla( - read_f32(cursor), - read_f32(cursor), - read_f32(cursor), - read_f32(cursor), - ); -} - -fn read_bounds(cursor: ptr) -> Bounds { - return Bounds(read_vec2_f32(cursor), read_vec2_f32(cursor)); -} - -fn read_content_mask(cursor: ptr) -> ContentMask { - return ContentMask(read_bounds(cursor), read_corners(cursor), read_word(cursor), read_word(cursor)); -} - -fn read_corners(cursor: ptr) -> Corners { - return Corners( - read_f32(cursor), - read_f32(cursor), - read_f32(cursor), - read_f32(cursor), - ); -} - -fn read_edges(cursor: ptr) -> Edges { - return Edges( - read_f32(cursor), - read_f32(cursor), - read_f32(cursor), - read_f32(cursor), - ); -} - -fn read_color_stop(cursor: ptr) -> LinearColorStop { - return LinearColorStop(read_hsla(cursor), read_f32(cursor)); -} - -fn read_background(cursor: ptr) -> Background { - return Background( - read_word(cursor), - read_word(cursor), - read_hsla(cursor), - read_f32(cursor), - array( - read_color_stop(cursor), - read_color_stop(cursor), - ), - read_word(cursor), - ); -} - -fn read_atlas_tile(cursor: ptr) -> AtlasTile { - return AtlasTile( - AtlasTextureId( - read_word(cursor), - read_word(cursor), - ), - read_word(cursor), - read_word(cursor), - AtlasBounds( - read_vec2_i32(cursor), - read_vec2_i32(cursor), - ), - ); -} - -fn read_transformation(cursor: ptr) -> TransformationMatrix { - return TransformationMatrix( - mat2x2( - read_vec2_f32(cursor), - read_vec2_f32(cursor), - ), - read_vec2_f32(cursor), - ); -} - -fn load_quad(instance_id: u32) -> Quad { - var cursor = instance_cursor(instance_id * 46u); - return Quad( - read_word(&cursor), - read_word(&cursor), - read_bounds(&cursor), - read_content_mask(&cursor), - read_background(&cursor), - read_hsla(&cursor), - read_corners(&cursor), - read_edges(&cursor), - ); -} - -fn load_shadow(instance_id: u32) -> Shadow { - var cursor = instance_cursor(instance_id * 34u); - return Shadow( - read_word(&cursor), - read_f32(&cursor), - read_bounds(&cursor), - read_corners(&cursor), - read_content_mask(&cursor), - read_hsla(&cursor), - read_bounds(&cursor), - read_corners(&cursor), - read_word(&cursor), - read_word(&cursor), - ); -} - -fn load_path_vertex(vertex_id: u32) -> PathRasterizationVertex { - var cursor = instance_cursor(vertex_id * 36u); - return PathRasterizationVertex( - read_vec2_f32(&cursor), - read_vec2_f32(&cursor), - read_background(&cursor), - read_bounds(&cursor), - read_content_mask(&cursor), - ); -} - -fn load_path_sprite(instance_id: u32) -> PathSprite { - var cursor = instance_cursor(instance_id * 4u); - return PathSprite(read_bounds(&cursor)); -} - -fn load_underline(instance_id: u32) -> Underline { - var cursor = instance_cursor(instance_id * 22u); - return Underline( - read_word(&cursor), - read_word(&cursor), - read_bounds(&cursor), - read_content_mask(&cursor), - read_hsla(&cursor), - read_f32(&cursor), - read_word(&cursor), - ); -} - -fn load_mono_sprite(instance_id: u32) -> MonochromeSprite { - var cursor = instance_cursor(instance_id * 34u); - return MonochromeSprite( - read_word(&cursor), - read_word(&cursor), - read_bounds(&cursor), - read_content_mask(&cursor), - read_hsla(&cursor), - read_atlas_tile(&cursor), - read_transformation(&cursor), - ); -} - -fn load_poly_sprite(instance_id: u32) -> PolychromeSprite { - var cursor = instance_cursor(instance_id * 30u); - return PolychromeSprite( - read_word(&cursor), - read_word(&cursor), - read_word(&cursor), - read_f32(&cursor), - read_bounds(&cursor), - read_content_mask(&cursor), - read_corners(&cursor), - read_atlas_tile(&cursor), - ); -} - -// Clip readers reference only group 3. Reusing the instance cursor would also -// make surface fragments require t_instances at their uniform-buffer binding. -fn fetch_clip_texel(index: u32, width: u32) -> vec4 { - return textureLoad(t_clips, vec2(i32(index % width), i32(index / width)), 0); -} - -fn load_clip(index: u32) -> RoundedClip { - let word_index = index * 14u; - let width = textureDimensions(t_clips).x; - let texel_index = word_index / 4u; - // A 14-word record starts at word 0 or 2 and always spans four texels. - let a = fetch_clip_texel(texel_index, width); - let b = fetch_clip_texel(texel_index + 1u, width); - let c = fetch_clip_texel(texel_index + 2u, width); - let d = fetch_clip_texel(texel_index + 3u, width); - var bounds_words = a; - var rx_words = b; - var ry_words = c; - var parent = d.x; - var padding = d.y; - if word_index % 4u == 2u { - bounds_words = vec4(a.zw, b.xy); - rx_words = vec4(b.zw, c.xy); - ry_words = vec4(c.zw, d.xy); - parent = d.z; - padding = d.w; - } - let bounds = bitcast>(bounds_words); - let rx = bitcast>(rx_words); - let ry = bitcast>(ry_words); - return RoundedClip( - Bounds(bounds.xy, bounds.zw), - Corners(rx.x, rx.y, rx.z, rx.w), - Corners(ry.x, ry.y, ry.z, ry.w), - parent, padding, - ); -} diff --git a/crates/gpui_pre_wgpu/src/wgpu_atlas.rs b/crates/gpui_pre_wgpu/src/wgpu_atlas.rs deleted file mode 100644 index 3cf71ac..0000000 --- a/crates/gpui_pre_wgpu/src/wgpu_atlas.rs +++ /dev/null @@ -1,527 +0,0 @@ -use anyhow::{Context as _, Result}; -use collections::FxHashMap; -use etagere::{BucketedAtlasAllocator, size2}; -use gpui::{ - AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTextureList, AtlasTile, Bounds, DevicePixels, - PlatformAtlas, Point, Size, -}; -use parking_lot::Mutex; -use std::{borrow::Cow, ops, sync::Arc}; - -use crate::WgpuContext; - -fn device_size_to_etagere(size: Size) -> etagere::Size { - size2(size.width.0, size.height.0) -} - -fn etagere_point_to_device(point: etagere::Point) -> Point { - Point { - x: DevicePixels(point.x), - y: DevicePixels(point.y), - } -} - -pub struct WgpuAtlas(Mutex); - -struct PendingUpload { - id: AtlasTextureId, - bounds: Bounds, - data: Vec, -} - -struct WgpuAtlasState { - device: Arc, - queue: Arc, - max_texture_size: u32, - color_texture_format: wgpu::TextureFormat, - storage: WgpuAtlasStorage, - tiles_by_key: FxHashMap, - pending_uploads: Vec, -} - -pub struct WgpuTextureInfo { - pub view: wgpu::TextureView, -} - -impl WgpuAtlas { - pub fn new( - device: Arc, - queue: Arc, - color_texture_format: wgpu::TextureFormat, - ) -> Self { - let max_texture_size = device.limits().max_texture_dimension_2d; - WgpuAtlas(Mutex::new(WgpuAtlasState { - device, - queue, - max_texture_size, - color_texture_format, - storage: WgpuAtlasStorage::default(), - tiles_by_key: Default::default(), - pending_uploads: Vec::new(), - })) - } - - pub fn from_context(context: &WgpuContext) -> Self { - Self::new( - context.device.clone(), - context.queue.clone(), - context.color_texture_format(), - ) - } - - pub fn before_frame(&self) { - let mut lock = self.0.lock(); - lock.flush_uploads(); - } - - pub fn get_texture_info(&self, id: AtlasTextureId) -> WgpuTextureInfo { - let lock = self.0.lock(); - let texture = &lock.storage[id]; - WgpuTextureInfo { - view: texture.view.clone(), - } - } - - /// Clears all cached textures and tiles, forcing them to be recreated. - /// Use this for incremental recovery when the device is still valid. - pub fn clear(&self) { - let mut lock = self.0.lock(); - lock.storage = WgpuAtlasStorage::default(); - lock.tiles_by_key.clear(); - lock.pending_uploads.clear(); - } - - /// Handles device lost by clearing all textures and cached tiles. - /// The atlas will lazily recreate textures as needed on subsequent frames. - pub fn handle_device_lost(&self, context: &WgpuContext) { - let mut lock = self.0.lock(); - lock.device = context.device.clone(); - lock.queue = context.queue.clone(); - lock.color_texture_format = context.color_texture_format(); - lock.storage = WgpuAtlasStorage::default(); - lock.tiles_by_key.clear(); - lock.pending_uploads.clear(); - } -} - -impl PlatformAtlas for WgpuAtlas { - fn get_or_insert_with<'a>( - &self, - key: &AtlasKey, - build: &mut dyn FnMut() -> Result, Cow<'a, [u8]>)>>, - ) -> Result> { - let mut lock = self.0.lock(); - if let Some(tile) = lock.tiles_by_key.get(key) { - Ok(Some(*tile)) - } else { - profiling::scope!("new tile"); - let Some((size, bytes)) = build()? else { - return Ok(None); - }; - let tile = lock - .allocate(size, key.texture_kind()) - .context("failed to allocate")?; - lock.upload_texture(tile.texture_id, tile.bounds, &bytes); - lock.tiles_by_key.insert(key.clone(), tile); - Ok(Some(tile)) - } - } - - fn remove(&self, key: &AtlasKey) { - let mut lock = self.0.lock(); - - let Some(tile) = lock.tiles_by_key.remove(key) else { - return; - }; - let id = tile.texture_id; - - let Some(texture_slot) = lock.storage[id.kind].textures.get_mut(id.index as usize) else { - return; - }; - - if let Some(mut texture) = texture_slot.take() { - texture.allocator.deallocate(tile.tile_id.into()); - texture.decrement_ref_count(); - if texture.is_unreferenced() { - lock.pending_uploads - .retain(|upload| upload.id != texture.id); - lock.storage[id.kind] - .free_list - .push(texture.id.index as usize); - } else { - *texture_slot = Some(texture); - } - } - } -} - -impl WgpuAtlasState { - fn allocate( - &mut self, - size: Size, - texture_kind: AtlasTextureKind, - ) -> Option { - { - let textures = &mut self.storage[texture_kind]; - - if let Some(tile) = textures - .iter_mut() - .rev() - .find_map(|texture| texture.allocate(size)) - { - return Some(tile); - } - } - - let texture = self.push_texture(size, texture_kind); - texture.allocate(size) - } - - fn push_texture( - &mut self, - min_size: Size, - kind: AtlasTextureKind, - ) -> &mut WgpuAtlasTexture { - const DEFAULT_ATLAS_SIZE: Size = Size { - width: DevicePixels(1024), - height: DevicePixels(1024), - }; - let max_texture_size = self.max_texture_size as i32; - let max_atlas_size = Size { - width: DevicePixels(max_texture_size), - height: DevicePixels(max_texture_size), - }; - - let size = min_size.min(&max_atlas_size).max(&DEFAULT_ATLAS_SIZE); - let format = match kind { - AtlasTextureKind::Monochrome => wgpu::TextureFormat::R8Unorm, - AtlasTextureKind::Subpixel | AtlasTextureKind::Polychrome => self.color_texture_format, - }; - - let texture = self.device.create_texture(&wgpu::TextureDescriptor { - label: Some("atlas"), - size: wgpu::Extent3d { - width: size.width.0 as u32, - height: size.height.0 as u32, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format, - usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, - view_formats: &[], - }); - - let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - - let texture_list = &mut self.storage[kind]; - let index = texture_list.free_list.pop(); - - let atlas_texture = WgpuAtlasTexture { - id: AtlasTextureId { - index: index.unwrap_or(texture_list.textures.len()) as u32, - kind, - }, - allocator: BucketedAtlasAllocator::new(device_size_to_etagere(size)), - format, - texture, - view, - live_atlas_keys: 0, - }; - - if let Some(ix) = index { - texture_list.textures[ix] = Some(atlas_texture); - texture_list - .textures - .get_mut(ix) - .and_then(|t| t.as_mut()) - .expect("texture must exist") - } else { - texture_list.textures.push(Some(atlas_texture)); - texture_list - .textures - .last_mut() - .and_then(|t| t.as_mut()) - .expect("texture must exist") - } - } - - fn upload_texture(&mut self, id: AtlasTextureId, bounds: Bounds, bytes: &[u8]) { - let data = self - .storage - .get(id) - .map(|texture| swizzle_upload_data(bytes, texture.format)) - .unwrap_or_else(|| bytes.to_vec()); - - self.pending_uploads - .push(PendingUpload { id, bounds, data }); - } - - fn flush_uploads(&mut self) { - for upload in self.pending_uploads.drain(..) { - let Some(texture) = self.storage.get(upload.id) else { - continue; - }; - let bytes_per_pixel = texture.bytes_per_pixel(); - - self.queue.write_texture( - wgpu::TexelCopyTextureInfo { - texture: &texture.texture, - mip_level: 0, - origin: wgpu::Origin3d { - x: upload.bounds.origin.x.0 as u32, - y: upload.bounds.origin.y.0 as u32, - z: 0, - }, - aspect: wgpu::TextureAspect::All, - }, - &upload.data, - wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(upload.bounds.size.width.0 as u32 * bytes_per_pixel as u32), - rows_per_image: None, - }, - wgpu::Extent3d { - width: upload.bounds.size.width.0 as u32, - height: upload.bounds.size.height.0 as u32, - depth_or_array_layers: 1, - }, - ); - } - } -} - -#[derive(Default)] -struct WgpuAtlasStorage { - monochrome_textures: AtlasTextureList, - subpixel_textures: AtlasTextureList, - polychrome_textures: AtlasTextureList, -} - -impl ops::Index for WgpuAtlasStorage { - type Output = AtlasTextureList; - fn index(&self, kind: AtlasTextureKind) -> &Self::Output { - match kind { - AtlasTextureKind::Monochrome => &self.monochrome_textures, - AtlasTextureKind::Subpixel => &self.subpixel_textures, - AtlasTextureKind::Polychrome => &self.polychrome_textures, - } - } -} - -impl ops::IndexMut for WgpuAtlasStorage { - fn index_mut(&mut self, kind: AtlasTextureKind) -> &mut Self::Output { - match kind { - AtlasTextureKind::Monochrome => &mut self.monochrome_textures, - AtlasTextureKind::Subpixel => &mut self.subpixel_textures, - AtlasTextureKind::Polychrome => &mut self.polychrome_textures, - } - } -} - -impl WgpuAtlasStorage { - fn get(&self, id: AtlasTextureId) -> Option<&WgpuAtlasTexture> { - self[id.kind] - .textures - .get(id.index as usize) - .and_then(|t| t.as_ref()) - } -} - -impl ops::Index for WgpuAtlasStorage { - type Output = WgpuAtlasTexture; - fn index(&self, id: AtlasTextureId) -> &Self::Output { - let textures = match id.kind { - AtlasTextureKind::Monochrome => &self.monochrome_textures, - AtlasTextureKind::Subpixel => &self.subpixel_textures, - AtlasTextureKind::Polychrome => &self.polychrome_textures, - }; - textures[id.index as usize] - .as_ref() - .expect("texture must exist") - } -} - -struct WgpuAtlasTexture { - id: AtlasTextureId, - allocator: BucketedAtlasAllocator, - texture: wgpu::Texture, - view: wgpu::TextureView, - format: wgpu::TextureFormat, - live_atlas_keys: u32, -} - -impl WgpuAtlasTexture { - fn allocate(&mut self, size: Size) -> Option { - let allocation = self.allocator.allocate(device_size_to_etagere(size))?; - let tile = AtlasTile { - texture_id: self.id, - tile_id: allocation.id.into(), - padding: 0, - bounds: Bounds { - origin: etagere_point_to_device(allocation.rectangle.min), - size, - }, - }; - self.live_atlas_keys += 1; - Some(tile) - } - - fn bytes_per_pixel(&self) -> u8 { - match self.format { - wgpu::TextureFormat::R8Unorm => 1, - wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Rgba8Unorm => 4, - _ => 4, - } - } - - fn decrement_ref_count(&mut self) { - self.live_atlas_keys -= 1; - } - - fn is_unreferenced(&self) -> bool { - self.live_atlas_keys == 0 - } -} - -fn swizzle_upload_data(bytes: &[u8], format: wgpu::TextureFormat) -> Vec { - match format { - wgpu::TextureFormat::Rgba8Unorm => { - let mut data = bytes.to_vec(); - for pixel in data.chunks_exact_mut(4) { - pixel.swap(0, 2); - } - data - } - _ => bytes.to_vec(), - } -} - -#[cfg(all(test, not(target_family = "wasm")))] -mod tests { - use super::*; - use gpui::block_on; - use gpui::{ImageId, RenderImageParams}; - use std::sync::Arc; - - fn test_device_and_queue() -> anyhow::Result<(Arc, Arc)> { - block_on(async { - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: wgpu::Backends::all(), - flags: wgpu::InstanceFlags::default(), - backend_options: wgpu::BackendOptions::default(), - memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(), - display: None, - }); - let adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::LowPower, - compatible_surface: None, - force_fallback_adapter: false, - }) - .await - .map_err(|error| anyhow::anyhow!("failed to request adapter: {error}"))?; - let (device, queue) = adapter - .request_device(&wgpu::DeviceDescriptor { - label: Some("wgpu_atlas_test_device"), - required_features: wgpu::Features::empty(), - required_limits: wgpu::Limits::downlevel_defaults() - .using_resolution(adapter.limits()) - .using_alignment(adapter.limits()), - memory_hints: wgpu::MemoryHints::MemoryUsage, - trace: wgpu::Trace::Off, - experimental_features: wgpu::ExperimentalFeatures::disabled(), - }) - .await - .map_err(|error| anyhow::anyhow!("failed to request device: {error}"))?; - Ok((Arc::new(device), Arc::new(queue))) - }) - } - - #[test] - fn before_frame_skips_uploads_for_removed_texture() -> anyhow::Result<()> { - let (device, queue) = test_device_and_queue()?; - - let atlas = WgpuAtlas::new(device, queue, wgpu::TextureFormat::Bgra8Unorm); - let key = AtlasKey::Image(RenderImageParams { - image_id: ImageId(1), - frame_index: 0, - }); - let size = Size { - width: DevicePixels(1), - height: DevicePixels(1), - }; - let mut build = || Ok(Some((size, Cow::Owned(vec![0, 0, 0, 255])))); - - // Regression test: before the fix, this panicked in flush_uploads - atlas - .get_or_insert_with(&key, &mut build)? - .expect("tile should be created"); - atlas.remove(&key); - atlas.before_frame(); - Ok(()) - } - - #[test] - fn remove_deallocates_tile_space_for_reuse() -> anyhow::Result<()> { - let (device, queue) = test_device_and_queue()?; - let atlas = WgpuAtlas::new(device, queue, wgpu::TextureFormat::Bgra8Unorm); - - let small = Size { - width: DevicePixels(64), - height: DevicePixels(64), - }; - let big = Size { - width: DevicePixels(700), - height: DevicePixels(700), - }; - - let make_key = |image_id: usize| { - AtlasKey::Image(RenderImageParams { - image_id: ImageId(image_id), - frame_index: 0, - }) - }; - let insert = |key: &AtlasKey, size: Size| { - let byte_count = (size.width.0 as usize) * (size.height.0 as usize) * 4; - atlas - .get_or_insert_with(key, &mut || { - Ok(Some((size, Cow::Owned(vec![0u8; byte_count])))) - }) - .expect("allocation should succeed") - .expect("callback returns Some") - }; - - let keeper_key = make_key(1); - let big_key_a = make_key(2); - let big_key_b = make_key(3); - - let keeper_tile = insert(&keeper_key, small); - let tile_a = insert(&big_key_a, big); - assert_eq!(keeper_tile.texture_id, tile_a.texture_id); - - atlas.remove(&big_key_a); - let tile_b = insert(&big_key_b, big); - assert_eq!(tile_b.texture_id, keeper_tile.texture_id); - Ok(()) - } - - #[test] - fn swizzle_upload_data_preserves_bgra_uploads() { - let input = vec![0x10, 0x20, 0x30, 0x40]; - assert_eq!( - swizzle_upload_data(&input, wgpu::TextureFormat::Bgra8Unorm), - input - ); - } - - #[test] - fn swizzle_upload_data_converts_bgra_to_rgba() { - let input = vec![0x10, 0x20, 0x30, 0x40, 0xAA, 0xBB, 0xCC, 0xDD]; - assert_eq!( - swizzle_upload_data(&input, wgpu::TextureFormat::Rgba8Unorm), - vec![0x30, 0x20, 0x10, 0x40, 0xCC, 0xBB, 0xAA, 0xDD] - ); - } -} diff --git a/crates/gpui_pre_wgpu/src/wgpu_context.rs b/crates/gpui_pre_wgpu/src/wgpu_context.rs deleted file mode 100644 index 7e744e8..0000000 --- a/crates/gpui_pre_wgpu/src/wgpu_context.rs +++ /dev/null @@ -1,606 +0,0 @@ -#[cfg(not(target_family = "wasm"))] -use anyhow::Context as _; -#[cfg(not(target_family = "wasm"))] -use gpui_util::ResultExt; -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; -use wgpu::TextureFormat; - -pub struct WgpuContext { - pub instance: wgpu::Instance, - pub adapter: wgpu::Adapter, - pub device: Arc, - pub queue: Arc, - backend: WgpuBackend, - dual_source_blending: bool, - color_texture_format: wgpu::TextureFormat, - device_lost: Arc, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum WgpuBackend { - BrowserWebGpu, - Gl, - Native(wgpu::Backend), -} - -#[cfg(target_family = "wasm")] -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -pub enum WebBackendPreference { - #[default] - Auto, - WebGpu, - WebGl, -} - -#[cfg(target_family = "wasm")] -pub struct PreparedWebGraphics { - pub context: WgpuContext, - pub surface: wgpu::Surface<'static>, -} - -/// wgpu-core refuses to create a surface when neither the instance nor the surface -/// target carries a display handle, and `SurfaceTarget::Canvas` always passes `None`. -/// The WebGL2 backend never reads the handle (WebGPU bypasses wgpu-core entirely), so -/// a unit web display handle on the instance satisfies the check. -#[cfg(target_family = "wasm")] -#[derive(Debug)] -struct WebDisplaySource; - -#[cfg(target_family = "wasm")] -impl raw_window_handle::HasDisplayHandle for WebDisplaySource { - fn display_handle( - &self, - ) -> Result, raw_window_handle::HandleError> { - Ok(raw_window_handle::DisplayHandle::web()) - } -} - -#[derive(Clone, Copy)] -pub struct CompositorGpuHint { - pub vendor_id: u32, - pub device_id: u32, -} - -impl WgpuContext { - #[cfg(not(target_family = "wasm"))] - pub fn new( - instance: wgpu::Instance, - surface: &wgpu::Surface<'_>, - compositor_gpu: Option, - ) -> anyhow::Result { - Self::new_with_options(instance, surface, compositor_gpu, false) - } - - #[cfg(not(target_family = "wasm"))] - pub fn new_rejecting_software( - instance: wgpu::Instance, - surface: &wgpu::Surface<'_>, - compositor_gpu: Option, - ) -> anyhow::Result { - Self::new_with_options(instance, surface, compositor_gpu, true) - } - - #[cfg(not(target_family = "wasm"))] - fn new_with_options( - instance: wgpu::Instance, - surface: &wgpu::Surface<'_>, - compositor_gpu: Option, - reject_software: bool, - ) -> anyhow::Result { - let device_id_filter = match std::env::var("ZED_DEVICE_ID") { - Ok(val) => parse_pci_id(&val) - .context("Failed to parse device ID from `ZED_DEVICE_ID` environment variable") - .log_err(), - Err(std::env::VarError::NotPresent) => None, - err => { - err.context("Failed to read value of `ZED_DEVICE_ID` environment variable") - .log_err(); - None - } - }; - - // Select an adapter by actually testing surface configuration with the real device. - // This is the only reliable way to determine compatibility on hybrid GPU systems. - let (adapter, device, queue, dual_source_blending, color_texture_format) = - gpui::block_on(Self::select_adapter_and_device( - &instance, - device_id_filter, - surface, - compositor_gpu.as_ref(), - reject_software, - ))?; - - let device_lost = Arc::new(AtomicBool::new(false)); - device.set_device_lost_callback({ - let device_lost = Arc::clone(&device_lost); - move |reason, message| { - log::error!("wgpu device lost: reason={reason:?}, message={message}"); - if reason != wgpu::DeviceLostReason::Destroyed { - device_lost.store(true, Ordering::Relaxed); - } - } - }); - - log::info!( - "Selected GPU adapter: {:?} ({:?})", - adapter.get_info().name, - adapter.get_info().backend - ); - - let backend = WgpuBackend::Native(adapter.get_info().backend); - Ok(Self { - instance, - adapter, - device: Arc::new(device), - queue: Arc::new(queue), - backend, - dual_source_blending, - color_texture_format, - device_lost, - }) - } - - #[cfg(target_family = "wasm")] - pub async fn new_web( - canvas: &web_sys::HtmlCanvasElement, - preference: WebBackendPreference, - ) -> anyhow::Result { - Self::new_web_with_backend(canvas, preference).await - } - - #[cfg(target_family = "wasm")] - #[allow(clippy::arc_with_non_send_sync)] - async fn new_web_with_backend( - canvas: &web_sys::HtmlCanvasElement, - preference: WebBackendPreference, - ) -> anyhow::Result { - let backends = match preference { - WebBackendPreference::Auto => wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL, - WebBackendPreference::WebGpu => wgpu::Backends::BROWSER_WEBGPU, - WebBackendPreference::WebGl => wgpu::Backends::GL, - }; - let descriptor = wgpu::InstanceDescriptor { - backends, - flags: wgpu::InstanceFlags::default(), - backend_options: wgpu::BackendOptions::default(), - memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(), - display: Some(Box::new(WebDisplaySource)), - }; - let instance = if preference == WebBackendPreference::Auto { - wgpu::util::new_instance_with_webgpu_detection(descriptor).await - } else { - wgpu::Instance::new(descriptor) - }; - let surface = instance - .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone())) - .map_err(|error| { - anyhow::anyhow!("Failed to create browser graphics surface: {error}") - })?; - - let adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::HighPerformance, - compatible_surface: Some(&surface), - force_fallback_adapter: false, - }) - .await - .map_err(|error| { - anyhow::anyhow!( - "Failed to request a {preference:?} adapter compatible with the canvas: {error}" - ) - })?; - let adapter_info = adapter.get_info(); - let backend = match adapter_info.backend { - wgpu::Backend::BrowserWebGpu => WgpuBackend::BrowserWebGpu, - wgpu::Backend::Gl => WgpuBackend::Gl, - backend => { - anyhow::bail!( - "Browser graphics initialization selected unexpected backend {backend:?}" - ) - } - }; - - let device_lost = Arc::new(AtomicBool::new(false)); - let (device, queue, dual_source_blending, color_texture_format) = - Self::create_device(&adapter).await?; - device.set_device_lost_callback({ - let device_lost = Arc::clone(&device_lost); - move |reason, message| { - log::error!("wgpu device lost: reason={reason:?}, message={message}"); - if reason != wgpu::DeviceLostReason::Destroyed { - device_lost.store(true, Ordering::Relaxed); - } - } - }); - log::info!( - "Browser graphics initialized: requested={preference:?}, selected={backend:?}, \ - adapter={:?}, limits={:?}, dual_source_blending={dual_source_blending}", - adapter_info.name, - device.limits(), - ); - - let context = Self { - instance, - adapter, - device: Arc::new(device), - queue: Arc::new(queue), - backend, - dual_source_blending, - color_texture_format, - device_lost, - }; - Ok(PreparedWebGraphics { context, surface }) - } - - async fn create_device( - adapter: &wgpu::Adapter, - ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> { - let dual_source_blending = adapter - .features() - .contains(wgpu::Features::DUAL_SOURCE_BLENDING); - - let mut required_features = wgpu::Features::empty(); - if dual_source_blending { - required_features |= wgpu::Features::DUAL_SOURCE_BLENDING; - } else { - log::warn!( - "Dual-source blending not available on this GPU. \ - Subpixel text antialiasing will be disabled." - ); - } - - let color_atlas_texture_format = Self::select_color_texture_format(adapter)?; - #[cfg(target_family = "wasm")] - let required_limits = if adapter.get_info().backend == wgpu::Backend::Gl { - wgpu::Limits::downlevel_webgl2_defaults() - .using_resolution(adapter.limits()) - .using_alignment(adapter.limits()) - } else { - wgpu::Limits::downlevel_defaults() - .using_resolution(adapter.limits()) - .using_alignment(adapter.limits()) - }; - #[cfg(not(target_family = "wasm"))] - let required_limits = wgpu::Limits::downlevel_defaults() - .using_resolution(adapter.limits()) - .using_alignment(adapter.limits()); - - let (device, queue) = adapter - .request_device(&wgpu::DeviceDescriptor { - label: Some("gpui_device"), - required_features, - required_limits, - memory_hints: wgpu::MemoryHints::MemoryUsage, - trace: wgpu::Trace::Off, - experimental_features: wgpu::ExperimentalFeatures::disabled(), - }) - .await - .map_err(|e| anyhow::anyhow!("Failed to create wgpu device: {e}"))?; - - Ok(( - device, - queue, - dual_source_blending, - color_atlas_texture_format, - )) - } - - #[cfg(not(target_family = "wasm"))] - pub fn instance(display: Box) -> wgpu::Instance { - wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: wgpu::Backends::VULKAN | wgpu::Backends::GL, - flags: wgpu::InstanceFlags::default(), - backend_options: wgpu::BackendOptions::default(), - memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(), - display: Some(display), - }) - } - - pub fn check_compatible_with_surface(&self, surface: &wgpu::Surface<'_>) -> anyhow::Result<()> { - let caps = surface.get_capabilities(&self.adapter); - if caps.formats.is_empty() { - let info = self.adapter.get_info(); - anyhow::bail!( - "Adapter {:?} (backend={:?}, device={:#06x}) is not compatible with the \ - display surface for this window.", - info.name, - info.backend, - info.device, - ); - } - Ok(()) - } - - /// Select an adapter and create a device, testing that the surface can actually be configured. - /// This is the only reliable way to determine compatibility on hybrid GPU systems, where - /// adapters may report surface compatibility via get_capabilities() but fail when actually - /// configuring (e.g., NVIDIA reporting Vulkan Wayland support but failing because the - /// Wayland compositor runs on the Intel GPU). - #[cfg(not(target_family = "wasm"))] - async fn select_adapter_and_device( - instance: &wgpu::Instance, - device_id_filter: Option, - surface: &wgpu::Surface<'_>, - compositor_gpu: Option<&CompositorGpuHint>, - reject_software: bool, - ) -> anyhow::Result<( - wgpu::Adapter, - wgpu::Device, - wgpu::Queue, - bool, - TextureFormat, - )> { - let mut adapters: Vec<_> = instance.enumerate_adapters(wgpu::Backends::all()).await; - - if adapters.is_empty() { - anyhow::bail!("No GPU adapters found"); - } - - if let Some(device_id) = device_id_filter { - log::info!("ZED_DEVICE_ID filter: {:#06x}", device_id); - } - - // Sort adapters into a single priority order. Tiers (from highest to lowest): - // - // 1. ZED_DEVICE_ID match — explicit user override - // 2. Compositor GPU match — the GPU the display server is rendering on - // 3. Device type (Discrete > Integrated > Other > Virtual > Cpu). - // "Other" ranks above "Virtual" because OpenGL seems to count as "Other". - // 4. Backend — prefer Vulkan/Metal/Dx12 over GL/etc. - adapters.sort_by_key(|adapter| { - let info = adapter.get_info(); - - // Backends like OpenGL report device=0 for all adapters, so - // device-based matching is only meaningful when non-zero. - let device_known = info.device != 0; - - let user_override: u8 = match device_id_filter { - Some(id) if device_known && info.device == id => 0, - _ => 1, - }; - - let compositor_match: u8 = match compositor_gpu { - Some(hint) - if device_known - && info.vendor == hint.vendor_id - && info.device == hint.device_id => - { - 0 - } - _ => 1, - }; - - let type_priority: u8 = if info.device_type == wgpu::DeviceType::Cpu { - 4 - } else { - match info.device_type { - wgpu::DeviceType::DiscreteGpu => 0, - wgpu::DeviceType::IntegratedGpu => 1, - wgpu::DeviceType::Other => 2, - wgpu::DeviceType::VirtualGpu => 3, - wgpu::DeviceType::Cpu => 4, - } - }; - - let backend_priority: u8 = match info.backend { - wgpu::Backend::Vulkan | wgpu::Backend::Metal | wgpu::Backend::Dx12 => 0, - _ => 1, - }; - - ( - user_override, - compositor_match, - type_priority, - backend_priority, - ) - }); - - // Log all available adapters (in sorted order) - log::info!("Found {} GPU adapter(s):", adapters.len()); - for adapter in &adapters { - let info = adapter.get_info(); - log::info!( - " - {} (vendor={:#06x}, device={:#06x}, backend={:?}, type={:?})", - info.name, - info.vendor, - info.device, - info.backend, - info.device_type, - ); - } - - // Test each adapter by creating a device and configuring the surface - for adapter in adapters { - let info = adapter.get_info(); - - if reject_software && info.device_type == wgpu::DeviceType::Cpu { - log::info!( - "Skipping software renderer: {} ({:?})", - info.name, - info.backend - ); - continue; - } - - log::info!("Testing adapter: {} ({:?})...", info.name, info.backend); - - match Self::try_adapter_with_surface(&adapter, surface).await { - Ok((device, queue, dual_source_blending, color_atlas_texture_format)) => { - log::info!( - "Selected GPU (passed configuration test): {} ({:?})", - info.name, - info.backend - ); - return Ok(( - adapter, - device, - queue, - dual_source_blending, - color_atlas_texture_format, - )); - } - Err(e) => { - log::info!( - " Adapter {} ({:?}) failed: {}, trying next...", - info.name, - info.backend, - e - ); - } - } - } - - anyhow::bail!("No GPU adapter found that can configure the display surface") - } - - /// Try to use an adapter with a surface by creating a device and testing configuration. - /// Returns the device and queue if successful, allowing them to be reused. - #[cfg(not(target_family = "wasm"))] - async fn try_adapter_with_surface( - adapter: &wgpu::Adapter, - surface: &wgpu::Surface<'_>, - ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> { - let caps = surface.get_capabilities(adapter); - if caps.formats.is_empty() { - anyhow::bail!("no compatible surface formats"); - } - if caps.alpha_modes.is_empty() { - anyhow::bail!("no compatible alpha modes"); - } - - let (device, queue, dual_source_blending, color_atlas_texture_format) = - Self::create_device(adapter).await?; - let error_scope = device.push_error_scope(wgpu::ErrorFilter::Validation); - - let test_config = wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: caps.formats[0], - width: 64, - height: 64, - present_mode: wgpu::PresentMode::Fifo, - desired_maximum_frame_latency: 2, - alpha_mode: caps.alpha_modes[0], - view_formats: vec![], - }; - - surface.configure(&device, &test_config); - - let error = error_scope.pop().await; - if let Some(e) = error { - anyhow::bail!("surface configuration failed: {e}"); - } - - Ok(( - device, - queue, - dual_source_blending, - color_atlas_texture_format, - )) - } - - fn select_color_texture_format(adapter: &wgpu::Adapter) -> anyhow::Result { - let required_usages = wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST; - let bgra_features = adapter.get_texture_format_features(wgpu::TextureFormat::Bgra8Unorm); - let rgba_features = adapter.get_texture_format_features(wgpu::TextureFormat::Rgba8Unorm); - #[cfg(target_family = "wasm")] - if adapter.get_info().backend == wgpu::Backend::Gl - && rgba_features.allowed_usages.contains(required_usages) - { - return Ok(wgpu::TextureFormat::Rgba8Unorm); - } - if bgra_features.allowed_usages.contains(required_usages) { - return Ok(wgpu::TextureFormat::Bgra8Unorm); - } - if rgba_features.allowed_usages.contains(required_usages) { - let info = adapter.get_info(); - log::warn!( - "Adapter {} ({:?}) does not support Bgra8Unorm atlas textures with usages {:?}; \ - falling back to Rgba8Unorm atlas textures.", - info.name, - info.backend, - required_usages, - ); - return Ok(wgpu::TextureFormat::Rgba8Unorm); - } - - let info = adapter.get_info(); - Err(anyhow::anyhow!( - "Adapter {} ({:?}, device={:#06x}) does not support a usable color atlas texture \ - format with usages {:?}. Bgra8Unorm allowed usages: {:?}; \ - Rgba8Unorm allowed usages: {:?}.", - info.name, - info.backend, - info.device, - required_usages, - bgra_features.allowed_usages, - rgba_features.allowed_usages, - )) - } - pub fn backend(&self) -> WgpuBackend { - self.backend - } - - pub fn uses_webgl_instance_data(&self) -> bool { - matches!(self.backend, WgpuBackend::Gl) && cfg!(target_family = "wasm") - } - - pub fn supports_dual_source_blending(&self) -> bool { - self.dual_source_blending - } - - pub fn color_texture_format(&self) -> wgpu::TextureFormat { - self.color_texture_format - } - - /// Returns true if the GPU device was lost (e.g., due to driver crash, suspend/resume). - /// When this returns true, the context should be recreated. - pub fn device_lost(&self) -> bool { - self.device_lost.load(Ordering::Relaxed) - } - - /// Returns a clone of the device_lost flag for sharing with renderers. - pub(crate) fn device_lost_flag(&self) -> Arc { - Arc::clone(&self.device_lost) - } -} - -#[cfg(not(target_family = "wasm"))] -fn parse_pci_id(id: &str) -> anyhow::Result { - let mut id = id.trim(); - - if id.starts_with("0x") || id.starts_with("0X") { - id = &id[2..]; - } - let is_hex_string = id.chars().all(|c| c.is_ascii_hexdigit()); - let is_4_chars = id.len() == 4; - anyhow::ensure!( - is_4_chars && is_hex_string, - "Expected a 4 digit PCI ID in hexadecimal format" - ); - - u32::from_str_radix(id, 16).context("parsing PCI ID as hex") -} - -#[cfg(test)] -mod tests { - use super::parse_pci_id; - - #[test] - fn test_parse_device_id() { - assert!(parse_pci_id("0xABCD").is_ok()); - assert!(parse_pci_id("ABCD").is_ok()); - assert!(parse_pci_id("abcd").is_ok()); - assert!(parse_pci_id("1234").is_ok()); - assert!(parse_pci_id("123").is_err()); - assert_eq!( - parse_pci_id(&format!("{:x}", 0x1234)).unwrap(), - parse_pci_id(&format!("{:X}", 0x1234)).unwrap(), - ); - - assert_eq!( - parse_pci_id(&format!("{:#x}", 0x1234)).unwrap(), - parse_pci_id(&format!("{:#X}", 0x1234)).unwrap(), - ); - } -} diff --git a/crates/gpui_pre_wgpu/src/wgpu_renderer.rs b/crates/gpui_pre_wgpu/src/wgpu_renderer.rs deleted file mode 100644 index b7a72b4..0000000 --- a/crates/gpui_pre_wgpu/src/wgpu_renderer.rs +++ /dev/null @@ -1,2261 +0,0 @@ -use crate::{CompositorGpuHint, WgpuAtlas, WgpuContext}; -use anyhow::{Context as _, Result}; -use bytemuck::{Pod, Zeroable}; -use gpui::{ - get_gamma_correction_ratios, AtlasTextureId, Background, Bounds, ContentMask, Corners, - DevicePixels, GpuSpecs, Path, Point, PrimitiveBatch, ScaledPixels, Scene, Size, -}; -use log::warn; -#[cfg(not(target_family = "wasm"))] -use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; -use std::cell::RefCell; -use std::num::NonZeroU64; -use std::ops::Range; -use std::rc::Rc; -use std::sync::{Arc, Mutex}; - -const MAX_INSTANCE_BUFFER_SIZE: u64 = 256 * 1024 * 1024; - -const INSTANCE_TEXTURE_TEXEL_SIZE: u64 = 16; - -use crate::shaders::{STORAGE_BUFFER_SHADERS, SUBPIXEL_SHADERS, WEBGL_SHADERS}; - -fn least_common_multiple(left: u64, right: u64) -> u64 { - let mut first = left; - let mut second = right; - while second != 0 { - let remainder = first % second; - first = second; - second = remainder; - } - left / first * right -} - -#[repr(C)] -#[derive(Clone, Copy, Pod, Zeroable)] -struct GlobalParams { - viewport_size: [f32; 2], - premultiplied_alpha: u32, - pad: u32, -} - -#[repr(C)] -#[derive(Clone, Copy, Pod, Zeroable)] -struct PodBounds { - origin: [f32; 2], - size: [f32; 2], -} - -impl From> for PodBounds { - fn from(bounds: Bounds) -> Self { - Self { - origin: [bounds.origin.x.0, bounds.origin.y.0], - size: [bounds.size.width.0, bounds.size.height.0], - } - } -} - -#[repr(C)] -#[derive(Clone, Copy, Pod, Zeroable)] -struct PodCorners { - top_left: f32, - top_right: f32, - bottom_right: f32, - bottom_left: f32, -} - -impl From> for PodCorners { - fn from(corners: Corners) -> Self { - Self { - top_left: corners.top_left.0, - top_right: corners.top_right.0, - bottom_right: corners.bottom_right.0, - bottom_left: corners.bottom_left.0, - } - } -} - -#[repr(C)] -#[derive(Clone, Copy, Pod, Zeroable)] -struct PodContentMask { - bounds: PodBounds, - corner_radii: PodCorners, - clip_index: u32, - clip_padding: u32, -} - -impl From> for PodContentMask { - fn from(mask: ContentMask) -> Self { - Self { - bounds: mask.bounds.into(), - corner_radii: mask.corner_radii.into(), - clip_index: mask.clip_index, - clip_padding: 0, - } - } -} - -#[repr(C)] -#[derive(Clone, Copy, Pod, Zeroable)] -struct SurfaceParams { - bounds: PodBounds, - content_mask: PodContentMask, - padding: [u32; 2], -} - -#[repr(C)] -#[derive(Clone, Copy, Pod, Zeroable)] -struct GammaParams { - gamma_ratios: [f32; 4], - grayscale_enhanced_contrast: f32, - subpixel_enhanced_contrast: f32, - is_bgr: u32, - _pad: u32, -} - -#[derive(Clone, Debug)] -#[repr(C)] -struct PathSprite { - bounds: Bounds, -} - -#[derive(Clone, Debug)] -#[repr(C)] -struct PathRasterizationVertex { - xy_position: Point, - st_position: Point, - color: Background, - bounds: Bounds, - content_mask: ContentMask, -} - -pub struct WgpuSurfaceConfig { - pub size: Size, - pub transparent: bool, - /// Preferred presentation mode. When `Some`, the renderer will use this - /// mode if supported by the surface, falling back to `Fifo`. - /// When `None`, defaults to `Fifo` (VSync). - /// - /// Mobile platforms may prefer `Mailbox` (triple-buffering) to avoid - /// blocking in `get_current_texture()` during lifecycle transitions. - pub preferred_present_mode: Option, -} - -struct WgpuPipelines { - quads: wgpu::RenderPipeline, - shadows: wgpu::RenderPipeline, - path_rasterization: wgpu::RenderPipeline, - paths: wgpu::RenderPipeline, - underlines: wgpu::RenderPipeline, - mono_sprites: wgpu::RenderPipeline, - subpixel_sprites: Option, - poly_sprites: wgpu::RenderPipeline, - #[allow(dead_code)] - surfaces: wgpu::RenderPipeline, -} - -/// One frame allocation of instance data, ready to bind. -struct InstanceBinding { - bind_group: wgpu::BindGroup, - /// Index of the allocation's first instance within the bound data. Always - /// zero on the storage-buffer path, where the binding offset already - /// positions the array; on the WebGL texture path the shader indexes the - /// shared instance texture absolutely, so draws must offset their - /// instance (or vertex) ranges by this value. - first_instance: u32, -} - -struct InstanceBindings { - clips: InstanceBinding, - quads: InstanceBinding, - shadows: InstanceBinding, - underlines: InstanceBinding, - monochrome_sprites: InstanceBinding, - subpixel_sprites: InstanceBinding, - polychrome_sprites: InstanceBinding, -} - -struct WgpuBindGroupLayouts { - globals: wgpu::BindGroupLayout, - instances: wgpu::BindGroupLayout, - texture: wgpu::BindGroupLayout, - surfaces: wgpu::BindGroupLayout, -} - -/// Shared GPU context reference, used to coordinate device recovery across multiple windows. -pub type GpuContext = Rc>>; - -enum InstanceData { - Storage(wgpu::Buffer), - // WebGL2 has no storage buffers. A uint texture keeps the records available to both shader - // stages while preserving integer and floating-point bit patterns exactly. - Texture { - texture: wgpu::Texture, - view: wgpu::TextureView, - width: u32, - height: u32, - }, -} - -/// GPU resources that must be dropped together during device recovery. -struct WgpuResources { - device: Arc, - queue: Arc, - surface: wgpu::Surface<'static>, - pipelines: WgpuPipelines, - bind_group_layouts: WgpuBindGroupLayouts, - atlas_sampler: wgpu::Sampler, - globals_buffer: wgpu::Buffer, - globals_bind_group: wgpu::BindGroup, - path_globals_bind_group: wgpu::BindGroup, - instance_data: InstanceData, - path_intermediate_texture: Option, - path_intermediate_view: Option, - path_msaa_texture: Option, - path_msaa_view: Option, -} - -impl WgpuResources { - fn invalidate_intermediate_textures(&mut self) { - self.path_intermediate_texture = None; - self.path_intermediate_view = None; - self.path_msaa_texture = None; - self.path_msaa_view = None; - } -} - -pub struct WgpuRenderer { - /// Shared GPU context for device recovery coordination (unused on WASM). - #[allow(dead_code)] - context: Option, - /// Compositor GPU hint for adapter selection (unused on WASM). - #[allow(dead_code)] - compositor_gpu: Option, - resources: Option, - surface_config: wgpu::SurfaceConfiguration, - atlas: Arc, - path_globals_offset: u64, - gamma_offset: u64, - instance_data_capacity: u64, - max_instance_data_size: u64, - instance_data_alignment: u64, - uses_webgl_instance_data: bool, - rendering_params: RenderingParameters, - is_bgr: bool, - dual_source_blending: bool, - adapter_info: wgpu::AdapterInfo, - transparent_alpha_mode: wgpu::CompositeAlphaMode, - opaque_alpha_mode: wgpu::CompositeAlphaMode, - max_texture_size: u32, - last_error: Arc>>, - failed_frame_count: u32, - device_lost: std::sync::Arc, - surface_configured: bool, - needs_redraw: bool, -} - -impl WgpuRenderer { - fn resources(&self) -> &WgpuResources { - self.resources - .as_ref() - .expect("GPU resources not available") - } - - fn resources_mut(&mut self) -> &mut WgpuResources { - self.resources - .as_mut() - .expect("GPU resources not available") - } - - /// Creates a new WgpuRenderer from raw window handles. - /// - /// The `gpu_context` is a shared reference that coordinates GPU context across - /// multiple windows. The first window to create a renderer will initialize the - /// context; subsequent windows will share it. - /// - /// # Safety - /// The caller must ensure that the window handle remains valid for the lifetime - /// of the returned renderer. - #[cfg(not(target_family = "wasm"))] - pub fn new( - gpu_context: GpuContext, - window: &W, - config: WgpuSurfaceConfig, - compositor_gpu: Option, - ) -> anyhow::Result - where - W: HasWindowHandle + HasDisplayHandle + std::fmt::Debug + Send + Sync + Clone + 'static, - { - let window_handle = window - .window_handle() - .map_err(|e| anyhow::anyhow!("Failed to get window handle: {e}"))?; - - let target = wgpu::SurfaceTargetUnsafe::RawHandle { - // Fall back to the display handle already provided via InstanceDescriptor::display. - raw_display_handle: None, - raw_window_handle: window_handle.as_raw(), - }; - - // Use the existing context's instance if available, otherwise create a new one. - // The surface must be created with the same instance that will be used for - // adapter selection, otherwise wgpu will panic. - let instance = gpu_context - .borrow() - .as_ref() - .map(|ctx| ctx.instance.clone()) - .unwrap_or_else(|| WgpuContext::instance(Box::new(window.clone()))); - - // Safety: The caller guarantees that the window handle is valid for the - // lifetime of this renderer. In practice, the RawWindow struct is created - // from the native window handles and the surface is dropped before the window. - let surface = unsafe { - instance - .create_surface_unsafe(target) - .map_err(|e| anyhow::anyhow!("Failed to create surface: {e}"))? - }; - - let mut ctx_ref = gpu_context.borrow_mut(); - let context = match ctx_ref.as_mut() { - Some(context) => { - context.check_compatible_with_surface(&surface)?; - context - } - None => ctx_ref.insert(WgpuContext::new(instance, &surface, compositor_gpu)?), - }; - - let atlas = Arc::new(WgpuAtlas::from_context(context)); - - Self::new_internal( - Some(Rc::clone(&gpu_context)), - context, - surface, - config, - compositor_gpu, - atlas, - ) - } - - #[cfg(target_family = "wasm")] - pub fn new_from_canvas( - context: &WgpuContext, - canvas: &web_sys::HtmlCanvasElement, - config: WgpuSurfaceConfig, - ) -> anyhow::Result { - let surface = context - .instance - .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone())) - .map_err(|e| anyhow::anyhow!("Failed to create surface: {e}"))?; - Self::new_from_surface(context, surface, config) - } - - #[cfg(target_family = "wasm")] - #[allow(clippy::arc_with_non_send_sync)] - pub fn new_from_surface( - context: &WgpuContext, - surface: wgpu::Surface<'static>, - config: WgpuSurfaceConfig, - ) -> anyhow::Result { - let atlas = Arc::new(WgpuAtlas::from_context(context)); - Self::new_internal(None, context, surface, config, None, atlas) - } - - fn new_internal( - gpu_context: Option, - context: &WgpuContext, - surface: wgpu::Surface<'static>, - config: WgpuSurfaceConfig, - compositor_gpu: Option, - atlas: Arc, - ) -> anyhow::Result { - let surface_caps = surface.get_capabilities(&context.adapter); - let preferred_formats = [ - wgpu::TextureFormat::Bgra8Unorm, - wgpu::TextureFormat::Rgba8Unorm, - ]; - let surface_format = preferred_formats - .iter() - .find(|f| surface_caps.formats.contains(f)) - .copied() - .or_else(|| surface_caps.formats.iter().find(|f| !f.is_srgb()).copied()) - .or_else(|| surface_caps.formats.first().copied()) - .ok_or_else(|| { - anyhow::anyhow!( - "Surface reports no supported texture formats for adapter {:?}", - context.adapter.get_info().name - ) - })?; - - let pick_alpha_mode = - |preferences: &[wgpu::CompositeAlphaMode]| -> anyhow::Result { - preferences - .iter() - .find(|p| surface_caps.alpha_modes.contains(p)) - .copied() - .or_else(|| surface_caps.alpha_modes.first().copied()) - .ok_or_else(|| { - anyhow::anyhow!( - "Surface reports no supported alpha modes for adapter {:?}", - context.adapter.get_info().name - ) - }) - }; - - let transparent_alpha_mode = pick_alpha_mode(&[ - wgpu::CompositeAlphaMode::PreMultiplied, - wgpu::CompositeAlphaMode::Inherit, - ])?; - - let opaque_alpha_mode = pick_alpha_mode(&[ - wgpu::CompositeAlphaMode::Opaque, - wgpu::CompositeAlphaMode::Inherit, - ])?; - - let alpha_mode = if config.transparent { - transparent_alpha_mode - } else { - opaque_alpha_mode - }; - - let device = Arc::clone(&context.device); - let max_texture_size = device.limits().max_texture_dimension_2d; - - let requested_width = config.size.width.0 as u32; - let requested_height = config.size.height.0 as u32; - let clamped_width = requested_width.min(max_texture_size); - let clamped_height = requested_height.min(max_texture_size); - - if clamped_width != requested_width || clamped_height != requested_height { - warn!( - "Requested surface size ({}, {}) exceeds maximum texture dimension {}. \ - Clamping to ({}, {}). Window content may not fill the entire window.", - requested_width, requested_height, max_texture_size, clamped_width, clamped_height - ); - } - - let surface_config = wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: surface_format, - width: clamped_width.max(1), - height: clamped_height.max(1), - present_mode: config - .preferred_present_mode - .filter(|mode| surface_caps.present_modes.contains(mode)) - .unwrap_or(wgpu::PresentMode::Fifo), - desired_maximum_frame_latency: 2, - alpha_mode, - view_formats: vec![], - }; - // Configure the surface immediately. The adapter selection process already validated - // that this adapter can successfully configure this surface. - surface.configure(&context.device, &surface_config); - - let queue = Arc::clone(&context.queue); - let rendering_params = RenderingParameters::new(&context.adapter, surface_format); - let uses_webgl_instance_data = context.uses_webgl_instance_data(); - let dual_source_blending = - context.supports_dual_source_blending() && !uses_webgl_instance_data; - let bind_group_layouts = Self::create_bind_group_layouts(&device, uses_webgl_instance_data); - let pipelines = Self::create_pipelines( - &device, - &bind_group_layouts, - surface_format, - alpha_mode, - rendering_params.path_sample_count, - dual_source_blending, - uses_webgl_instance_data, - ); - - let atlas_sampler = device.create_sampler(&wgpu::SamplerDescriptor { - label: Some("atlas_sampler"), - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Linear, - ..Default::default() - }); - - let uniform_alignment = device.limits().min_uniform_buffer_offset_alignment as u64; - let globals_size = std::mem::size_of::() as u64; - let gamma_size = std::mem::size_of::() as u64; - let path_globals_offset = globals_size.next_multiple_of(uniform_alignment); - let gamma_offset = (path_globals_offset + globals_size).next_multiple_of(uniform_alignment); - - let globals_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("globals_buffer"), - size: gamma_offset + gamma_size, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - let ( - instance_data, - instance_data_capacity, - max_instance_data_size, - instance_data_alignment, - ) = if uses_webgl_instance_data { - let max_texture_dimension = device.limits().max_texture_dimension_2d; - let max_instance_data_size = (u64::from(max_texture_dimension).pow(2) - * INSTANCE_TEXTURE_TEXEL_SIZE) - .min(MAX_INSTANCE_BUFFER_SIZE); - let initial_capacity = (2 * 1024 * 1024).min(max_instance_data_size); - let (instance_data, capacity) = - Self::create_instance_texture(&device, initial_capacity, max_texture_dimension); - ( - instance_data, - capacity, - max_instance_data_size, - INSTANCE_TEXTURE_TEXEL_SIZE, - ) - } else { - // Every frame allocation is exposed as one storage-buffer binding, so - // its backing buffer must satisfy both the allocation and binding limits. - let max_buffer_size = device - .limits() - .max_buffer_size - .min(device.limits().max_storage_buffer_binding_size) - .min(MAX_INSTANCE_BUFFER_SIZE); - let initial_capacity = (2 * 1024 * 1024).min(max_buffer_size); - let buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("instance_buffer"), - size: initial_capacity, - usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - ( - InstanceData::Storage(buffer), - initial_capacity, - max_buffer_size, - device.limits().min_storage_buffer_offset_alignment as u64, - ) - }; - - let globals_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("globals_bind_group"), - layout: &bind_group_layouts.globals, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &globals_buffer, - offset: 0, - size: Some(NonZeroU64::new(globals_size).unwrap()), - }), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &globals_buffer, - offset: gamma_offset, - size: Some(NonZeroU64::new(gamma_size).unwrap()), - }), - }, - ], - }); - - let path_globals_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("path_globals_bind_group"), - layout: &bind_group_layouts.globals, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &globals_buffer, - offset: path_globals_offset, - size: Some(NonZeroU64::new(globals_size).unwrap()), - }), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &globals_buffer, - offset: gamma_offset, - size: Some(NonZeroU64::new(gamma_size).unwrap()), - }), - }, - ], - }); - - let adapter_info = context.adapter.get_info(); - - let last_error: Arc>> = Arc::new(Mutex::new(None)); - let last_error_clone = Arc::clone(&last_error); - device.on_uncaptured_error(Arc::new(move |error| { - let mut guard = last_error_clone.lock().unwrap(); - *guard = Some(error.to_string()); - })); - - let resources = WgpuResources { - device, - queue, - surface, - pipelines, - bind_group_layouts, - atlas_sampler, - globals_buffer, - globals_bind_group, - path_globals_bind_group, - instance_data, - // Defer intermediate texture creation to first draw call via ensure_intermediate_textures(). - // This avoids panics when the device/surface is in an invalid state during initialization. - path_intermediate_texture: None, - path_intermediate_view: None, - path_msaa_texture: None, - path_msaa_view: None, - }; - - Ok(Self { - context: gpu_context, - compositor_gpu, - resources: Some(resources), - surface_config, - atlas, - path_globals_offset, - gamma_offset, - instance_data_capacity, - max_instance_data_size, - instance_data_alignment, - uses_webgl_instance_data, - rendering_params, - is_bgr: false, - dual_source_blending, - adapter_info, - transparent_alpha_mode, - opaque_alpha_mode, - max_texture_size, - last_error, - failed_frame_count: 0, - device_lost: context.device_lost_flag(), - surface_configured: true, - needs_redraw: false, - }) - } - - fn create_bind_group_layouts( - device: &wgpu::Device, - uses_webgl_instance_data: bool, - ) -> WgpuBindGroupLayouts { - let globals = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("globals_layout"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: NonZeroU64::new( - std::mem::size_of::() as u64 - ), - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: NonZeroU64::new( - std::mem::size_of::() as u64 - ), - }, - count: None, - }, - ], - }); - - let instance_data_entry = wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, - ty: if uses_webgl_instance_data { - wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Uint, - view_dimension: wgpu::TextureViewDimension::D2, - multisampled: false, - } - } else { - wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Storage { read_only: true }, - has_dynamic_offset: false, - min_binding_size: None, - } - }, - count: None, - }; - - let instances = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("instances_layout"), - entries: &[instance_data_entry], - }); - - let texture = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("texture_layout"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - view_dimension: wgpu::TextureViewDimension::D2, - multisampled: false, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - ], - }); - - let surfaces = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("surfaces_layout"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: NonZeroU64::new( - std::mem::size_of::() as u64 - ), - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - view_dimension: wgpu::TextureViewDimension::D2, - multisampled: false, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 2, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - view_dimension: wgpu::TextureViewDimension::D2, - multisampled: false, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 3, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - ], - }); - - WgpuBindGroupLayouts { - globals, - instances, - texture, - surfaces, - } - } - - fn create_instance_texture( - device: &wgpu::Device, - requested_capacity: u64, - max_texture_dimension: u32, - ) -> (InstanceData, u64) { - let texel_count = requested_capacity.div_ceil(INSTANCE_TEXTURE_TEXEL_SIZE); - let width = texel_count.min(u64::from(max_texture_dimension)).max(1) as u32; - let height = texel_count - .div_ceil(u64::from(width)) - .min(u64::from(max_texture_dimension)) - .max(1) as u32; - let texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some("instance_texture"), - size: wgpu::Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba32Uint, - usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, - view_formats: &[], - }); - let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - let capacity = u64::from(width) * u64::from(height) * INSTANCE_TEXTURE_TEXEL_SIZE; - ( - InstanceData::Texture { - texture, - view, - width, - height, - }, - capacity, - ) - } - - fn create_pipelines( - device: &wgpu::Device, - layouts: &WgpuBindGroupLayouts, - surface_format: wgpu::TextureFormat, - alpha_mode: wgpu::CompositeAlphaMode, - path_sample_count: u32, - dual_source_blending: bool, - uses_webgl_instance_data: bool, - ) -> WgpuPipelines { - // Diagnostic guard: verify the device actually has - // DUAL_SOURCE_BLENDING. We have a crash report (ZED-5G1) where a - // feature mismatch caused a wgpu-hal abort, but we haven't - // identified the code path that produces the mismatch. This - // guard prevents the crash and logs more evidence. - // Remove this check once: - // a) We find and fix the root cause, or - // b) There are no reports of this warning appearing for some time. - let device_has_feature = device - .features() - .contains(wgpu::Features::DUAL_SOURCE_BLENDING); - if dual_source_blending && !device_has_feature { - log::error!( - "BUG: dual_source_blending flag is true but device does not \ - have DUAL_SOURCE_BLENDING enabled (device features: {:?}). \ - Falling back to mono text rendering. Please report this at \ - https://github.com/zed-industries/zed/issues", - device.features(), - ); - } - let dual_source_blending = - dual_source_blending && device_has_feature && !uses_webgl_instance_data; - - let shader_source = if uses_webgl_instance_data { - WEBGL_SHADERS - } else { - STORAGE_BUFFER_SHADERS - }; - let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("gpui_shaders"), - source: wgpu::ShaderSource::Wgsl(shader_source.into()), - }); - - let subpixel_shader_module = if dual_source_blending { - Some(device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("gpui_subpixel_shaders"), - source: wgpu::ShaderSource::Wgsl(SUBPIXEL_SHADERS.into()), - })) - } else { - None - }; - - let blend_mode = match alpha_mode { - wgpu::CompositeAlphaMode::PreMultiplied => { - wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING - } - _ => wgpu::BlendState::ALPHA_BLENDING, - }; - - let color_target = wgpu::ColorTargetState { - format: surface_format, - blend: Some(blend_mode), - write_mask: wgpu::ColorWrites::ALL, - }; - - let create_pipeline = |name: &str, - vs_entry: &str, - fs_entry: &str, - globals_layout: &wgpu::BindGroupLayout, - data_layout: &wgpu::BindGroupLayout, - texture_layout: Option<&wgpu::BindGroupLayout>, - topology: wgpu::PrimitiveTopology, - color_targets: &[Option], - sample_count: u32, - module: &wgpu::ShaderModule| { - let bind_group_layouts = [ - Some(globals_layout), - Some(data_layout), - texture_layout, - Some(&layouts.instances), - ]; - let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some(&format!("{name}_layout")), - bind_group_layouts: &bind_group_layouts, - immediate_size: 0, - }); - - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some(name), - layout: Some(&pipeline_layout), - vertex: wgpu::VertexState { - module, - entry_point: Some(vs_entry), - buffers: &[], - compilation_options: wgpu::PipelineCompilationOptions::default(), - }, - fragment: Some(wgpu::FragmentState { - module, - entry_point: Some(fs_entry), - targets: color_targets, - compilation_options: wgpu::PipelineCompilationOptions::default(), - }), - primitive: wgpu::PrimitiveState { - topology, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: None, - polygon_mode: wgpu::PolygonMode::Fill, - unclipped_depth: false, - conservative: false, - }, - depth_stencil: None, - multisample: wgpu::MultisampleState { - count: sample_count, - mask: !0, - alpha_to_coverage_enabled: false, - }, - multiview_mask: None, - cache: None, - }) - }; - - let quads = create_pipeline( - "quads", - "vs_quad", - "fs_quad", - &layouts.globals, - &layouts.instances, - None, - wgpu::PrimitiveTopology::TriangleStrip, - &[Some(color_target.clone())], - 1, - &shader_module, - ); - - let shadows = create_pipeline( - "shadows", - "vs_shadow", - "fs_shadow", - &layouts.globals, - &layouts.instances, - None, - wgpu::PrimitiveTopology::TriangleStrip, - &[Some(color_target.clone())], - 1, - &shader_module, - ); - - let path_rasterization = create_pipeline( - "path_rasterization", - "vs_path_rasterization", - "fs_path_rasterization", - &layouts.globals, - &layouts.instances, - None, - wgpu::PrimitiveTopology::TriangleList, - &[Some(wgpu::ColorTargetState { - format: surface_format, - blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - path_sample_count, - &shader_module, - ); - - let paths_blend = wgpu::BlendState { - color: wgpu::BlendComponent { - src_factor: wgpu::BlendFactor::One, - dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, - operation: wgpu::BlendOperation::Add, - }, - alpha: wgpu::BlendComponent { - src_factor: wgpu::BlendFactor::One, - dst_factor: wgpu::BlendFactor::One, - operation: wgpu::BlendOperation::Add, - }, - }; - - let paths = create_pipeline( - "paths", - "vs_path", - "fs_path", - &layouts.globals, - &layouts.instances, - Some(&layouts.texture), - wgpu::PrimitiveTopology::TriangleStrip, - &[Some(wgpu::ColorTargetState { - format: surface_format, - blend: Some(paths_blend), - write_mask: wgpu::ColorWrites::ALL, - })], - 1, - &shader_module, - ); - - let underlines = create_pipeline( - "underlines", - "vs_underline", - "fs_underline", - &layouts.globals, - &layouts.instances, - None, - wgpu::PrimitiveTopology::TriangleStrip, - &[Some(color_target.clone())], - 1, - &shader_module, - ); - - let mono_sprites = create_pipeline( - "mono_sprites", - "vs_mono_sprite", - "fs_mono_sprite", - &layouts.globals, - &layouts.instances, - Some(&layouts.texture), - wgpu::PrimitiveTopology::TriangleStrip, - &[Some(color_target.clone())], - 1, - &shader_module, - ); - - let subpixel_sprites = if let Some(subpixel_module) = &subpixel_shader_module { - let subpixel_blend = wgpu::BlendState { - color: wgpu::BlendComponent { - src_factor: wgpu::BlendFactor::Src1, - dst_factor: wgpu::BlendFactor::OneMinusSrc1, - operation: wgpu::BlendOperation::Add, - }, - alpha: wgpu::BlendComponent { - src_factor: wgpu::BlendFactor::One, - dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, - operation: wgpu::BlendOperation::Add, - }, - }; - - Some(create_pipeline( - "subpixel_sprites", - "vs_subpixel_sprite", - "fs_subpixel_sprite", - &layouts.globals, - &layouts.instances, - Some(&layouts.texture), - wgpu::PrimitiveTopology::TriangleStrip, - &[Some(wgpu::ColorTargetState { - format: surface_format, - blend: Some(subpixel_blend), - write_mask: wgpu::ColorWrites::COLOR, - })], - 1, - subpixel_module, - )) - } else { - None - }; - - let poly_sprites = create_pipeline( - "poly_sprites", - "vs_poly_sprite", - "fs_poly_sprite", - &layouts.globals, - &layouts.instances, - Some(&layouts.texture), - wgpu::PrimitiveTopology::TriangleStrip, - &[Some(color_target.clone())], - 1, - &shader_module, - ); - - let surfaces = create_pipeline( - "surfaces", - "vs_surface", - "fs_surface", - &layouts.globals, - &layouts.surfaces, - None, - wgpu::PrimitiveTopology::TriangleStrip, - &[Some(color_target)], - 1, - &shader_module, - ); - - WgpuPipelines { - quads, - shadows, - path_rasterization, - paths, - underlines, - mono_sprites, - subpixel_sprites, - poly_sprites, - surfaces, - } - } - - fn create_path_intermediate( - device: &wgpu::Device, - format: wgpu::TextureFormat, - width: u32, - height: u32, - ) -> (wgpu::Texture, wgpu::TextureView) { - let texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some("path_intermediate"), - size: wgpu::Extent3d { - width: width.max(1), - height: height.max(1), - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }); - let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - (texture, view) - } - - fn create_msaa_if_needed( - device: &wgpu::Device, - format: wgpu::TextureFormat, - width: u32, - height: u32, - sample_count: u32, - ) -> Option<(wgpu::Texture, wgpu::TextureView)> { - if sample_count <= 1 { - return None; - } - let texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some("path_msaa"), - size: wgpu::Extent3d { - width: width.max(1), - height: height.max(1), - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count, - dimension: wgpu::TextureDimension::D2, - format, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - view_formats: &[], - }); - let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - Some((texture, view)) - } - - pub fn update_drawable_size(&mut self, size: Size) { - let width = size.width.0 as u32; - let height = size.height.0 as u32; - - if width != self.surface_config.width || height != self.surface_config.height { - let clamped_width = width.min(self.max_texture_size); - let clamped_height = height.min(self.max_texture_size); - - if clamped_width != width || clamped_height != height { - warn!( - "Requested surface size ({}, {}) exceeds maximum texture dimension {}. \ - Clamping to ({}, {}). Window content may not fill the entire window.", - width, height, self.max_texture_size, clamped_width, clamped_height - ); - } - - self.surface_config.width = clamped_width.max(1); - self.surface_config.height = clamped_height.max(1); - let surface_config = self.surface_config.clone(); - - let Some(resources) = self.resources.as_mut() else { - return; - }; - - // Wait for any in-flight GPU work to complete before destroying textures - if let Err(e) = resources.device.poll(wgpu::PollType::Wait { - submission_index: None, - timeout: None, - }) { - warn!("Failed to poll device during resize: {e:?}"); - } - - // Destroy old textures before allocating new ones to avoid GPU memory spikes - if let Some(ref texture) = resources.path_intermediate_texture { - texture.destroy(); - } - if let Some(ref texture) = resources.path_msaa_texture { - texture.destroy(); - } - - resources - .surface - .configure(&resources.device, &surface_config); - - // Invalidate intermediate textures - they will be lazily recreated - // in draw() after we confirm the surface is healthy. This avoids - // panics when the device/surface is in an invalid state during resize. - resources.invalidate_intermediate_textures(); - } - } - - fn ensure_intermediate_textures(&mut self) { - if self.resources().path_intermediate_texture.is_some() { - return; - } - - let format = self.surface_config.format; - let width = self.surface_config.width; - let height = self.surface_config.height; - let path_sample_count = self.rendering_params.path_sample_count; - let resources = self.resources_mut(); - - let (t, v) = Self::create_path_intermediate(&resources.device, format, width, height); - resources.path_intermediate_texture = Some(t); - resources.path_intermediate_view = Some(v); - - let (path_msaa_texture, path_msaa_view) = Self::create_msaa_if_needed( - &resources.device, - format, - width, - height, - path_sample_count, - ) - .map(|(t, v)| (Some(t), Some(v))) - .unwrap_or((None, None)); - resources.path_msaa_texture = path_msaa_texture; - resources.path_msaa_view = path_msaa_view; - } - - pub fn set_subpixel_layout(&mut self, is_bgr: bool) { - self.is_bgr = is_bgr; - } - - pub fn update_transparency(&mut self, transparent: bool) { - let new_alpha_mode = if transparent { - self.transparent_alpha_mode - } else { - self.opaque_alpha_mode - }; - - if new_alpha_mode != self.surface_config.alpha_mode { - self.surface_config.alpha_mode = new_alpha_mode; - let surface_config = self.surface_config.clone(); - let path_sample_count = self.rendering_params.path_sample_count; - let dual_source_blending = self.dual_source_blending; - let uses_webgl_instance_data = self.uses_webgl_instance_data; - let Some(resources) = self.resources.as_mut() else { - return; - }; - resources - .surface - .configure(&resources.device, &surface_config); - resources.pipelines = Self::create_pipelines( - &resources.device, - &resources.bind_group_layouts, - surface_config.format, - surface_config.alpha_mode, - path_sample_count, - dual_source_blending, - uses_webgl_instance_data, - ); - } - } - - #[allow(dead_code)] - pub fn viewport_size(&self) -> Size { - Size { - width: DevicePixels(self.surface_config.width as i32), - height: DevicePixels(self.surface_config.height as i32), - } - } - - pub fn sprite_atlas(&self) -> &Arc { - &self.atlas - } - - pub fn supports_dual_source_blending(&self) -> bool { - self.dual_source_blending - } - - pub fn gpu_specs(&self) -> GpuSpecs { - GpuSpecs { - is_software_emulated: self.adapter_info.device_type == wgpu::DeviceType::Cpu, - device_name: self.adapter_info.name.clone(), - driver_name: self.adapter_info.driver.clone(), - driver_info: self.adapter_info.driver_info.clone(), - } - } - - pub fn max_texture_size(&self) -> u32 { - self.max_texture_size - } - - pub fn draw(&mut self, scene: &Scene) -> bool { - #[cfg(target_family = "wasm")] - if self.device_lost() { - if self.surface_configured { - log::error!( - "Browser graphics context was lost; rendering has stopped. Reload the page to recover." - ); - self.surface_configured = false; - } - return false; - } - - // Bail out early if the surface has been unconfigured (e.g. during - // Android background/rotation transitions). Attempting to acquire - // a texture from an unconfigured surface can block indefinitely on - // some drivers (Adreno). - if !self.surface_configured { - return false; - } - - let last_error = self.last_error.lock().unwrap().take(); - if let Some(error) = last_error { - self.failed_frame_count += 1; - log::error!( - "GPU error during frame (failure {} of 10): {error}", - self.failed_frame_count - ); - - // TBD. Does retrying more actually help? - if self.failed_frame_count > 10 { - panic!("Too many consecutive GPU errors. Last error: {error}"); - } else if self.failed_frame_count > 5 { - if let Some(res) = self.resources.as_mut() { - res.invalidate_intermediate_textures(); - } - self.atlas.clear(); - self.needs_redraw = true; - self.failed_frame_count = 0; - return false; - } - } else { - self.failed_frame_count = 0; - } - - self.atlas.before_frame(); - - let frame = match self.resources().surface.get_current_texture() { - wgpu::CurrentSurfaceTexture::Success(frame) => frame, - wgpu::CurrentSurfaceTexture::Suboptimal(frame) => { - // Textures must be destroyed before the surface can be reconfigured. - drop(frame); - let surface_config = self.surface_config.clone(); - let resources = self.resources_mut(); - resources - .surface - .configure(&resources.device, &surface_config); - return false; - } - wgpu::CurrentSurfaceTexture::Lost | wgpu::CurrentSurfaceTexture::Outdated => { - let surface_config = self.surface_config.clone(); - let resources = self.resources_mut(); - resources - .surface - .configure(&resources.device, &surface_config); - return false; - } - wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => { - return false; - } - wgpu::CurrentSurfaceTexture::Validation => { - *self.last_error.lock().unwrap() = - Some("Surface texture validation error".to_string()); - return false; - } - }; - - // Now that we know the surface is healthy, ensure intermediate textures exist - self.ensure_intermediate_textures(); - - let frame_view = frame - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - - let gamma_params = GammaParams { - gamma_ratios: self.rendering_params.gamma_ratios, - grayscale_enhanced_contrast: self.rendering_params.grayscale_enhanced_contrast, - subpixel_enhanced_contrast: self.rendering_params.subpixel_enhanced_contrast, - is_bgr: self.is_bgr as u32, - _pad: 0, - }; - - let globals = GlobalParams { - viewport_size: [ - self.surface_config.width as f32, - self.surface_config.height as f32, - ], - premultiplied_alpha: if self.surface_config.alpha_mode - == wgpu::CompositeAlphaMode::PreMultiplied - { - 1 - } else { - 0 - }, - pad: 0, - }; - - let path_globals = GlobalParams { - premultiplied_alpha: 0, - ..globals - }; - - { - let resources = self.resources(); - resources.queue.write_buffer( - &resources.globals_buffer, - 0, - bytemuck::bytes_of(&globals), - ); - resources.queue.write_buffer( - &resources.globals_buffer, - self.path_globals_offset, - bytemuck::bytes_of(&path_globals), - ); - resources.queue.write_buffer( - &resources.globals_buffer, - self.gamma_offset, - bytemuck::bytes_of(&gamma_params), - ); - } - - if let Err(error) = self.record_frame(scene, &frame_view) { - log::error!("{error:#}"); - self.resources().queue.submit(std::iter::empty()); - return false; - } - - frame.present(); - true - } - - fn record_frame(&mut self, scene: &Scene, frame_view: &wgpu::TextureView) -> Result<()> { - let mut instance_offset = 0; - let instance_bindings = self - .write_instances(scene, &mut instance_offset) - .with_context(|| { - format!( - "scene too large: {} paths, {} shadows, {} quads, {} underlines, {} monochrome sprites, {} subpixel sprites, {} polychrome sprites", - scene.paths.len(), - scene.shadows.len(), - scene.quads.len(), - scene.underlines.len(), - scene.monochrome_sprites.len(), - scene.subpixel_sprites.len(), - scene.polychrome_sprites.len(), - ) - })?; - - let mut encoder = - self.resources() - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("main_encoder"), - }); - - { - let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("main_pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: frame_view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), - store: wgpu::StoreOp::Store, - }, - depth_slice: None, - })], - depth_stencil_attachment: None, - ..Default::default() - }); - - pass.set_bind_group(3, &instance_bindings.clips.bind_group, &[]); - for batch in scene.batches() { - match batch { - PrimitiveBatch::Quads(range) => self.draw_instances( - &instance_bindings.quads, - &self.resources().pipelines.quads, - instance_range(range), - &mut pass, - ), - PrimitiveBatch::Shadows(range) => self.draw_instances( - &instance_bindings.shadows, - &self.resources().pipelines.shadows, - instance_range(range), - &mut pass, - ), - PrimitiveBatch::Paths(range) => { - let paths = &scene.paths[range]; - if paths.is_empty() { - continue; - } - - drop(pass); - let rasterized = self.draw_paths_to_intermediate( - &mut encoder, - paths, - &mut instance_offset, - &instance_bindings.clips, - )?; - - pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("main_pass_continued"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: frame_view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - depth_slice: None, - })], - depth_stencil_attachment: None, - ..Default::default() - }); - - pass.set_bind_group(3, &instance_bindings.clips.bind_group, &[]); - if rasterized { - self.draw_paths_from_intermediate( - paths, - &mut instance_offset, - &mut pass, - )?; - } - } - PrimitiveBatch::Underlines(range) => self.draw_instances( - &instance_bindings.underlines, - &self.resources().pipelines.underlines, - instance_range(range), - &mut pass, - ), - PrimitiveBatch::MonochromeSprites { texture_id, range } => self.draw_sprites( - &instance_bindings.monochrome_sprites, - texture_id, - &self.resources().pipelines.mono_sprites, - instance_range(range), - &mut pass, - ), - PrimitiveBatch::SubpixelSprites { texture_id, range } => { - let resources = self.resources(); - self.draw_sprites( - &instance_bindings.subpixel_sprites, - texture_id, - resources - .pipelines - .subpixel_sprites - .as_ref() - .unwrap_or(&resources.pipelines.mono_sprites), - instance_range(range), - &mut pass, - ); - } - PrimitiveBatch::PolychromeSprites { texture_id, range } => self.draw_sprites( - &instance_bindings.polychrome_sprites, - texture_id, - &self.resources().pipelines.poly_sprites, - instance_range(range), - &mut pass, - ), - // Surfaces are macOS-only for video playback and are not - // implemented by the WGPU renderer. - PrimitiveBatch::Surfaces(_surfaces) => {} - } - } - } - - self.resources() - .queue - .submit(std::iter::once(encoder.finish())); - Ok(()) - } - - fn write_instances( - &mut self, - scene: &Scene, - instance_offset: &mut u64, - ) -> Result { - let empty_clip = gpui::RoundedClip::::default(); - let clips = if scene.rounded_clips.is_empty() { - std::slice::from_ref(&empty_clip) - } else { - &scene.rounded_clips - }; - // Keep clip records first: WebGL addresses this binding's texture from zero. - Ok(InstanceBindings { - clips: self.write_instance_binding("clips_bind_group", instance_offset, clips)?, - quads: self.write_instance_binding( - "quads_bind_group", - instance_offset, - &scene.quads, - )?, - shadows: self.write_instance_binding( - "shadows_bind_group", - instance_offset, - &scene.shadows, - )?, - underlines: self.write_instance_binding( - "underlines_bind_group", - instance_offset, - &scene.underlines, - )?, - monochrome_sprites: self.write_instance_binding( - "monochrome_sprites_bind_group", - instance_offset, - &scene.monochrome_sprites, - )?, - subpixel_sprites: self.write_instance_binding( - "subpixel_sprites_bind_group", - instance_offset, - &scene.subpixel_sprites, - )?, - polychrome_sprites: self.write_instance_binding( - "polychrome_sprites_bind_group", - instance_offset, - &scene.polychrome_sprites, - )?, - }) - } - - fn create_texture_bind_group( - &self, - label: &str, - texture_view: &wgpu::TextureView, - ) -> wgpu::BindGroup { - let resources = self.resources(); - resources - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - label: Some(label), - layout: &resources.bind_group_layouts.texture, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(texture_view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(&resources.atlas_sampler), - }, - ], - }) - } - - fn draw_instances( - &self, - instances: &InstanceBinding, - pipeline: &wgpu::RenderPipeline, - range: Range, - pass: &mut wgpu::RenderPass<'_>, - ) { - if range.is_empty() { - return; - } - pass.set_pipeline(pipeline); - pass.set_bind_group(0, &self.resources().globals_bind_group, &[]); - pass.set_bind_group(1, &instances.bind_group, &[]); - pass.draw( - 0..4, - instances.first_instance + range.start..instances.first_instance + range.end, - ); - } - - fn draw_sprites( - &self, - sprite_instances: &InstanceBinding, - texture_id: AtlasTextureId, - pipeline: &wgpu::RenderPipeline, - range: Range, - pass: &mut wgpu::RenderPass<'_>, - ) { - if range.is_empty() { - return; - } - let texture_info = self.atlas.get_texture_info(texture_id); - let texture = - self.create_texture_bind_group("atlas_texture_bind_group", &texture_info.view); - pass.set_pipeline(pipeline); - pass.set_bind_group(0, &self.resources().globals_bind_group, &[]); - pass.set_bind_group(1, &sprite_instances.bind_group, &[]); - pass.set_bind_group(2, &texture, &[]); - pass.draw( - 0..4, - sprite_instances.first_instance + range.start - ..sprite_instances.first_instance + range.end, - ); - } - - unsafe fn instance_bytes(instances: &[T]) -> &[u8] { - unsafe { - std::slice::from_raw_parts( - instances.as_ptr() as *const u8, - std::mem::size_of_val(instances), - ) - } - } - - fn draw_paths_from_intermediate( - &mut self, - paths: &[Path], - instance_offset: &mut u64, - pass: &mut wgpu::RenderPass<'_>, - ) -> Result<()> { - let first_path = &paths[0]; - let sprites: Vec = if paths.last().map(|p| &p.order) == Some(&first_path.order) - { - paths - .iter() - .map(|p| PathSprite { - bounds: p.clipped_bounds(), - }) - .collect() - } else { - let mut bounds = first_path.clipped_bounds(); - for path in paths.iter().skip(1) { - bounds = bounds.union(&path.clipped_bounds()); - } - vec![PathSprite { bounds }] - }; - - let Some(path_intermediate_view) = self.resources().path_intermediate_view.clone() else { - return Ok(()); - }; - let instances = - self.write_instance_binding("path_sprites_bind_group", instance_offset, &sprites)?; - let texture = self.create_texture_bind_group( - "path_intermediate_texture_bind_group", - &path_intermediate_view, - ); - let resources = self.resources(); - pass.set_pipeline(&resources.pipelines.paths); - pass.set_bind_group(0, &resources.globals_bind_group, &[]); - pass.set_bind_group(1, &instances.bind_group, &[]); - pass.set_bind_group(2, &texture, &[]); - pass.draw( - 0..4, - instances.first_instance..instances.first_instance + sprites.len() as u32, - ); - Ok(()) - } - - fn draw_paths_to_intermediate( - &mut self, - encoder: &mut wgpu::CommandEncoder, - paths: &[Path], - instance_offset: &mut u64, - clips: &InstanceBinding, - ) -> Result { - let mut vertices = Vec::new(); - for path in paths { - let bounds = path.clipped_bounds(); - vertices.extend(path.vertices.iter().map(|v| PathRasterizationVertex { - xy_position: v.xy_position, - st_position: v.st_position, - color: path.color, - bounds, - content_mask: path.content_mask, - })); - } - - if vertices.is_empty() { - return Ok(false); - } - - let vertex_binding = self.write_instance_binding( - "path_rasterization_bind_group", - instance_offset, - &vertices, - )?; - - let resources = self.resources(); - let Some(path_intermediate_view) = resources.path_intermediate_view.as_ref() else { - return Ok(false); - }; - - let (target_view, resolve_target) = if let Some(ref msaa_view) = resources.path_msaa_view { - (msaa_view, Some(path_intermediate_view)) - } else { - (path_intermediate_view, None) - }; - - { - let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("path_rasterization_pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: target_view, - resolve_target, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), - store: wgpu::StoreOp::Store, - }, - depth_slice: None, - })], - depth_stencil_attachment: None, - ..Default::default() - }); - - pass.set_bind_group(3, &clips.bind_group, &[]); - pass.set_pipeline(&resources.pipelines.path_rasterization); - pass.set_bind_group(0, &resources.path_globals_bind_group, &[]); - pass.set_bind_group(1, &vertex_binding.bind_group, &[]); - // The path rasterization shader loads records by vertex index - // rather than instance index, so the allocation's base shifts the - // vertex range here. - pass.draw( - vertex_binding.first_instance - ..vertex_binding.first_instance + vertices.len() as u32, - 0..1, - ); - } - - Ok(true) - } - - fn write_instance_binding( - &mut self, - label: &str, - instance_offset: &mut u64, - instances: &[T], - ) -> Result { - let data = unsafe { Self::instance_bytes(instances) }; - // wgpu rejects zero-sized bindings, so empty primitive arrays still - // reserve the 16-byte minimum. - let size = (data.len() as u64).max(16); - let stride = (std::mem::size_of::() as u64).max(1); - let (alignment, allocation_size) = if self.uses_webgl_instance_data { - // The texture transport has no binding offset: the shader indexes - // the instance texture absolutely, so each allocation must start on - // a whole instance (a stride multiple) and a whole texel, and must - // end on a texel boundary so the zero padding of its final partial - // texel cannot overlap the next allocation. - ( - least_common_multiple(self.instance_data_alignment, stride), - size.next_multiple_of(INSTANCE_TEXTURE_TEXEL_SIZE), - ) - } else { - (self.instance_data_alignment.max(1), size) - }; - let mut offset = (*instance_offset).next_multiple_of(alignment); - if offset + allocation_size > self.instance_data_capacity { - self.grow_instance_data(allocation_size)?; - offset = 0; - } - *instance_offset = offset + allocation_size; - - let first_instance = if self.uses_webgl_instance_data { - u32::try_from(offset / stride).context("instance index exceeds u32 range")? - } else { - 0 - }; - - let resources = self.resources(); - if !data.is_empty() { - match &resources.instance_data { - InstanceData::Storage(buffer) => resources.queue.write_buffer(buffer, offset, data), - InstanceData::Texture { .. } => { - Self::write_instance_texture(resources, offset, data) - } - } - } - let bind_group = resources - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - label: Some(label), - layout: &resources.bind_group_layouts.instances, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: match &resources.instance_data { - InstanceData::Storage(buffer) => { - wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer, - offset, - size: NonZeroU64::new(size), - }) - } - InstanceData::Texture { view, .. } => { - wgpu::BindingResource::TextureView(view) - } - }, - }], - }); - Ok(InstanceBinding { - bind_group, - first_instance, - }) - } - - fn write_instance_texture(resources: &WgpuResources, offset: u64, data: &[u8]) { - let InstanceData::Texture { - texture, - width, - height, - .. - } = &resources.instance_data - else { - return; - }; - let mut byte_offset = 0usize; - let mut texel_offset = offset / INSTANCE_TEXTURE_TEXEL_SIZE; - while byte_offset < data.len() { - let x = (texel_offset % u64::from(*width)) as u32; - let y = (texel_offset / u64::from(*width)) as u32; - if y >= *height { - // The capacity check in write_instance_binding should make this - // unreachable. Truncating silently would leave stale bytes in the - // texture and draw garbage for the remaining instances. - debug_assert!( - false, - "instance texture write out of bounds: row {y} >= height {}", - *height - ); - log::error!( - "instance texture write out of bounds; dropping {} bytes of instance data", - data.len() - byte_offset - ); - return; - } - let available_texels = u64::from(*width - x); - let remaining_bytes = data.len() - byte_offset; - let complete_texels = remaining_bytes as u64 / INSTANCE_TEXTURE_TEXEL_SIZE; - let texels = complete_texels.min(available_texels); - if texels > 0 { - let byte_count = (texels * INSTANCE_TEXTURE_TEXEL_SIZE) as usize; - resources.queue.write_texture( - wgpu::TexelCopyTextureInfo { - texture, - mip_level: 0, - origin: wgpu::Origin3d { x, y, z: 0 }, - aspect: wgpu::TextureAspect::All, - }, - &data[byte_offset..byte_offset + byte_count], - wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(byte_count as u32), - rows_per_image: None, - }, - wgpu::Extent3d { - width: texels as u32, - height: 1, - depth_or_array_layers: 1, - }, - ); - byte_offset += byte_count; - texel_offset += texels; - continue; - } - - let mut final_texel = [0; INSTANCE_TEXTURE_TEXEL_SIZE as usize]; - final_texel[..remaining_bytes].copy_from_slice(&data[byte_offset..]); - resources.queue.write_texture( - wgpu::TexelCopyTextureInfo { - texture, - mip_level: 0, - origin: wgpu::Origin3d { x, y, z: 0 }, - aspect: wgpu::TextureAspect::All, - }, - &final_texel, - wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(INSTANCE_TEXTURE_TEXEL_SIZE as u32), - rows_per_image: None, - }, - wgpu::Extent3d { - width: 1, - height: 1, - depth_or_array_layers: 1, - }, - ); - break; - } - } - - fn grow_instance_data(&mut self, required: u64) -> Result<()> { - let capacity = (self.instance_data_capacity * 2) - .max(required.next_power_of_two()) - .min(self.max_instance_data_size); - anyhow::ensure!( - capacity >= required, - "instance data needs {required} bytes, above the maximum of {}", - self.max_instance_data_size - ); - anyhow::ensure!( - capacity > self.instance_data_capacity, - "frame instance data exceeds the {}-byte maximum", - self.max_instance_data_size - ); - log::debug!( - "instance data grown from {} to {capacity}", - self.instance_data_capacity - ); - // Bind groups created earlier in the frame keep the previous buffer or - // texture alive, so allocations written before the grow remain valid; - // only subsequent writes land in the new allocation. - let uses_webgl_instance_data = self.uses_webgl_instance_data; - let resources = self.resources_mut(); - if uses_webgl_instance_data { - let max_texture_dimension = resources.device.limits().max_texture_dimension_2d; - let (instance_data, actual_capacity) = - Self::create_instance_texture(&resources.device, capacity, max_texture_dimension); - resources.instance_data = instance_data; - self.instance_data_capacity = actual_capacity; - } else { - resources.instance_data = - InstanceData::Storage(resources.device.create_buffer(&wgpu::BufferDescriptor { - label: Some("instance_buffer"), - size: capacity, - usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - })); - self.instance_data_capacity = capacity; - } - Ok(()) - } - - /// Mark the surface as unconfigured so rendering is skipped until a new - /// surface is provided via [`replace_surface`](Self::replace_surface). - /// - /// This does **not** drop the renderer — the device, queue, atlas, and - /// pipelines stay alive. Use this when the native window is destroyed - /// (e.g. Android `TerminateWindow`) but you intend to re-create the - /// surface later without losing cached atlas textures. - pub fn unconfigure_surface(&mut self) { - self.surface_configured = false; - // Drop intermediate textures since they reference the old surface size. - if let Some(res) = self.resources.as_mut() { - res.invalidate_intermediate_textures(); - } - } - - /// Replace the wgpu surface with a new one (e.g. after Android destroys - /// and recreates the native window). Keeps the device, queue, atlas, and - /// all pipelines intact so cached `AtlasTextureId`s remain valid. - /// - /// The `instance` **must** be the same [`wgpu::Instance`] that was used to - /// create the adapter and device (i.e. from the [`WgpuContext`]). Using a - /// different instance will cause a "Device does not exist" panic because - /// the wgpu device is bound to its originating instance. - #[cfg(not(target_family = "wasm"))] - pub fn replace_surface( - &mut self, - window: &W, - config: WgpuSurfaceConfig, - instance: &wgpu::Instance, - ) -> anyhow::Result<()> { - let window_handle = window - .window_handle() - .map_err(|e| anyhow::anyhow!("Failed to get window handle: {e}"))?; - - let surface = create_surface(instance, window_handle.as_raw())?; - - let width = (config.size.width.0 as u32).max(1); - let height = (config.size.height.0 as u32).max(1); - - let alpha_mode = if config.transparent { - self.transparent_alpha_mode - } else { - self.opaque_alpha_mode - }; - - self.surface_config.width = width; - self.surface_config.height = height; - self.surface_config.alpha_mode = alpha_mode; - if let Some(mode) = config.preferred_present_mode { - self.surface_config.present_mode = mode; - } - - { - let res = self - .resources - .as_mut() - .expect("GPU resources not available"); - surface.configure(&res.device, &self.surface_config); - res.surface = surface; - - // Invalidate intermediate textures — they'll be recreated lazily. - res.invalidate_intermediate_textures(); - } - - self.surface_configured = true; - - Ok(()) - } - - pub fn destroy(&mut self) { - // Release surface-bound GPU resources eagerly so the underlying native - // window can be destroyed before the renderer itself is dropped. - self.resources.take(); - } - - /// Returns true if the GPU device was lost and recovery is needed. - pub fn device_lost(&self) -> bool { - self.device_lost.load(std::sync::atomic::Ordering::SeqCst) - } - - /// Returns true if a redraw is needed because GPU state was cleared. - /// Calling this method clears the flag. - pub fn needs_redraw(&mut self) -> bool { - std::mem::take(&mut self.needs_redraw) - } - - /// Recovers from a lost GPU device by recreating the renderer with a new context. - /// - /// Call this after detecting `device_lost()` returns true. - /// - /// This method coordinates recovery across multiple windows: - /// - The first window to call this will recreate the shared context - /// - Subsequent windows will adopt the already-recovered context - #[cfg(not(target_family = "wasm"))] - pub fn recover(&mut self, window: &W) -> anyhow::Result<()> - where - W: HasWindowHandle + HasDisplayHandle + std::fmt::Debug + Send + Sync + Clone + 'static, - { - let gpu_context = self.context.as_ref().expect("recover requires gpu_context"); - - // Check if another window already recovered the context - let needs_new_context = gpu_context - .borrow() - .as_ref() - .is_none_or(|ctx| ctx.device_lost()); - - let window_handle = window - .window_handle() - .map_err(|e| anyhow::anyhow!("Failed to get window handle: {e}"))?; - - let surface = if needs_new_context { - log::warn!("GPU device lost, recreating context..."); - - // Drop old resources to release Arc/Arc and GPU resources - self.resources = None; - *gpu_context.borrow_mut() = None; - - // Wait briefly for the GPU driver to stabilize, then try to - // recreate the context without software renderers. If this fails - // the caller should request another frame and retry — the real GPU - // may need more time to come back (e.g. after suspend/resume). - std::thread::sleep(std::time::Duration::from_millis(350)); - - let instance = WgpuContext::instance(Box::new(window.clone())); - let surface = create_surface(&instance, window_handle.as_raw())?; - let new_context = - WgpuContext::new_rejecting_software(instance, &surface, self.compositor_gpu)?; - *gpu_context.borrow_mut() = Some(new_context); - surface - } else { - let ctx_ref = gpu_context.borrow(); - let instance = &ctx_ref.as_ref().unwrap().instance; - create_surface(instance, window_handle.as_raw())? - }; - - let config = WgpuSurfaceConfig { - size: gpui::Size { - width: gpui::DevicePixels(self.surface_config.width as i32), - height: gpui::DevicePixels(self.surface_config.height as i32), - }, - transparent: self.surface_config.alpha_mode != wgpu::CompositeAlphaMode::Opaque, - preferred_present_mode: Some(self.surface_config.present_mode), - }; - let gpu_context = Rc::clone(gpu_context); - let ctx_ref = gpu_context.borrow(); - let context = ctx_ref.as_ref().expect("context should exist"); - - self.resources = None; - self.atlas.handle_device_lost(context); - - *self = Self::new_internal( - Some(gpu_context.clone()), - context, - surface, - config, - self.compositor_gpu, - self.atlas.clone(), - )?; - - log::info!("GPU recovery complete"); - Ok(()) - } -} - -fn instance_range(range: Range) -> Range { - range.start as u32..range.end as u32 -} - -#[cfg(not(target_family = "wasm"))] -fn create_surface( - instance: &wgpu::Instance, - raw_window_handle: raw_window_handle::RawWindowHandle, -) -> anyhow::Result> { - unsafe { - instance - .create_surface_unsafe(wgpu::SurfaceTargetUnsafe::RawHandle { - // Fall back to the display handle already provided via InstanceDescriptor::display. - raw_display_handle: None, - raw_window_handle, - }) - .map_err(|e| anyhow::anyhow!("{e}")) - } -} - -struct RenderingParameters { - path_sample_count: u32, - gamma_ratios: [f32; 4], - grayscale_enhanced_contrast: f32, - subpixel_enhanced_contrast: f32, -} - -impl RenderingParameters { - fn new(adapter: &wgpu::Adapter, surface_format: wgpu::TextureFormat) -> Self { - use std::env; - - let format_features = adapter.get_texture_format_features(surface_format); - let path_sample_count = [4, 2, 1] - .into_iter() - .find(|&n| format_features.flags.sample_count_supported(n)) - .unwrap_or(1); - - let gamma = env::var("ZED_FONTS_GAMMA") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(1.8_f32) - .clamp(1.0, 2.2); - let gamma_ratios = get_gamma_correction_ratios(gamma); - - let grayscale_enhanced_contrast = env::var("ZED_FONTS_GRAYSCALE_ENHANCED_CONTRAST") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(1.0_f32) - .max(0.0); - - let subpixel_enhanced_contrast = env::var("ZED_FONTS_SUBPIXEL_ENHANCED_CONTRAST") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(0.5_f32) - .max(0.0); - - Self { - path_sample_count, - gamma_ratios, - grayscale_enhanced_contrast, - subpixel_enhanced_contrast, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::{MonochromeSprite, PolychromeSprite, Quad, Shadow, SubpixelSprite, Underline}; - - #[test] - fn webgl_record_sizes_match_shader_word_strides() { - assert_eq!(std::mem::size_of::(), 64); - assert_eq!(std::mem::size_of::(), 46 * 4); - assert_eq!(std::mem::size_of::(), 34 * 4); - assert_eq!(std::mem::size_of::(), 36 * 4); - assert_eq!(std::mem::size_of::(), 4 * 4); - assert_eq!( - std::mem::size_of::>(), - 14 * 4 - ); - assert_eq!(std::mem::size_of::(), 22 * 4); - assert_eq!(std::mem::size_of::(), 34 * 4); - assert_eq!(std::mem::size_of::(), 34 * 4); - assert_eq!(std::mem::size_of::(), 30 * 4); - } -} diff --git a/crates/gpui_pre_windows/Cargo.lock b/crates/gpui_pre_windows/Cargo.lock deleted file mode 100644 index 2990f4d..0000000 --- a/crates/gpui_pre_windows/Cargo.lock +++ /dev/null @@ -1,4881 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "accesskit" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" -dependencies = [ - "enumn", - "uuid", -] - -[[package]] -name = "accesskit_consumer" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d10a236f96f87d70732e44520046785431ef01d5bcd6b041317bfadd2f88245" -dependencies = [ - "accesskit", - "hashbrown 0.16.1", -] - -[[package]] -name = "accesskit_windows" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "106c2b961215864d1c2e703ee63269c25c4e80a577ffb2c1017b9c17dcdf83a1" -dependencies = [ - "accesskit", - "accesskit_consumer", - "hashbrown 0.16.1", - "static_assertions", - "windows 0.62.2", - "windows-core 0.62.2", -] - -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "aligned" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" -dependencies = [ - "as-slice", -] - -[[package]] -name = "aligned-vec" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" -dependencies = [ - "equator", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" - -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" - -[[package]] -name = "arg_enum_proc_macro" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "as-slice" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" -dependencies = [ - "stable_deref_trait", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-compression" -version = "0.4.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0" -dependencies = [ - "compression-codecs", - "compression-core", - "futures-core", - "futures-io", - "pin-project-lite", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "atomic" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "av-scenechange" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" -dependencies = [ - "aligned", - "anyhow", - "arg_enum_proc_macro", - "arrayvec", - "log", - "num-rational", - "num-traits", - "pastey", - "rayon", - "thiserror 2.0.17", - "v_frame", - "y4m", -] - -[[package]] -name = "av1-grain" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" -dependencies = [ - "anyhow", - "arrayvec", - "log", - "nom", - "num-rational", - "v_frame", -] - -[[package]] -name = "avif-serialize" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link 0.2.1", -] - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.13.1", - "cexpr", - "clang-sys", - "itertools 0.11.0", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex 1.3.0", - "syn 2.0.117", -] - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bit_field" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" - -[[package]] -name = "bitstream-io" -version = "4.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" -dependencies = [ - "core2", -] - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "borsh" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" -dependencies = [ - "cfg_aliases", -] - -[[package]] -name = "built" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "bytemuck" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - -[[package]] -name = "bzip2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" -dependencies = [ - "libbz2-rs-sys", -] - -[[package]] -name = "cc" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex 2.0.1", -] - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "cgl" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" -dependencies = [ - "libc", -] - -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link 0.2.1", -] - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "cocoa" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" -dependencies = [ - "bitflags 1.3.2", - "block", - "cocoa-foundation", - "core-foundation 0.9.4", - "core-graphics 0.23.2", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" -dependencies = [ - "bitflags 1.3.2", - "block", - "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "libc", - "objc", -] - -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - -[[package]] -name = "compression-codecs" -version = "0.4.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" -dependencies = [ - "bzip2", - "compression-core", - "flate2", - "memchr", -] - -[[package]] -name = "compression-core" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "convert_case" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" -dependencies = [ - "bitflags 2.13.1", - "core-foundation 0.10.0", - "core-graphics-types 0.2.0", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-helmer-fork" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5" -dependencies = [ - "bitflags 2.13.1", - "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.13.1", - "core-foundation 0.10.0", - "libc", -] - -[[package]] -name = "core-graphics2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4416167a69126e617f8d0a214af0e3c1dbdeffcb100ddf72dcd1a1ac9893c146" -dependencies = [ - "bitflags 2.13.1", - "block", - "cfg-if", - "core-foundation 0.10.0", - "libc", -] - -[[package]] -name = "core-text" -version = "21.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130" -dependencies = [ - "core-foundation 0.10.0", - "core-graphics 0.24.0", - "foreign-types", - "libc", -] - -[[package]] -name = "core-video" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139679cc63eb9504bdbe37e37874b0247136177655f0008588781e90863afa62" -dependencies = [ - "block", - "core-foundation 0.10.0", - "core-graphics2", - "io-surface", - "libc", - "metal", -] - -[[package]] -name = "core2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" -dependencies = [ - "memchr", -] - -[[package]] -name = "core_maths" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" -dependencies = [ - "libm", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "ctor" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d83cb7e7a873830708d6b02a78cd36a592c6fa14bf267b68725103b85c0d77f" -dependencies = [ - "link-section", - "linktime-proc-macro", -] - -[[package]] -name = "data-url" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case 0.10.0", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.117", - "unicode-xid", -] - -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.48.0", -] - -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "dlib" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" -dependencies = [ - "libloading", -] - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dwrote" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b35532432acc8b19ceed096e35dfa088d3ea037fe4f3c085f1f97f33b4d02" -dependencies = [ - "lazy_static", - "libc", - "winapi", - "wio", -] - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "embed-resource" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" -dependencies = [ - "cc", - "memchr", - "rustc_version", - "toml", - "vswhom", - "winreg", -] - -[[package]] -name = "enumn" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "equator" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" -dependencies = [ - "equator-macro", -] - -[[package]] -name = "equator-macro" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "erased-serde" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "etagere" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" -dependencies = [ - "euclid", - "svg_fmt", -] - -[[package]] -name = "euclid" -version = "0.22.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" -dependencies = [ - "num-traits", -] - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "exr" -version = "1.74.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" -dependencies = [ - "bit_field", - "half", - "lebe", - "miniz_oxide", - "rayon-core", - "smallvec", - "zune-inflate", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "fax" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" -dependencies = [ - "fax_derive", -] - -[[package]] -name = "fax_derive" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" - -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - -[[package]] -name = "flate2" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" - -[[package]] -name = "float-ord" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" - -[[package]] -name = "float_next_after" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" - -[[package]] -name = "flume" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" -dependencies = [ - "fastrand", - "futures-core", - "futures-sink", - "spin 0.9.8", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "fontconfig-parser" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" -dependencies = [ - "roxmltree 0.20.0", -] - -[[package]] -name = "fontdb" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" -dependencies = [ - "fontconfig-parser", - "log", - "memmap2", - "slotmap", - "tinyvec", - "ttf-parser", -] - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "freetype-sys" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-concurrency" -version = "7.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6" -dependencies = [ - "fixedbitset", - "futures-core", - "futures-lite", - "pin-project", - "smallvec", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", - "wasip3", -] - -[[package]] -name = "gif" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" -dependencies = [ - "color_quant", - "weezl", -] - -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "gpui-pre" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8b0f6f593153b6eb84c336ea7ccf12a1fca186f0db99af628e72f85048c1d7b" -dependencies = [ - "accesskit", - "anyhow", - "async-channel", - "async-task", - "backtrace", - "bindgen", - "bitflags 2.13.1", - "chrono", - "core-video", - "ctor", - "derive_more", - "embed-resource", - "etagere", - "futures", - "futures-concurrency", - "getrandom 0.3.4", - "gpui-pre-collections", - "gpui-pre-http-client", - "gpui-pre-macros", - "gpui-pre-refineable", - "gpui-pre-scheduler", - "gpui-pre-shared-string", - "gpui-pre-sum-tree", - "gpui-pre-util", - "gpui-pre-util-macros", - "gpui-pre-ztracing", - "heapless", - "image", - "inventory", - "itertools 0.14.0", - "log", - "lyon", - "num_cpus", - "parking", - "parking_lot", - "pin-project", - "pollster 0.4.0", - "postage", - "profiling", - "proptest", - "rand 0.9.4", - "raw-window-handle", - "regex", - "resvg", - "schemars", - "seahash", - "serde", - "serde_json", - "slotmap", - "smallvec", - "spin 0.10.0", - "strum", - "taffy", - "thiserror 2.0.17", - "tracing", - "ttf-parser", - "url", - "usvg", - "uuid", - "waker-fn", - "web-time", - "windows 0.62.2", - "zed-font-kit", - "zed-scap", -] - -[[package]] -name = "gpui-pre-collections" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89c8efa2e51e368c8538a7be1ea9a12127ca03abe6cc01d9d3474e9ac4f53016" -dependencies = [ - "gpui-pre-util", - "indexmap", - "rustc-hash", -] - -[[package]] -name = "gpui-pre-derive-refineable" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a098d319acc9f84bf159944f96c5ea43a4f4cd7ed759f984acbd15719495aa0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "gpui-pre-http-client" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3495a45a28cb800c8626d2053406114bcaca26d390a4d4b183365b5bb8fdaf02" -dependencies = [ - "anyhow", - "async-compression", - "bytes", - "derive_more", - "futures", - "http", - "http-body", - "log", - "parking_lot", - "serde", - "serde_json", - "serde_urlencoded", - "url", -] - -[[package]] -name = "gpui-pre-macros" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be2db7b5097b4d523b2bce933604bcc5acdaf679bb9a150e8299e6c07efc29c" -dependencies = [ - "heck", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "gpui-pre-perf" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1db0c046b93c2a29120f8ee4c04bc80a4d7d6117d1164ef349faface8943491" -dependencies = [ - "gpui-pre-collections", - "serde", - "serde_json", -] - -[[package]] -name = "gpui-pre-refineable" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864e2e54a3029481dae5b6aae3ba2905f2fb1dfe66ced913b68c9ff527f8e327" -dependencies = [ - "gpui-pre-derive-refineable", -] - -[[package]] -name = "gpui-pre-scheduler" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b58a78c4e0c032900704ea49922641c534cf8bbf1bf22eda6ba00a76e88c6c3" -dependencies = [ - "async-task", - "backtrace", - "chrono", - "flume", - "futures", - "parking_lot", - "rand 0.9.4", - "web-time", -] - -[[package]] -name = "gpui-pre-shared-string" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82fc99fe88e44173758a500522d3f4059a6541da4b471af720e3f60cf34a2bc3" -dependencies = [ - "schemars", - "serde", - "smol_str", -] - -[[package]] -name = "gpui-pre-sum-tree" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "002baed852f20cef1188d3d5e0f749025fc718e4d6cea026b9077a9a6c10d042" -dependencies = [ - "gpui-pre-ztracing", - "heapless", - "log", - "rayon", - "tracing", -] - -[[package]] -name = "gpui-pre-util" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fe779c4cb00929aafcb2b307cd1240588aebb5d1eb328c59b80f77c05a41fad" -dependencies = [ - "anyhow", - "log", - "which", -] - -[[package]] -name = "gpui-pre-util-macros" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f0ccc4bccb6a31786095d15fc9f40d6a4c6a295522e3460331dd7e929ff2f79" -dependencies = [ - "gpui-pre-perf", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "gpui-pre-windows" -version = "0.3.3" -dependencies = [ - "accesskit", - "accesskit_windows", - "anyhow", - "dunce", - "etagere", - "futures", - "gpui-pre", - "gpui-pre-collections", - "gpui-pre-util", - "image", - "itertools 0.14.0", - "log", - "parking_lot", - "rand 0.9.4", - "raw-window-handle", - "smallvec", - "uuid", - "windows 0.62.2", - "windows-core 0.62.2", - "windows-numerics 0.3.1", - "windows-registry", - "zed-scap", -] - -[[package]] -name = "gpui-pre-zlog" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1416ea5f018e3a8a1c8be266332c443583f28f8f87379a84ab1e77622173ac0" -dependencies = [ - "anyhow", - "chrono", - "gpui-pre-collections", - "log", -] - -[[package]] -name = "gpui-pre-ztracing" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cc64a25a3cc4e4f1d8acb00074d3925339dae657c020083735738ba8af96bf7" -dependencies = [ - "gpui-pre-zlog", - "gpui-pre-ztracing-macro", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "gpui-pre-ztracing-macro" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f3ea75672348f37d94472e579f5979ee19b744d0fa5aaadc2fe129ccafa80b" - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - -[[package]] -name = "hash32" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" -dependencies = [ - "byteorder", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "foldhash 0.2.0", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heapless" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" -dependencies = [ - "hash32", - "stable_deref_trait", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "http" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" - -[[package]] -name = "icu_properties" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" - -[[package]] -name = "icu_provider" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "image" -version = "0.25.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" -dependencies = [ - "bytemuck", - "byteorder-lite", - "color_quant", - "exr", - "gif", - "image-webp", - "moxcms", - "num-traits", - "png 0.18.0", - "qoi", - "ravif", - "rayon", - "tiff", - "zune-core", - "zune-jpeg", -] - -[[package]] -name = "image-webp" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" -dependencies = [ - "byteorder-lite", - "quick-error 2.0.1", -] - -[[package]] -name = "imagesize" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" - -[[package]] -name = "imgref" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[package]] -name = "interpolate_name" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "inventory" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" -dependencies = [ - "rustversion", -] - -[[package]] -name = "io-surface" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "554b8c5d64ec09a3a520fe58e4d48a73e00ff32899cdcbe32a4877afd4968b8e" -dependencies = [ - "cgl", - "core-foundation 0.10.0", - "core-foundation-sys", - "leaky-cow", -] - -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" -dependencies = [ - "cfg-if", - "futures-util", - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "kurbo" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" -dependencies = [ - "arrayvec", - "euclid", - "polycool", - "smallvec", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leak" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd100e01f1154f2908dfa7d02219aeab25d0b9c7fa955164192e3245255a0c73" - -[[package]] -name = "leaky-cow" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a8225d44241fd324a8af2806ba635fc7c8a7e9a7de4d5cf3ef54e71f5926fc" -dependencies = [ - "leak", -] - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "lebe" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" - -[[package]] -name = "libbz2-rs-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "libfuzzer-sys" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" -dependencies = [ - "arbitrary", - "cc", -] - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link 0.2.1", -] - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "libredox" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" -dependencies = [ - "bitflags 2.13.1", - "libc", -] - -[[package]] -name = "link-section" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee1a0d6e252afe82e7bc2db42fba60e02ddf3b1accaf8cb21d96e34ba61f3d4" - -[[package]] -name = "linktime-proc-macro" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "348d0075b1fc163b26d72a7f75fc5141daf2fd1bdf128d873cbaf6785d495bdf" - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litemap" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -dependencies = [ - "serde_core", - "value-bag", -] - -[[package]] -name = "loop9" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" -dependencies = [ - "imgref", -] - -[[package]] -name = "lyon" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbcb7d54d54c8937364c9d41902d066656817dce1e03a44e5533afebd1ef4352" -dependencies = [ - "lyon_algorithms", - "lyon_tessellation", -] - -[[package]] -name = "lyon_algorithms" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c0829e28c4f336396f250d850c3987e16ce6db057ffe047ce0dd54aab6b647" -dependencies = [ - "lyon_path", - "num-traits", -] - -[[package]] -name = "lyon_geom" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e16770d760c7848b0c1c2d209101e408207a65168109509f8483837a36cf2e7" -dependencies = [ - "arrayvec", - "euclid", - "num-traits", -] - -[[package]] -name = "lyon_path" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aeca86bcfd632a15984ba029b539ffb811e0a70bf55e814ef8b0f54f506fdeb" -dependencies = [ - "lyon_geom", - "num-traits", -] - -[[package]] -name = "lyon_tessellation" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f586142e1280335b1bc89539f7c97dd80f08fc43e9ab1b74ef0a42b04aa353" -dependencies = [ - "float_next_after", - "lyon_path", - "num-traits", -] - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - -[[package]] -name = "maybe-rayon" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" -dependencies = [ - "cfg-if", - "rayon", -] - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "memmap2" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" -dependencies = [ - "libc", -] - -[[package]] -name = "metal" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15" -dependencies = [ - "bitflags 2.13.1", - "block", - "core-graphics-types 0.2.0", - "foreign-types", - "log", - "objc", - "paste", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "moxcms" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" -dependencies = [ - "num-traits", - "pxfm", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "noop_proc_macro" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" - -[[package]] -name = "ntapi" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" -dependencies = [ - "winapi", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num-bigint" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", - "objc_exception", -] - -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - -[[package]] -name = "objc_exception" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" -dependencies = [ - "cc", -] - -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link 0.2.1", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pastey" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" - -[[package]] -name = "pathfinder_geometry" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" -dependencies = [ - "log", - "pathfinder_simd", -] - -[[package]] -name = "pathfinder_simd" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4500030c302e4af1d423f36f3b958d1aecb6c04184356ed5a833bf6b60435777" -dependencies = [ - "rustc_version", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pico-args" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" - -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "png" -version = "0.17.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "png" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" -dependencies = [ - "bitflags 2.13.1", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "pollster" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" - -[[package]] -name = "pollster" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" - -[[package]] -name = "polycool" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "postage" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" -dependencies = [ - "atomic", - "crossbeam-queue", - "futures", - "log", - "parking_lot", - "pin-project", - "pollster 0.2.5", - "static_assertions", - "thiserror 1.0.69", -] - -[[package]] -name = "potential_utf" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" -dependencies = [ - "zerovec", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - -[[package]] -name = "proc-macro-crate" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "profiling" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" -dependencies = [ - "profiling-procmacros", -] - -[[package]] -name = "profiling-procmacros" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" -dependencies = [ - "quote", - "syn 2.0.117", -] - -[[package]] -name = "proptest" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" -dependencies = [ - "bit-set", - "bit-vec", - "bitflags 2.13.1", - "num-traits", - "proptest-macro", - "rand 0.9.4", - "rand_chacha 0.9.0", - "rand_xorshift", - "regex-syntax", - "rusty-fork", - "tempfile", - "unarray", -] - -[[package]] -name = "proptest-macro" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efaa288b896cb2b345da7b7f2110ab19e51565b83495b56fcec98a62f8b1f33e" -dependencies = [ - "convert_case 0.11.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pxfm" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" -dependencies = [ - "num-traits", -] - -[[package]] -name = "qoi" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - -[[package]] -name = "quick-xml" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rand_xorshift" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" -dependencies = [ - "rand_core 0.9.3", -] - -[[package]] -name = "rav1e" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" -dependencies = [ - "aligned-vec", - "arbitrary", - "arg_enum_proc_macro", - "arrayvec", - "av-scenechange", - "av1-grain", - "bitstream-io", - "built", - "cfg-if", - "interpolate_name", - "itertools 0.14.0", - "libc", - "libfuzzer-sys", - "log", - "maybe-rayon", - "new_debug_unreachable", - "noop_proc_macro", - "num-derive", - "num-traits", - "paste", - "profiling", - "rand 0.9.4", - "rand_chacha 0.9.0", - "simd_helpers", - "thiserror 2.0.17", - "v_frame", - "wasm-bindgen", -] - -[[package]] -name = "ravif" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" -dependencies = [ - "avif-serialize", - "imgref", - "loop9", - "quick-error 2.0.1", - "rav1e", - "rayon", - "rgb", -] - -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - -[[package]] -name = "rayon" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.13.1", -] - -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.16", - "libredox", - "thiserror 1.0.69", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "resvg" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b563218631706d614e23059436526d005b50ab5f2d506b55a17eb65c5eb83419" -dependencies = [ - "gif", - "image-webp", - "log", - "pico-args", - "rgb", - "svgtypes", - "tiny-skia", - "usvg", - "zune-jpeg", -] - -[[package]] -name = "rgb" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "roxmltree" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" - -[[package]] -name = "roxmltree" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" -dependencies = [ - "memchr", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.13.1", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "rusty-fork" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" -dependencies = [ - "fnv", - "quick-error 1.2.3", - "tempfile", - "wait-timeout", -] - -[[package]] -name = "rustybuzz" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" -dependencies = [ - "bitflags 2.13.1", - "bytemuck", - "core_maths", - "log", - "smallvec", - "ttf-parser", - "unicode-bidi-mirroring", - "unicode-ccc", - "unicode-properties", - "unicode-script", -] - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schemars" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" -dependencies = [ - "dyn-clone", - "indexmap", - "ref-cast", - "schemars_derive", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.117", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "screencapturekit" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5eeeb57ac94960cfe5ff4c402be6585ae4c8d29a2cf41b276048c2e849d64e" -dependencies = [ - "screencapturekit-sys", -] - -[[package]] -name = "screencapturekit-sys" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22411b57f7d49e7fe08025198813ee6fd65e1ee5eff4ebc7880c12c82bde4c60" -dependencies = [ - "block", - "dispatch", - "objc", - "objc-foundation", - "objc_id", - "once_cell", -] - -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_fmt" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_json" -version = "1.0.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "indexmap", - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "simd_helpers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" -dependencies = [ - "quote", -] - -[[package]] -name = "simplecss" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" -dependencies = [ - "log", -] - -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "slotmap" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" -dependencies = [ - "version_check", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "smol_str" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" -dependencies = [ - "borsh", - "serde_core", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spin" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" -dependencies = [ - "lock_api", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strict-num" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" -dependencies = [ - "float-cmp", -] - -[[package]] -name = "strum" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "sval" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d94c4464e595f0284970fd9c7e9013804d035d4a61ab74b113242c874c05814d" - -[[package]] -name = "sval_buffer" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0f46e34b20a39e6a2bf02b926983149b3af6609fd1ee8a6e63f6f340f3e2164" -dependencies = [ - "sval", - "sval_ref", -] - -[[package]] -name = "sval_dynamic" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d0970e53c92ab5381d3b2db1828da8af945954d4234225f6dd9c3afbcef3f5" -dependencies = [ - "sval", -] - -[[package]] -name = "sval_fmt" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43e5e6e1613e1e7fc2e1a9fdd709622e54c122ceb067a60d170d75efd491a839" -dependencies = [ - "itoa", - "ryu", - "sval", -] - -[[package]] -name = "sval_json" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aec382f7bfa6e367b23c9611f129b94eb7daaf3d8fae45a8d0a0211eb4d4c8e6" -dependencies = [ - "itoa", - "ryu", - "sval", -] - -[[package]] -name = "sval_nested" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3049d0f99ce6297f8f7d9953b35a0103b7584d8f638de40e64edb7105fa578ae" -dependencies = [ - "sval", - "sval_buffer", - "sval_ref", -] - -[[package]] -name = "sval_ref" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f88913e77506085c0a8bf6912bb6558591a960faf5317df6c1d9b227224ca6e1" -dependencies = [ - "sval", -] - -[[package]] -name = "sval_serde" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f579fd7254f4be6cd7b450034f856b78523404655848789c451bacc6aa8b387d" -dependencies = [ - "serde_core", - "sval", - "sval_nested", -] - -[[package]] -name = "svg_fmt" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" - -[[package]] -name = "svgtypes" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d" -dependencies = [ - "kurbo", - "siphasher", -] - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "sysinfo" -version = "0.31.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" -dependencies = [ - "core-foundation-sys", - "libc", - "memchr", - "ntapi", - "rayon", - "windows 0.57.0", -] - -[[package]] -name = "taffy" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c034e05f6ee85a12daa63863c2245797715075c70649947aa0da54f3f2ab1d0f" -dependencies = [ - "arrayvec", - "serde", - "slotmap", - "smallvec", -] - -[[package]] -name = "tao-core-video-sys" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271450eb289cb4d8d0720c6ce70c72c8c858c93dd61fc625881616752e6b98f6" -dependencies = [ - "cfg-if", - "core-foundation-sys", - "libc", - "objc", -] - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.1", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" -dependencies = [ - "thiserror-impl 2.0.17", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "tiff" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" -dependencies = [ - "fax", - "flate2", - "half", - "quick-error 2.0.1", - "weezl", - "zune-jpeg", -] - -[[package]] -name = "tiny-skia" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" -dependencies = [ - "arrayref", - "arrayvec", - "bytemuck", - "cfg-if", - "log", - "png 0.17.16", - "tiny-skia-path", -] - -[[package]] -name = "tiny-skia-path" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" -dependencies = [ - "arrayref", - "bytemuck", - "strict-num", -] - -[[package]] -name = "tinystr" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "toml" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.23.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_writer" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" - -[[package]] -name = "tracing" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tracing-core" -version = "0.1.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "ttf-parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" -dependencies = [ - "core_maths", -] - -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - -[[package]] -name = "unarray" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" - -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - -[[package]] -name = "unicode-bidi-mirroring" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" - -[[package]] -name = "unicode-ccc" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-properties" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" - -[[package]] -name = "unicode-script" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" - -[[package]] -name = "unicode-segmentation" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" - -[[package]] -name = "unicode-vo" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "usvg" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e419dff010bb12512b0ae9e3d2f318dfbdf0167fde7eb05465134d4e8756076f" -dependencies = [ - "base64", - "data-url", - "flate2", - "fontdb", - "imagesize", - "kurbo", - "log", - "pico-args", - "roxmltree 0.21.1", - "rustybuzz", - "simplecss", - "siphasher", - "strict-num", - "svgtypes", - "tiny-skia-path", - "unicode-bidi", - "unicode-script", - "unicode-vo", - "xmlwriter", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" -dependencies = [ - "getrandom 0.3.4", - "js-sys", - "serde", - "sha1_smol", - "wasm-bindgen", -] - -[[package]] -name = "v_frame" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" -dependencies = [ - "aligned-vec", - "num-traits", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "value-bag" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" -dependencies = [ - "value-bag-serde1", - "value-bag-sval2", -] - -[[package]] -name = "value-bag-serde1" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16530907bfe2999a1773ca5900a65101e092c70f642f25cc23ca0c43573262c5" -dependencies = [ - "erased-serde", - "serde_core", - "serde_fmt", -] - -[[package]] -name = "value-bag-sval2" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d00ae130edd690eaa877e4f40605d534790d1cf1d651e7685bd6a144521b251f" -dependencies = [ - "sval", - "sval_buffer", - "sval_dynamic", - "sval_fmt", - "sval_json", - "sval_ref", - "sval_serde", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vswhom" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" -dependencies = [ - "libc", - "vswhom-sys", -] - -[[package]] -name = "vswhom-sys" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "wait-timeout" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" -dependencies = [ - "libc", -] - -[[package]] -name = "waker-fn" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen 0.46.0", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.117", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.13.1", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "weezl" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" - -[[package]] -name = "which" -version = "8.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3ef584124b911bcc3875c2f1472e80f24361ceb789bd1c62b3e9a3df9ff43c" -dependencies = [ - "libc", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" -dependencies = [ - "windows-core 0.57.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections 0.2.0", - "windows-core 0.61.2", - "windows-future 0.2.1", - "windows-link 0.1.3", - "windows-numerics 0.2.0", -] - -[[package]] -name = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections 0.3.2", - "windows-core 0.62.2", - "windows-future 0.3.2", - "windows-numerics 0.3.1", -] - -[[package]] -name = "windows-capture" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a4df73e95feddb9ec1a7e9c2ca6323b8c97d5eeeff78d28f1eccdf19c882b24" -dependencies = [ - "parking_lot", - "rayon", - "thiserror 2.0.17", - "windows 0.61.3", - "windows-future 0.2.1", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core 0.61.2", -] - -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core 0.62.2", -] - -[[package]] -name = "windows-core" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" -dependencies = [ - "windows-implement 0.57.0", - "windows-interface 0.57.0", - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading 0.1.0", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core 0.62.2", - "windows-link 0.2.1", - "windows-threading 0.2.1", -] - -[[package]] -name = "windows-implement" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-interface" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", -] - -[[package]] -name = "windows-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core 0.62.2", - "windows-link 0.2.1", -] - -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winnow" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" -dependencies = [ - "memchr", -] - -[[package]] -name = "winreg" -version = "0.55.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" -dependencies = [ - "cfg-if", - "windows-sys 0.59.0", -] - -[[package]] -name = "wio" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" -dependencies = [ - "winapi", -] - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.13.1", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "writeable" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" - -[[package]] -name = "x11" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "xcb" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f07c123b796139bfe0603e654eaf08e132e52387ba95b252c78bad3640ba37ea" -dependencies = [ - "bitflags 1.3.2", - "libc", - "quick-xml", - "x11", -] - -[[package]] -name = "xmlwriter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" - -[[package]] -name = "y4m" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" - -[[package]] -name = "yeslogic-fontconfig-sys" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503a066b4c037c440169d995b869046827dbc71263f6e8f3be6d77d4f3229dbd" -dependencies = [ - "dlib", - "once_cell", - "pkg-config", -] - -[[package]] -name = "yoke" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zed-font-kit" -version = "0.14.1-zed" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3898e450f36f852edda72e3f985c34426042c4951790b23b107f93394f9bff5" -dependencies = [ - "bitflags 2.13.1", - "byteorder", - "core-foundation 0.10.0", - "core-graphics 0.24.0", - "core-text", - "dirs", - "dwrote", - "float-ord", - "freetype-sys", - "lazy_static", - "libc", - "log", - "pathfinder_geometry", - "pathfinder_simd", - "walkdir", - "winapi", - "yeslogic-fontconfig-sys", -] - -[[package]] -name = "zed-scap" -version = "0.0.8-zed" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b338d705ae33a43ca00287c11129303a7a0aa57b101b72a1c08c863f698ac8" -dependencies = [ - "anyhow", - "cocoa", - "core-graphics-helmer-fork", - "log", - "objc", - "rand 0.8.6", - "screencapturekit", - "screencapturekit-sys", - "sysinfo", - "tao-core-video-sys", - "windows 0.61.3", - "windows-capture", - "x11", - "xcb", -] - -[[package]] -name = "zerocopy" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f212a141d820099d57ffafb9569be9617a6f27d3dc881fbee8fb56642f917a9" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" - -[[package]] -name = "zune-core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" - -[[package]] -name = "zune-inflate" -version = "0.2.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "zune-jpeg" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" -dependencies = [ - "zune-core", -] diff --git a/crates/gpui_pre_windows/Cargo.toml b/crates/gpui_pre_windows/Cargo.toml deleted file mode 100644 index e5cee6e..0000000 --- a/crates/gpui_pre_windows/Cargo.toml +++ /dev/null @@ -1,238 +0,0 @@ -# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO -# -# When uploading crates to the registry Cargo will automatically -# "normalize" Cargo.toml files for maximal compatibility -# with all versions of Cargo and also rewrite `path` dependencies -# to registry (e.g., crates.io) dependencies. -# -# If you are reading this file be aware that the original Cargo.toml -# will likely look very different (and much more reasonable). -# See Cargo.toml.orig for the original contents. - -[package] -edition = "2024" -name = "gpui-pre-windows" -version = "0.3.3" -build = "build.rs" -publish = true -autolib = false -autobins = false -autoexamples = false -autotests = false -autobenches = false -description = "Zed's `gpui_windows` crate (gpui-pre snapshot of zed@5b055fa)" -readme = false -license = "Apache-2.0" -repository = "https://github.com/zed-industries/zed" -resolver = "2" - -[package.metadata.cargo-shear] -ignored = ["scap"] - -[package.metadata.gpui-pre] -zed-crate = "gpui_windows" -zed-version = "0.1.0" -zed-rev = "5b055fa789a8b8d38ac951a6e0cde272f66b4495" - -[features] -default = ["gpui/default"] -screen-capture = [ - "gpui/screen-capture", - "scap", -] -test-support = ["gpui/test-support"] - -[lib] -name = "gpui_windows" -path = "src/gpui_windows.rs" - -[dependencies.gpui] -version = "=0.3.3" -default-features = false -package = "gpui-pre" - -[target.'cfg(target_os = "windows")'.dependencies.accesskit] -version = "0.24.0" -features = ["enumn"] - -[target.'cfg(target_os = "windows")'.dependencies.accesskit_windows] -version = "0.34" - -[target.'cfg(target_os = "windows")'.dependencies.anyhow] -version = "1.0.86" - -[target.'cfg(target_os = "windows")'.dependencies.collections] -version = "=0.3.3" -package = "gpui-pre-collections" - -[target.'cfg(target_os = "windows")'.dependencies.dunce] -version = "1.0" - -[target.'cfg(target_os = "windows")'.dependencies.etagere] -version = "0.2" - -[target.'cfg(target_os = "windows")'.dependencies.futures] -version = "0.3.32" - -[target.'cfg(target_os = "windows")'.dependencies.gpui_util] -version = "=0.3.3" -package = "gpui-pre-util" - -[target.'cfg(target_os = "windows")'.dependencies.image] -version = "0.25.1" -features = [ - "bmp", - "dds", - "exr", - "ff", - "gif", - "hdr", - "ico", - "jpeg", - "png", - "pnm", - "qoi", - "rayon", - "tga", - "tiff", - "webp", -] -default-features = false - -[target.'cfg(target_os = "windows")'.dependencies.itertools] -version = "0.14.0" - -[target.'cfg(target_os = "windows")'.dependencies.log] -version = "0.4.16" -features = [ - "kv_unstable_serde", - "serde", -] - -[target.'cfg(target_os = "windows")'.dependencies.parking_lot] -version = "0.12.1" - -[target.'cfg(target_os = "windows")'.dependencies.rand] -version = "0.9" - -[target.'cfg(target_os = "windows")'.dependencies.raw-window-handle] -version = "0.6" - -[target.'cfg(target_os = "windows")'.dependencies.scap] -version = "0.0.8-zed" -optional = true -default-features = false -package = "zed-scap" - -[target.'cfg(target_os = "windows")'.dependencies.smallvec] -version = "1.6" -features = [ - "union", - "const_new", -] - -[target.'cfg(target_os = "windows")'.dependencies.uuid] -version = "1.1.2" -features = [ - "v4", - "v5", - "v7", - "serde", -] - -[target.'cfg(target_os = "windows")'.dependencies.windows] -version = "0.62" -features = [ - "Data_Xml_Dom", - "Foundation_Numerics", - "Globalization_DateTimeFormatting", - "Storage_Search", - "Storage_Streams", - "System_Threading", - "UI_Notifications", - "UI_ViewManagement", - "Wdk_System_SystemServices", - "Win32_Foundation", - "Win32_Globalization", - "Win32_Graphics_Direct3D", - "Win32_Graphics_Direct3D11", - "Win32_Graphics_Direct3D_Fxc", - "Win32_Graphics_DirectComposition", - "Win32_Graphics_DirectWrite", - "Win32_Graphics_DirectManipulation", - "Win32_Graphics_Dwm", - "Win32_Graphics_Dxgi", - "Win32_Graphics_Dxgi_Common", - "Win32_Graphics_Gdi", - "Win32_Graphics_Imaging", - "Win32_Graphics_Hlsl", - "Win32_Networking_WinSock", - "Win32_Security", - "Win32_Security_Credentials", - "Win32_Security_Cryptography", - "Win32_Storage_FileSystem", - "Win32_Storage_Packaging_Appx", - "Win32_System_Com", - "Win32_System_Com_StructuredStorage", - "Win32_System_Console", - "Win32_System_Diagnostics_Debug", - "Win32_System_DataExchange", - "Win32_System_IO", - "Win32_System_JobObjects", - "Win32_System_LibraryLoader", - "Win32_System_Memory", - "Win32_System_Ole", - "Win32_System_Performance", - "Win32_System_Pipes", - "Win32_System_RestartManager", - "Win32_System_SystemInformation", - "Win32_System_SystemServices", - "Win32_System_Threading", - "Win32_System_Variant", - "Win32_System_WinRT", - "Win32_UI_Controls", - "Win32_UI_HiDpi", - "Win32_UI_Input_Ime", - "Win32_UI_Input_KeyboardAndMouse", - "Win32_UI_Input_Pointer", - "Win32_UI_Shell", - "Win32_UI_Shell_Common", - "Win32_UI_Shell_PropertiesSystem", - "Win32_UI_WindowsAndMessaging", - "Win32_Media", -] - -[target.'cfg(target_os = "windows")'.dependencies.windows-core] -version = "0.62" - -[target.'cfg(target_os = "windows")'.dependencies.windows-numerics] -version = "0.3" - -[target.'cfg(target_os = "windows")'.dependencies.windows-registry] -version = "0.6.0" - -[target.'cfg(target_os = "windows")'.build-dependencies.windows-registry] -version = "0.6.0" - -[lints.clippy] -dbg_macro = "deny" -declare_interior_mutable_const = "deny" -disallowed_methods = "deny" -large_enum_variant = "allow" -let_underscore_future = "allow" -nonminimal_bool = "allow" -redundant_clone = "deny" -single_range_in_vec_init = "allow" -todo = "deny" -too_many_arguments = "allow" -type_complexity = "allow" - -[lints.clippy.style] -level = "allow" -priority = -1 - -[lints.rust.unexpected_cfgs] -level = "allow" -priority = 0 - -[workspace] diff --git a/crates/gpui_pre_windows/LICENSE-APACHE b/crates/gpui_pre_windows/LICENSE-APACHE deleted file mode 100644 index 461a0fe..0000000 --- a/crates/gpui_pre_windows/LICENSE-APACHE +++ /dev/null @@ -1,222 +0,0 @@ -Copyright 2022 - 2025 Zed Industries, Inc. - - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - - http://www.apache.org/licenses/LICENSE-2.0 - - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - - - -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 diff --git a/crates/gpui_pre_windows/build.rs b/crates/gpui_pre_windows/build.rs deleted file mode 100644 index 1db9571..0000000 --- a/crates/gpui_pre_windows/build.rs +++ /dev/null @@ -1,242 +0,0 @@ -#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")] - -fn main() { - #[cfg(target_os = "windows")] - { - // Compile HLSL shaders - #[cfg(not(debug_assertions))] - compile_shaders(); - } -} - -#[cfg(all(target_os = "windows", not(debug_assertions)))] -mod shader_compilation { - use std::{ - fs, - io::Write, - path::{Path, PathBuf}, - process::{self, Command}, - }; - - pub fn compile_shaders() { - let shader_path = - PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("src/shaders.hlsl"); - let out_dir = std::env::var("OUT_DIR").unwrap(); - - println!("cargo:rerun-if-changed={}", shader_path.display()); - - // Check if fxc.exe is available - let fxc_path = find_fxc_compiler(); - - // Define all modules - let modules = [ - "quad", - "shadow", - "path_rasterization", - "path_sprite", - "underline", - "monochrome_sprite", - "subpixel_sprite", - "polychrome_sprite", - ]; - - let rust_binding_path = format!("{}/shaders_bytes.rs", out_dir); - if Path::new(&rust_binding_path).exists() { - fs::remove_file(&rust_binding_path) - .expect("Failed to remove existing Rust binding file"); - } - for module in modules { - compile_shader_for_module( - module, - &out_dir, - &fxc_path, - shader_path.to_str().unwrap(), - &rust_binding_path, - ); - } - - { - let shader_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()) - .join("src/color_text_raster.hlsl"); - compile_shader_for_module( - "emoji_rasterization", - &out_dir, - &fxc_path, - shader_path.to_str().unwrap(), - &rust_binding_path, - ); - } - } - - /// Locate `binary` in the newest installed Windows SDK. - pub fn find_latest_windows_sdk_binary( - binary: &str, - ) -> Result, Box> { - let key = windows_registry::LOCAL_MACHINE - .open("SOFTWARE\\WOW6432Node\\Microsoft\\Microsoft SDKs\\Windows\\v10.0")?; - - let install_folder: String = key.get_string("InstallationFolder")?; // "C:\Program Files (x86)\Windows Kits\10\" - let install_folder_bin = Path::new(&install_folder).join("bin"); - - let mut versions: Vec<_> = std::fs::read_dir(&install_folder_bin)? - .flatten() - .filter(|entry| entry.path().is_dir()) - .filter_map(|entry| entry.file_name().into_string().ok()) - .collect(); - - versions.sort_by_key(|s| { - s.split('.') - .filter_map(|p| p.parse().ok()) - .collect::>() - }); - - let arch = match std::env::consts::ARCH { - "x86_64" => "x64", - "aarch64" => "arm64", - _ => Err(format!( - "Unsupported architecture: {}", - std::env::consts::ARCH - ))?, - }; - - if let Some(highest_version) = versions.last() { - return Ok(Some( - install_folder_bin - .join(highest_version) - .join(arch) - .join(binary), - )); - } - - Ok(None) - } - - /// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler. - fn find_fxc_compiler() -> String { - // Check environment variable - if let Ok(path) = std::env::var("GPUI_FXC_PATH") - && Path::new(&path).exists() - { - return path; - } - - // Try to find in PATH - // NOTE: This has to be `where.exe` on Windows, not `where`, it must be ended with `.exe` - if let Ok(output) = std::process::Command::new("where.exe") - .arg("fxc.exe") - .output() - && output.status.success() - { - let path = String::from_utf8_lossy(&output.stdout); - return path.trim().to_string(); - } - - if let Ok(Some(path)) = find_latest_windows_sdk_binary("fxc.exe") { - return path.to_string_lossy().into_owned(); - } - - panic!("Failed to find fxc.exe"); - } - - fn compile_shader_for_module( - module: &str, - out_dir: &str, - fxc_path: &str, - shader_path: &str, - rust_binding_path: &str, - ) { - // Compile vertex shader - let output_file = format!("{}/{}_vs.h", out_dir, module); - let const_name = format!("{}_VERTEX_BYTES", module.to_uppercase()); - compile_shader_impl( - fxc_path, - &format!("{module}_vertex"), - &output_file, - &const_name, - shader_path, - "vs_4_1", - ); - generate_rust_binding(&const_name, &output_file, rust_binding_path); - - // Compile fragment shader - let output_file = format!("{}/{}_ps.h", out_dir, module); - let const_name = format!("{}_FRAGMENT_BYTES", module.to_uppercase()); - compile_shader_impl( - fxc_path, - &format!("{module}_fragment"), - &output_file, - &const_name, - shader_path, - "ps_4_1", - ); - generate_rust_binding(&const_name, &output_file, rust_binding_path); - } - - fn compile_shader_impl( - fxc_path: &str, - entry_point: &str, - output_path: &str, - var_name: &str, - shader_path: &str, - target: &str, - ) { - let output = Command::new(fxc_path) - .args([ - "/T", - target, - "/E", - entry_point, - "/Fh", - output_path, - "/Vn", - var_name, - "/O3", - shader_path, - ]) - .output(); - - match output { - Ok(result) => { - if result.status.success() { - return; - } - println!( - "cargo::error=Shader compilation failed for {}:\n{}", - entry_point, - String::from_utf8_lossy(&result.stderr) - ); - process::exit(1); - } - Err(e) => { - println!("cargo::error=Failed to run fxc for {}: {}", entry_point, e); - process::exit(1); - } - } - } - - fn generate_rust_binding(const_name: &str, head_file: &str, output_path: &str) { - let header_content = fs::read_to_string(head_file).expect("Failed to read header file"); - let const_definition = { - let global_var_start = header_content.find("const BYTE").unwrap(); - let global_var = &header_content[global_var_start..]; - let equal = global_var.find('=').unwrap(); - global_var[equal + 1..].trim() - }; - let rust_binding = format!( - "const {}: &[u8] = &{}\n", - const_name, - const_definition.replace('{', "[").replace('}', "]") - ); - let mut options = fs::OpenOptions::new() - .create(true) - .append(true) - .open(output_path) - .expect("Failed to open Rust binding file"); - options - .write_all(rust_binding.as_bytes()) - .expect("Failed to write Rust binding file"); - } -} - -#[cfg(all(target_os = "windows", not(debug_assertions)))] -use shader_compilation::compile_shaders; diff --git a/crates/gpui_pre_windows/src/alpha_correction.hlsl b/crates/gpui_pre_windows/src/alpha_correction.hlsl deleted file mode 100644 index 5a34a4e..0000000 --- a/crates/gpui_pre_windows/src/alpha_correction.hlsl +++ /dev/null @@ -1,49 +0,0 @@ -// Adapted from https://github.com/microsoft/terminal/blob/1283c0f5b99a2961673249fa77c6b986efb5086c/src/renderer/atlas/dwrite.hlsl -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -float color_brightness(float3 color) { - // REC. 601 luminance coefficients for perceived brightness - return dot(color, float3(0.30f, 0.59f, 0.11f)); -} - -float light_on_dark_contrast(float enhancedContrast, float3 color) { - float brightness = color_brightness(color); - float multiplier = saturate(4.0f * (0.75f - brightness)); - return enhancedContrast * multiplier; -} - -float enhance_contrast(float alpha, float k) { - return alpha * (k + 1.0f) / (alpha * k + 1.0f); -} - -float3 enhance_contrast3(float3 alpha, float k) { - return alpha * (k + 1.0f) / (alpha * k + 1.0f); -} - -float apply_alpha_correction(float a, float b, float4 g) { - float brightness_adjustment = g.x * b + g.y; - float correction = brightness_adjustment * a + (g.z * b + g.w); - return a + a * (1.0f - a) * correction; -} - -float3 apply_alpha_correction3(float3 a, float3 b, float4 g) { - float3 brightness_adjustment = g.x * b + g.y; - float3 correction = brightness_adjustment * a + (g.z * b + g.w); - return a + a * (1.0f - a) * correction; -} - -float apply_contrast_and_gamma_correction(float sample, float3 color, float enhanced_contrast_factor, float4 gamma_ratios) { - float enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color); - float brightness = color_brightness(color); - - float contrasted = enhance_contrast(sample, enhanced_contrast); - return apply_alpha_correction(contrasted, brightness, gamma_ratios); -} - -float3 apply_contrast_and_gamma_correction3(float3 sample, float3 color, float enhanced_contrast_factor, float4 gamma_ratios) { - float enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color); - - float3 contrasted = enhance_contrast3(sample, enhanced_contrast); - return apply_alpha_correction3(contrasted, color, gamma_ratios); -} diff --git a/crates/gpui_pre_windows/src/clipboard.rs b/crates/gpui_pre_windows/src/clipboard.rs deleted file mode 100644 index cd0694a..0000000 --- a/crates/gpui_pre_windows/src/clipboard.rs +++ /dev/null @@ -1,388 +0,0 @@ -use std::sync::LazyLock; - -use anyhow::Result; -use collections::FxHashMap; -use itertools::Itertools; -use windows::Win32::{ - Foundation::{HANDLE, HGLOBAL}, - System::{ - DataExchange::{ - CloseClipboard, CountClipboardFormats, EmptyClipboard, EnumClipboardFormats, - GetClipboardData, GetClipboardFormatNameW, OpenClipboard, RegisterClipboardFormatW, - SetClipboardData, - }, - Memory::{GMEM_MOVEABLE, GlobalAlloc, GlobalLock, GlobalSize, GlobalUnlock}, - Ole::{CF_DIB, CF_HDROP, CF_UNICODETEXT}, - }, - UI::Shell::{DragQueryFileW, HDROP}, -}; -use windows::core::{Owned, PCWSTR}; - -use gpui::{ - ClipboardEntry, ClipboardItem, ClipboardString, ExternalPaths, Image, ImageFormat, hash, -}; - -const DRAGDROP_GET_FILES_COUNT: u32 = 0xFFFFFFFF; - -static CLIPBOARD_HASH_FORMAT: LazyLock = - LazyLock::new(|| register_clipboard_format(windows::core::w!("GPUI internal text hash"))); -static CLIPBOARD_METADATA_FORMAT: LazyLock = - LazyLock::new(|| register_clipboard_format(windows::core::w!("GPUI internal metadata"))); -static CLIPBOARD_SVG_FORMAT: LazyLock = - LazyLock::new(|| register_clipboard_format(windows::core::w!("image/svg+xml"))); -static CLIPBOARD_GIF_FORMAT: LazyLock = - LazyLock::new(|| register_clipboard_format(windows::core::w!("GIF"))); -static CLIPBOARD_PNG_FORMAT: LazyLock = - LazyLock::new(|| register_clipboard_format(windows::core::w!("PNG"))); -static CLIPBOARD_JPG_FORMAT: LazyLock = - LazyLock::new(|| register_clipboard_format(windows::core::w!("JFIF"))); - -static IMAGE_FORMATS_MAP: LazyLock> = LazyLock::new(|| { - let mut map = FxHashMap::default(); - map.insert(*CLIPBOARD_PNG_FORMAT, ImageFormat::Png); - map.insert(*CLIPBOARD_GIF_FORMAT, ImageFormat::Gif); - map.insert(*CLIPBOARD_JPG_FORMAT, ImageFormat::Jpeg); - map.insert(*CLIPBOARD_SVG_FORMAT, ImageFormat::Svg); - map -}); - -fn register_clipboard_format(format: PCWSTR) -> u32 { - let ret = unsafe { RegisterClipboardFormatW(format) }; - if ret == 0 { - panic!( - "Error when registering clipboard format: {}", - std::io::Error::last_os_error() - ); - } - log::debug!( - "Registered clipboard format {} as {}", - unsafe { format.display() }, - ret - ); - ret -} - -fn get_clipboard_data(format: u32) -> Option { - let global = HGLOBAL(unsafe { GetClipboardData(format).ok() }?.0); - LockedGlobal::lock(global) -} - -pub(crate) fn write_to_clipboard(item: ClipboardItem) { - let Some(_clip) = ClipboardGuard::open() else { - return; - }; - - let result: Result<()> = (|| { - unsafe { EmptyClipboard()? }; - for entry in item.entries() { - match entry { - ClipboardEntry::String(string) => write_string(string)?, - ClipboardEntry::Image(image) => write_image(image)?, - ClipboardEntry::ExternalPaths(_) => {} - } - } - Ok(()) - })(); - - if let Err(e) = result { - log::error!("Failed to write to clipboard: {e}"); - } -} - -pub(crate) fn read_from_clipboard() -> Option { - let _clip = ClipboardGuard::open()?; - - let mut entries = Vec::new(); - let mut have_text = false; - let mut have_image = false; - let mut have_files = false; - - let count = unsafe { CountClipboardFormats() }; - let mut format = 0; - for _ in 0..count { - format = unsafe { EnumClipboardFormats(format) }; - - if !have_text && format == CF_UNICODETEXT.0 as u32 { - if let Some(entry) = read_string() { - entries.push(entry); - have_text = true; - } - } else if !have_image && is_image_format(format) { - if let Some(entry) = read_image(format) { - entries.push(entry); - have_image = true; - } - } else if !have_files && format == CF_HDROP.0 as u32 { - if let Some(entry) = read_files() { - entries.push(entry); - have_files = true; - } - } - } - - if entries.is_empty() { - log_unsupported_clipboard_formats(); - return None; - } - Some(ClipboardItem { entries }) -} - -pub(crate) fn with_file_names(hdrop: HDROP, mut f: F) -where - F: FnMut(String), -{ - let file_count = unsafe { DragQueryFileW(hdrop, DRAGDROP_GET_FILES_COUNT, None) }; - for file_index in 0..file_count { - let filename_length = unsafe { DragQueryFileW(hdrop, file_index, None) } as usize; - let mut buffer = vec![0u16; filename_length + 1]; - let ret = unsafe { DragQueryFileW(hdrop, file_index, Some(buffer.as_mut_slice())) }; - if ret == 0 { - log::error!("unable to read file name of dragged file"); - continue; - } - match String::from_utf16(&buffer[0..filename_length]) { - Ok(file_name) => f(file_name), - Err(e) => log::error!("dragged file name is not UTF-16: {}", e), - } - } -} - -fn set_clipboard_bytes(data: &[T], format: u32) -> Result<()> { - unsafe { - let global = Owned::new(GlobalAlloc(GMEM_MOVEABLE, std::mem::size_of_val(data))?); - let ptr = GlobalLock(*global); - anyhow::ensure!(!ptr.is_null(), "GlobalLock returned null"); - std::ptr::copy_nonoverlapping(data.as_ptr(), ptr as _, data.len()); - GlobalUnlock(*global).ok(); - SetClipboardData(format, Some(HANDLE(global.0)))?; - // SetClipboardData succeeded — the system now owns the memory. - std::mem::forget(global); - } - Ok(()) -} - -fn get_clipboard_string(format: u32) -> Option { - let locked = get_clipboard_data(format)?; - let bytes = locked.as_bytes(); - let words_len = bytes.len() / std::mem::size_of::(); - if words_len == 0 { - return Some(String::new()); - } - let slice = unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const u16, words_len) }; - let actual_len = slice.iter().position(|&c| c == 0).unwrap_or(words_len); - Some(String::from_utf16_lossy(&slice[..actual_len])) -} - -fn is_image_format(format: u32) -> bool { - IMAGE_FORMATS_MAP.contains_key(&format) || format == CF_DIB.0 as u32 -} - -fn write_string(item: &ClipboardString) -> Result<()> { - let wide: Vec = item.text.encode_utf16().chain(Some(0)).collect_vec(); - set_clipboard_bytes(&wide, CF_UNICODETEXT.0 as u32)?; - - if let Some(metadata) = item.metadata.as_ref() { - let hash_bytes = ClipboardString::text_hash(&item.text).to_ne_bytes(); - set_clipboard_bytes(&hash_bytes, *CLIPBOARD_HASH_FORMAT)?; - - let wide: Vec = metadata.encode_utf16().chain(Some(0)).collect_vec(); - set_clipboard_bytes(&wide, *CLIPBOARD_METADATA_FORMAT)?; - } - Ok(()) -} - -fn write_image(item: &Image) -> Result<()> { - let native_format = match item.format { - ImageFormat::Svg => Some(*CLIPBOARD_SVG_FORMAT), - ImageFormat::Gif => Some(*CLIPBOARD_GIF_FORMAT), - ImageFormat::Png => Some(*CLIPBOARD_PNG_FORMAT), - ImageFormat::Jpeg => Some(*CLIPBOARD_JPG_FORMAT), - _ => None, - }; - if let Some(format) = native_format { - set_clipboard_bytes(item.bytes(), format)?; - } - - // Also provide a PNG copy for broad compatibility. - // SVG can't be rasterized by the image crate, so skip it. - if item.format != ImageFormat::Svg && native_format != Some(*CLIPBOARD_PNG_FORMAT) { - if let Some(png_bytes) = convert_to_png(item.bytes(), item.format) { - set_clipboard_bytes(&png_bytes, *CLIPBOARD_PNG_FORMAT)?; - } - } - Ok(()) -} - -fn convert_to_png(bytes: &[u8], format: ImageFormat) -> Option> { - let img_format = gpui_to_image_format(format)?; - let image = image::load_from_memory_with_format(bytes, img_format) - .map_err(|e| log::warn!("Failed to decode image for PNG conversion: {e}")) - .ok()?; - let mut buf = Vec::new(); - image - .write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png) - .map_err(|e| log::warn!("Failed to encode PNG: {e}")) - .ok()?; - Some(buf) -} - -fn read_string() -> Option { - let text = get_clipboard_string(CF_UNICODETEXT.0 as u32)?; - let metadata = read_clipboard_metadata(&text); - Some(ClipboardEntry::String(ClipboardString { text, metadata })) -} - -fn read_clipboard_metadata(text: &str) -> Option { - let locked = get_clipboard_data(*CLIPBOARD_HASH_FORMAT)?; - let hash_bytes: [u8; 8] = locked.as_bytes().get(..8)?.try_into().ok()?; - let hash = u64::from_ne_bytes(hash_bytes); - if hash != ClipboardString::text_hash(text) { - return None; - } - get_clipboard_string(*CLIPBOARD_METADATA_FORMAT) -} - -fn read_image(format: u32) -> Option { - let locked = get_clipboard_data(format)?; - let (bytes, image_format) = if format == CF_DIB.0 as u32 { - (convert_dib_to_bmp(locked.as_bytes())?, ImageFormat::Bmp) - } else { - let image_format = *IMAGE_FORMATS_MAP.get(&format)?; - (locked.as_bytes().to_vec(), image_format) - }; - let id = hash(&bytes); - Some(ClipboardEntry::Image(Image { - format: image_format, - bytes, - id, - })) -} - -fn read_files() -> Option { - let locked = get_clipboard_data(CF_HDROP.0 as u32)?; - let hdrop = HDROP(locked.ptr as *mut _); - let mut filenames = Vec::new(); - with_file_names(hdrop, |name| filenames.push(std::path::PathBuf::from(name))); - Some(ClipboardEntry::ExternalPaths(ExternalPaths( - filenames.into(), - ))) -} - -/// DIB is BMP without the 14-byte BITMAPFILEHEADER. Prepend one. -fn convert_dib_to_bmp(dib: &[u8]) -> Option> { - if dib.len() < 40 { - return None; - } - - let header_size = u32::from_le_bytes(dib[0..4].try_into().ok()?); - let bit_count = u16::from_le_bytes(dib[14..16].try_into().ok()?); - let compression = u32::from_le_bytes(dib[16..20].try_into().ok()?); - - let color_table_size = if bit_count <= 8 { - let colors_used = u32::from_le_bytes(dib[32..36].try_into().ok()?); - (if colors_used == 0 { - 1u32 << bit_count - } else { - colors_used - }) * 4 - } else if compression == 3 { - 12 // BI_BITFIELDS - } else { - 0 - }; - - let pixel_offset = 14 + header_size + color_table_size; - let file_size = 14 + dib.len() as u32; - - let mut bmp = Vec::with_capacity(file_size as usize); - bmp.extend_from_slice(b"BM"); - bmp.extend_from_slice(&file_size.to_le_bytes()); - bmp.extend_from_slice(&[0u8; 4]); // reserved - bmp.extend_from_slice(&pixel_offset.to_le_bytes()); - bmp.extend_from_slice(dib); - Some(bmp) -} - -fn log_unsupported_clipboard_formats() { - let count = unsafe { CountClipboardFormats() }; - let mut format = 0; - for _ in 0..count { - format = unsafe { EnumClipboardFormats(format) }; - let mut buffer = [0u16; 64]; - unsafe { GetClipboardFormatNameW(format, &mut buffer) }; - let format_name = String::from_utf16_lossy(&buffer); - log::warn!( - "Try to paste with unsupported clipboard format: {}, {}.", - format, - format_name - ); - } -} - -fn gpui_to_image_format(value: ImageFormat) -> Option { - match value { - ImageFormat::Png => Some(image::ImageFormat::Png), - ImageFormat::Jpeg => Some(image::ImageFormat::Jpeg), - ImageFormat::Webp => Some(image::ImageFormat::WebP), - ImageFormat::Gif => Some(image::ImageFormat::Gif), - ImageFormat::Bmp => Some(image::ImageFormat::Bmp), - ImageFormat::Tiff => Some(image::ImageFormat::Tiff), - other => { - log::warn!("No image crate equivalent for format: {other:?}"); - None - } - } -} - -struct ClipboardGuard; - -impl ClipboardGuard { - fn open() -> Option { - match unsafe { OpenClipboard(None) } { - Ok(()) => Some(Self), - Err(e) => { - log::error!("Failed to open clipboard: {e}"); - None - } - } - } -} - -impl Drop for ClipboardGuard { - fn drop(&mut self) { - if let Err(e) = unsafe { CloseClipboard() } { - log::error!("Failed to close clipboard: {e}"); - } - } -} - -struct LockedGlobal { - global: HGLOBAL, - ptr: *const u8, - size: usize, -} - -impl LockedGlobal { - fn lock(global: HGLOBAL) -> Option { - let size = unsafe { GlobalSize(global) }; - let ptr = unsafe { GlobalLock(global) }; - if ptr.is_null() { - return None; - } - Some(Self { - global, - ptr: ptr as *const u8, - size, - }) - } - - fn as_bytes(&self) -> &[u8] { - unsafe { std::slice::from_raw_parts(self.ptr, self.size) } - } -} - -impl Drop for LockedGlobal { - fn drop(&mut self) { - unsafe { GlobalUnlock(self.global).ok() }; - } -} diff --git a/crates/gpui_pre_windows/src/color_text_raster.hlsl b/crates/gpui_pre_windows/src/color_text_raster.hlsl deleted file mode 100644 index 2fbc156..0000000 --- a/crates/gpui_pre_windows/src/color_text_raster.hlsl +++ /dev/null @@ -1,44 +0,0 @@ -#include "alpha_correction.hlsl" - -struct RasterVertexOutput { - float4 position : SV_Position; - float2 texcoord : TEXCOORD0; -}; - -RasterVertexOutput emoji_rasterization_vertex(uint vertexID : SV_VERTEXID) -{ - RasterVertexOutput output; - output.texcoord = float2((vertexID << 1) & 2, vertexID & 2); - output.position = float4(output.texcoord * 2.0f - 1.0f, 0.0f, 1.0f); - output.position.y = -output.position.y; - - return output; -} - -struct PixelInput { - float4 position: SV_Position; - float2 texcoord : TEXCOORD0; -}; - -struct Bounds { - int2 origin; - int2 size; -}; - -Texture2D t_layer : register(t0); -SamplerState s_layer : register(s0); - -cbuffer GlyphLayerTextureParams : register(b0) { - Bounds bounds; - float4 run_color; - float4 gamma_ratios; - float grayscale_enhanced_contrast; - float3 _pad; -}; - -float4 emoji_rasterization_fragment(PixelInput input): SV_Target { - float sample = t_layer.Sample(s_layer, input.texcoord.xy).r; - float alpha_corrected = apply_contrast_and_gamma_correction(sample, run_color.rgb, grayscale_enhanced_contrast, gamma_ratios); - float alpha = alpha_corrected * run_color.a; - return float4(run_color.rgb * alpha, alpha); -} diff --git a/crates/gpui_pre_windows/src/destination_list.rs b/crates/gpui_pre_windows/src/destination_list.rs deleted file mode 100644 index d6967c0..0000000 --- a/crates/gpui_pre_windows/src/destination_list.rs +++ /dev/null @@ -1,207 +0,0 @@ -use std::{path::PathBuf, sync::Arc}; - -use itertools::Itertools; -use smallvec::SmallVec; -use windows::{ - Win32::{ - Foundation::PROPERTYKEY, - Globalization::u_strlen, - System::Com::{CLSCTX_INPROC_SERVER, CoCreateInstance, StructuredStorage::PROPVARIANT}, - UI::{ - Controls::INFOTIPSIZE, - Shell::{ - Common::{IObjectArray, IObjectCollection}, - DestinationList, EnumerableObjectCollection, ICustomDestinationList, IShellLinkW, - PropertiesSystem::IPropertyStore, - ShellLink, - }, - }, - }, - core::{GUID, HSTRING, Interface}, -}; - -use gpui::{Action, MenuItem, SharedString}; - -pub(crate) struct JumpList { - pub(crate) dock_menus: Vec, - pub(crate) recent_workspaces: Arc<[SmallVec<[PathBuf; 2]>]>, -} - -impl JumpList { - pub(crate) fn new() -> Self { - Self { - dock_menus: Vec::default(), - recent_workspaces: Arc::default(), - } - } -} - -pub(crate) struct DockMenuItem { - pub(crate) name: SharedString, - pub(crate) description: SharedString, - pub(crate) action: Box, -} - -impl DockMenuItem { - pub(crate) fn new(item: MenuItem) -> anyhow::Result { - match item { - MenuItem::Action { name, action, .. } => Ok(Self { - name: name.clone(), - description: if name == "New Window" { - "Opens a new window".into() - } else { - name - }, - action, - }), - _ => anyhow::bail!("Only `MenuItem::Action` is supported for dock menu on Windows."), - } - } -} - -// This code is based on the example from Microsoft: -// https://github.com/microsoft/Windows-classic-samples/blob/main/Samples/Win7Samples/winui/shell/appshellintegration/RecipePropertyHandler/RecipePropertyHandler.cpp -pub(crate) fn update_jump_list( - recent_workspaces: &[SmallVec<[PathBuf; 2]>], - dock_menus: &[(SharedString, SharedString)], -) -> anyhow::Result>> { - let (list, removed) = create_destination_list()?; - add_recent_folders(&list, recent_workspaces, removed.as_ref())?; - add_dock_menu(&list, dock_menus)?; - unsafe { list.CommitList() }?; - Ok(removed) -} - -// Copied from: -// https://github.com/microsoft/windows-rs/blob/0fc3c2e5a13d4316d242bdeb0a52af611eba8bd4/crates/libs/windows/src/Windows/Win32/Storage/EnhancedStorage/mod.rs#L1881 -const PKEY_TITLE: PROPERTYKEY = PROPERTYKEY { - fmtid: GUID::from_u128(0xf29f85e0_4ff9_1068_ab91_08002b27b3d9), - pid: 2, -}; - -fn create_destination_list() -> anyhow::Result<(ICustomDestinationList, Vec>)> -{ - let list: ICustomDestinationList = - unsafe { CoCreateInstance(&DestinationList, None, CLSCTX_INPROC_SERVER) }?; - - let mut slots = 0; - let user_removed: IObjectArray = unsafe { list.BeginList(&mut slots) }?; - - let count = unsafe { user_removed.GetCount() }?; - if count == 0 { - return Ok((list, Vec::new())); - } - - let mut removed = Vec::with_capacity(count as usize); - for i in 0..count { - let shell_link: IShellLinkW = unsafe { user_removed.GetAt(i)? }; - let description = { - // INFOTIPSIZE is the maximum size of the buffer - // see https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ishelllinkw-getdescription - let mut buffer = [0u16; INFOTIPSIZE as usize]; - unsafe { shell_link.GetDescription(&mut buffer)? }; - let len = unsafe { u_strlen(buffer.as_ptr()) }; - String::from_utf16_lossy(&buffer[..len as usize]) - }; - let args = description.split('\n').map(PathBuf::from).collect(); - - removed.push(args); - } - - Ok((list, removed)) -} - -fn add_dock_menu( - list: &ICustomDestinationList, - dock_menus: &[(SharedString, SharedString)], -) -> anyhow::Result<()> { - unsafe { - let tasks: IObjectCollection = - CoCreateInstance(&EnumerableObjectCollection, None, CLSCTX_INPROC_SERVER)?; - for (idx, (name, description)) in dock_menus.iter().enumerate() { - let argument = HSTRING::from(format!("--dock-action {}", idx)); - let description = HSTRING::from(description.as_str()); - let display = name.as_str(); - let task = create_shell_link(argument, description, None, display)?; - tasks.AddObject(&task)?; - } - list.AddUserTasks(&tasks)?; - Ok(()) - } -} - -fn add_recent_folders( - list: &ICustomDestinationList, - entries: &[SmallVec<[PathBuf; 2]>], - removed: &Vec>, -) -> anyhow::Result<()> { - unsafe { - let tasks: IObjectCollection = - CoCreateInstance(&EnumerableObjectCollection, None, CLSCTX_INPROC_SERVER)?; - - for folder_path in entries.iter().filter(|path| !removed.contains(path)) { - let argument = HSTRING::from( - folder_path - .iter() - .map(|path| format!("\"{}\"", path.display())) - .join(" "), - ); - - let description = HSTRING::from( - folder_path - .iter() - .map(|path| path.to_string_lossy()) - .collect::>() - .join("\n"), - ); - // simulate folder icon - // https://github.com/microsoft/vscode/blob/7a5dc239516a8953105da34f84bae152421a8886/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts#L380 - let icon = HSTRING::from("explorer.exe"); - - let display = folder_path - .iter() - .map(|p| { - p.file_name() - .map(|name| name.to_string_lossy()) - .unwrap_or_else(|| p.to_string_lossy()) - }) - .join(", "); - - tasks.AddObject(&create_shell_link( - argument, - description, - Some(icon), - &display, - )?)?; - } - - if tasks.GetCount().unwrap_or(0) > 0 { - list.AppendCategory(&HSTRING::from("Recent Folders"), &tasks)?; - } - Ok(()) - } -} - -fn create_shell_link( - argument: HSTRING, - description: HSTRING, - icon: Option, - display: &str, -) -> anyhow::Result { - unsafe { - let link: IShellLinkW = CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER)?; - let exe_path = HSTRING::from(std::env::current_exe()?.as_os_str()); - link.SetPath(&exe_path)?; - link.SetArguments(&argument)?; - link.SetDescription(&description)?; - if let Some(icon) = icon { - link.SetIconLocation(&icon, 0)?; - } - let store: IPropertyStore = link.cast()?; - let title = PROPVARIANT::from(display); - store.SetValue(&PKEY_TITLE, &title)?; - store.Commit()?; - - Ok(link) - } -} diff --git a/crates/gpui_pre_windows/src/direct_manipulation.rs b/crates/gpui_pre_windows/src/direct_manipulation.rs deleted file mode 100644 index 9adcbed..0000000 --- a/crates/gpui_pre_windows/src/direct_manipulation.rs +++ /dev/null @@ -1,359 +0,0 @@ -use std::cell::{Cell, RefCell}; -use std::rc::Rc; - -use anyhow::Result; -use gpui::*; -use gpui_util::ResultExt; -use windows::Win32::{ - Foundation::*, - Graphics::{DirectManipulation::*, Gdi::*}, - System::Com::*, - UI::{Input::Pointer::*, WindowsAndMessaging::*}, -}; - -use crate::*; - -/// Default viewport size in pixels. The actual content size doesn't matter -/// because we're using the viewport only for gesture recognition, not for -/// visual output. -const DEFAULT_VIEWPORT_SIZE: i32 = 1000; - -pub(crate) struct DirectManipulationHandler { - manager: IDirectManipulationManager, - update_manager: IDirectManipulationUpdateManager, - viewport: IDirectManipulationViewport, - _handler_cookie: u32, - window: HWND, - scale_factor: Rc>, - pending_events: Rc>>, -} - -impl DirectManipulationHandler { - pub fn new(window: HWND, scale_factor: f32) -> Result { - unsafe { - let manager: IDirectManipulationManager = - CoCreateInstance(&DirectManipulationManager, None, CLSCTX_INPROC_SERVER)?; - - let update_manager: IDirectManipulationUpdateManager = manager.GetUpdateManager()?; - - let viewport: IDirectManipulationViewport = manager.CreateViewport(None, window)?; - - let configuration = DIRECTMANIPULATION_CONFIGURATION_INTERACTION - | DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_X - | DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_Y - | DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_INERTIA - | DIRECTMANIPULATION_CONFIGURATION_RAILS_X - | DIRECTMANIPULATION_CONFIGURATION_RAILS_Y - | DIRECTMANIPULATION_CONFIGURATION_SCALING; - viewport.ActivateConfiguration(configuration)?; - - viewport.SetViewportOptions( - DIRECTMANIPULATION_VIEWPORT_OPTIONS_MANUALUPDATE - | DIRECTMANIPULATION_VIEWPORT_OPTIONS_DISABLEPIXELSNAPPING, - )?; - - let mut rect = RECT { - left: 0, - top: 0, - right: DEFAULT_VIEWPORT_SIZE, - bottom: DEFAULT_VIEWPORT_SIZE, - }; - viewport.SetViewportRect(&mut rect)?; - - manager.Activate(window)?; - viewport.Enable()?; - - let scale_factor = Rc::new(Cell::new(scale_factor)); - let pending_events = Rc::new(RefCell::new(Vec::new())); - - let event_handler: IDirectManipulationViewportEventHandler = - DirectManipulationEventHandler::new( - window, - Rc::clone(&scale_factor), - Rc::clone(&pending_events), - ) - .into(); - - let handler_cookie = viewport.AddEventHandler(Some(window), &event_handler)?; - - update_manager.Update(None)?; - - Ok(Self { - manager, - update_manager, - viewport, - _handler_cookie: handler_cookie, - window, - scale_factor, - pending_events, - }) - } - } - - pub fn set_scale_factor(&self, scale_factor: f32) { - self.scale_factor.set(scale_factor); - } - - pub fn on_pointer_hit_test(&self, wparam: WPARAM) { - unsafe { - let pointer_id = wparam.loword() as u32; - let mut pointer_type = POINTER_INPUT_TYPE::default(); - if GetPointerType(pointer_id, &mut pointer_type).is_ok() && pointer_type == PT_TOUCHPAD - { - self.viewport.SetContact(pointer_id).log_err(); - } - } - } - - pub fn update(&self) { - unsafe { - self.update_manager.Update(None).log_err(); - } - } - - pub fn drain_events(&self) -> Vec { - std::mem::take(&mut *self.pending_events.borrow_mut()) - } -} - -impl Drop for DirectManipulationHandler { - fn drop(&mut self) { - unsafe { - self.viewport.Stop().log_err(); - self.viewport.Abandon().log_err(); - self.manager.Deactivate(self.window).log_err(); - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum GestureKind { - None, - Scroll, - Pinch, -} - -#[windows_core::implement(IDirectManipulationViewportEventHandler)] -struct DirectManipulationEventHandler { - window: HWND, - scale_factor: Rc>, - gesture_kind: Cell, - last_scale: Cell, - last_x_offset: Cell, - last_y_offset: Cell, - scroll_phase: Cell, - pending_events: Rc>>, -} - -impl DirectManipulationEventHandler { - fn new( - window: HWND, - scale_factor: Rc>, - pending_events: Rc>>, - ) -> Self { - Self { - window, - scale_factor, - gesture_kind: Cell::new(GestureKind::None), - last_scale: Cell::new(1.0), - last_x_offset: Cell::new(0.0), - last_y_offset: Cell::new(0.0), - scroll_phase: Cell::new(TouchPhase::Started), - pending_events, - } - } - - fn end_gesture(&self) { - let position = self.mouse_position(); - let modifiers = current_modifiers(); - match self.gesture_kind.get() { - GestureKind::Scroll => { - self.pending_events - .borrow_mut() - .push(PlatformInput::ScrollWheel(ScrollWheelEvent { - position, - delta: ScrollDelta::Pixels(point(px(0.0), px(0.0))), - modifiers, - touch_phase: TouchPhase::Ended, - })); - } - GestureKind::Pinch => { - self.pending_events - .borrow_mut() - .push(PlatformInput::Pinch(PinchEvent { - position, - delta: 0.0, - modifiers, - phase: TouchPhase::Ended, - })); - } - GestureKind::None => {} - } - self.gesture_kind.set(GestureKind::None); - } - - fn mouse_position(&self) -> Point { - let scale_factor = self.scale_factor.get(); - unsafe { - let mut point: POINT = std::mem::zeroed(); - let _ = GetCursorPos(&mut point); - let _ = ScreenToClient(self.window, &mut point); - logical_point(point.x as f32, point.y as f32, scale_factor) - } - } -} - -impl IDirectManipulationViewportEventHandler_Impl for DirectManipulationEventHandler_Impl { - fn OnViewportStatusChanged( - &self, - viewport: windows_core::Ref<'_, IDirectManipulationViewport>, - current: DIRECTMANIPULATION_STATUS, - previous: DIRECTMANIPULATION_STATUS, - ) -> windows_core::Result<()> { - if current == previous { - return Ok(()); - } - - // A new gesture interrupted inertia, so end the old sequence. - if current == DIRECTMANIPULATION_RUNNING && previous == DIRECTMANIPULATION_INERTIA { - self.end_gesture(); - } - - if current == DIRECTMANIPULATION_READY { - self.end_gesture(); - - // Reset the content transform so the viewport is ready for the next gesture. - // ZoomToRect triggers a second RUNNING -> READY cycle, so prevent an infinite loop here. - if self.last_scale.get() != 1.0 - || self.last_x_offset.get() != 0.0 - || self.last_y_offset.get() != 0.0 - { - if let Some(viewport) = viewport.as_ref() { - unsafe { - viewport - .ZoomToRect( - 0.0, - 0.0, - DEFAULT_VIEWPORT_SIZE as f32, - DEFAULT_VIEWPORT_SIZE as f32, - false, - ) - .log_err(); - } - } - } - - self.last_scale.set(1.0); - self.last_x_offset.set(0.0); - self.last_y_offset.set(0.0); - } - - Ok(()) - } - - fn OnViewportUpdated( - &self, - _viewport: windows_core::Ref<'_, IDirectManipulationViewport>, - ) -> windows_core::Result<()> { - Ok(()) - } - - fn OnContentUpdated( - &self, - _viewport: windows_core::Ref<'_, IDirectManipulationViewport>, - content: windows_core::Ref<'_, IDirectManipulationContent>, - ) -> windows_core::Result<()> { - let content = content.as_ref().ok_or(E_POINTER)?; - - // Get the 6-element content transform: [scale, 0, 0, scale, tx, ty] - let mut xform = [0.0f32; 6]; - unsafe { - content.GetContentTransform(&mut xform)?; - } - - let scale = xform[0]; - let scale_factor = self.scale_factor.get(); - let x_offset = xform[4] / scale_factor; - let y_offset = xform[5] / scale_factor; - - if scale == 0.0 { - return Ok(()); - } - - let last_scale = self.last_scale.get(); - let last_x = self.last_x_offset.get(); - let last_y = self.last_y_offset.get(); - - if float_equals(scale, last_scale) - && float_equals(x_offset, last_x) - && float_equals(y_offset, last_y) - { - return Ok(()); - } - - let position = self.mouse_position(); - let modifiers = current_modifiers(); - - // Direct Manipulation reports both translation and scale in every content update. - // Translation values can shift during a pinch due to the zoom center shifting. - // We classify each gesture as either scroll or pinch and only emit one type of event. - // We allow Scroll -> Pinch (a pinch can start with a small pan) but not the reverse. - if !float_equals(scale, 1.0) { - if self.gesture_kind.get() != GestureKind::Pinch { - self.end_gesture(); - self.gesture_kind.set(GestureKind::Pinch); - self.pending_events - .borrow_mut() - .push(PlatformInput::Pinch(PinchEvent { - position, - delta: 0.0, - modifiers, - phase: TouchPhase::Started, - })); - } - } else if self.gesture_kind.get() == GestureKind::None { - self.gesture_kind.set(GestureKind::Scroll); - self.scroll_phase.set(TouchPhase::Started); - } - - match self.gesture_kind.get() { - GestureKind::Scroll => { - let dx = x_offset - last_x; - let dy = y_offset - last_y; - let touch_phase = self.scroll_phase.get(); - self.scroll_phase.set(TouchPhase::Moved); - self.pending_events - .borrow_mut() - .push(PlatformInput::ScrollWheel(ScrollWheelEvent { - position, - delta: ScrollDelta::Pixels(point(px(dx), px(dy))), - modifiers, - touch_phase, - })); - } - GestureKind::Pinch => { - let scale_delta = scale / last_scale; - self.pending_events - .borrow_mut() - .push(PlatformInput::Pinch(PinchEvent { - position, - delta: scale_delta - 1.0, - modifiers, - phase: TouchPhase::Moved, - })); - } - GestureKind::None => {} - } - - self.last_scale.set(scale); - self.last_x_offset.set(x_offset); - self.last_y_offset.set(y_offset); - - Ok(()) - } -} - -fn float_equals(f1: f32, f2: f32) -> bool { - const EPSILON_SCALE: f32 = 0.00001; - (f1 - f2).abs() < EPSILON_SCALE * f1.abs().max(f2.abs()).max(EPSILON_SCALE) -} diff --git a/crates/gpui_pre_windows/src/direct_write.rs b/crates/gpui_pre_windows/src/direct_write.rs deleted file mode 100644 index 5a761cc..0000000 --- a/crates/gpui_pre_windows/src/direct_write.rs +++ /dev/null @@ -1,2161 +0,0 @@ -use std::{ - borrow::Cow, - ffi::{c_uint, c_void}, - mem::ManuallyDrop, -}; - -use anyhow::{Context, Result}; -use collections::HashMap; -use gpui_util::{ResultExt, maybe}; -use parking_lot::{RwLock, RwLockUpgradableReadGuard}; -use windows::{ - Win32::{ - Foundation::*, - Globalization::GetUserDefaultLocaleName, - Graphics::{ - Direct3D::D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, Direct3D11::*, DirectWrite::*, - Dxgi::Common::*, Gdi::LOGFONTW, - }, - System::SystemServices::LOCALE_NAME_MAX_LENGTH, - UI::WindowsAndMessaging::*, - }, - core::*, -}; -use windows_numerics::Vector2; - -use crate::*; -use gpui::*; - -#[derive(Debug)] -struct FontInfo { - font_family_h: HSTRING, - font_face: IDWriteFontFace3, - features: IDWriteTypography, - fallbacks: Option, - font_collection: IDWriteFontCollection1, -} - -pub(crate) struct DirectWriteTextSystem { - components: DirectWriteComponents, - state: RwLock, -} - -struct DirectWriteComponents { - locale: HSTRING, - factory: IDWriteFactory5, - in_memory_loader: IDWriteInMemoryFontFileLoader, - builder: IDWriteFontSetBuilder1, - text_renderer: TextRendererWrapper, - system_ui_font_name: SharedString, - system_subpixel_rendering: bool, -} - -impl Drop for DirectWriteComponents { - fn drop(&mut self) { - unsafe { - let _ = self - .factory - .UnregisterFontFileLoader(&self.in_memory_loader); - } - } -} - -struct GPUState { - device: ID3D11Device, - device_context: ID3D11DeviceContext, - sampler: Option, - blend_state: ID3D11BlendState, - vertex_shader: ID3D11VertexShader, - pixel_shader: ID3D11PixelShader, -} - -struct DirectWriteState { - gpu_state: GPUState, - system_font_collection: IDWriteFontCollection1, - custom_font_collection: IDWriteFontCollection1, - fonts: Vec, - font_to_font_id: HashMap, - font_info_cache: HashMap, - layout_line_scratch: Vec, -} - -impl GPUState { - fn new(directx_devices: &DirectXDevices) -> Result { - let device = directx_devices.device.clone(); - let device_context = directx_devices.device_context.clone(); - - let blend_state = { - let mut blend_state = None; - let desc = D3D11_BLEND_DESC { - AlphaToCoverageEnable: false.into(), - IndependentBlendEnable: false.into(), - RenderTarget: [ - D3D11_RENDER_TARGET_BLEND_DESC { - BlendEnable: true.into(), - SrcBlend: D3D11_BLEND_ONE, - DestBlend: D3D11_BLEND_INV_SRC_ALPHA, - BlendOp: D3D11_BLEND_OP_ADD, - SrcBlendAlpha: D3D11_BLEND_ONE, - DestBlendAlpha: D3D11_BLEND_INV_SRC_ALPHA, - BlendOpAlpha: D3D11_BLEND_OP_ADD, - RenderTargetWriteMask: D3D11_COLOR_WRITE_ENABLE_ALL.0 as u8, - }, - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - ], - }; - unsafe { device.CreateBlendState(&desc, Some(&mut blend_state)) }?; - blend_state.unwrap() - }; - - let sampler = { - let mut sampler = None; - let desc = D3D11_SAMPLER_DESC { - Filter: D3D11_FILTER_MIN_MAG_MIP_POINT, - AddressU: D3D11_TEXTURE_ADDRESS_BORDER, - AddressV: D3D11_TEXTURE_ADDRESS_BORDER, - AddressW: D3D11_TEXTURE_ADDRESS_BORDER, - MipLODBias: 0.0, - MaxAnisotropy: 1, - ComparisonFunc: D3D11_COMPARISON_ALWAYS, - BorderColor: [0.0, 0.0, 0.0, 0.0], - MinLOD: 0.0, - MaxLOD: 0.0, - }; - unsafe { device.CreateSamplerState(&desc, Some(&mut sampler)) }?; - sampler - }; - - let vertex_shader = { - let source = shader_resources::RawShaderBytes::new( - shader_resources::ShaderModule::EmojiRasterization, - shader_resources::ShaderTarget::Vertex, - )?; - let mut shader = None; - unsafe { device.CreateVertexShader(source.as_bytes(), None, Some(&mut shader)) }?; - shader.unwrap() - }; - - let pixel_shader = { - let source = shader_resources::RawShaderBytes::new( - shader_resources::ShaderModule::EmojiRasterization, - shader_resources::ShaderTarget::Fragment, - )?; - let mut shader = None; - unsafe { device.CreatePixelShader(source.as_bytes(), None, Some(&mut shader)) }?; - shader.unwrap() - }; - - Ok(Self { - device, - device_context, - sampler, - blend_state, - vertex_shader, - pixel_shader, - }) - } -} - -impl DirectWriteTextSystem { - pub(crate) fn new(directx_devices: &DirectXDevices) -> Result { - let factory: IDWriteFactory5 = unsafe { DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED)? }; - // The `IDWriteInMemoryFontFileLoader` here is supported starting from - // Windows 10 Creators Update, which consequently requires the entire - // `DirectWriteTextSystem` to run on `win10 1703`+. - let in_memory_loader = unsafe { factory.CreateInMemoryFontFileLoader()? }; - unsafe { factory.RegisterFontFileLoader(&in_memory_loader)? }; - let builder = unsafe { factory.CreateFontSetBuilder()? }; - let mut locale = [0u16; LOCALE_NAME_MAX_LENGTH as usize]; - unsafe { GetUserDefaultLocaleName(&mut locale) }; - let locale = HSTRING::from_wide(&locale); - let text_renderer = TextRendererWrapper::new(locale.clone()); - - let gpu_state = GPUState::new(directx_devices)?; - - let system_subpixel_rendering = get_system_subpixel_rendering(); - let system_ui_font_name = get_system_ui_font_name(); - let components = DirectWriteComponents { - locale, - factory, - in_memory_loader, - builder, - text_renderer, - system_ui_font_name, - system_subpixel_rendering, - }; - - let system_font_collection = unsafe { - let mut result = None; - components - .factory - .GetSystemFontCollection(false, &mut result, true)?; - result.context("Failed to get system font collection")? - }; - let custom_font_set = unsafe { components.builder.CreateFontSet()? }; - let custom_font_collection = unsafe { - components - .factory - .CreateFontCollectionFromFontSet(&custom_font_set)? - }; - - Ok(Self { - components, - state: RwLock::new(DirectWriteState { - gpu_state, - system_font_collection, - custom_font_collection, - fonts: Vec::new(), - font_to_font_id: HashMap::default(), - font_info_cache: HashMap::default(), - layout_line_scratch: Vec::new(), - }), - }) - } - - pub(crate) fn handle_gpu_lost(&self, directx_devices: &DirectXDevices) -> Result<()> { - self.state.write().handle_gpu_lost(directx_devices) - } -} - -impl PlatformTextSystem for DirectWriteTextSystem { - fn add_fonts(&self, fonts: Vec>) -> Result<()> { - self.state.write().add_fonts(&self.components, fonts) - } - - fn all_font_names(&self) -> Vec { - self.state.read().all_font_names(&self.components) - } - - fn font_id(&self, font: &Font) -> Result { - let lock = self.state.upgradable_read(); - if let Some(font_id) = lock.font_to_font_id.get(font) { - Ok(*font_id) - } else { - RwLockUpgradableReadGuard::upgrade(lock) - .select_and_cache_font(&self.components, font) - .with_context(|| format!("Failed to select font: {:?}", font)) - } - } - - fn font_metrics(&self, font_id: FontId) -> FontMetrics { - self.state.read().font_metrics(font_id) - } - - fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { - self.state.read().get_typographic_bounds(font_id, glyph_id) - } - - fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result> { - self.state.read().get_advance(font_id, glyph_id) - } - - fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { - self.state.read().glyph_for_char(font_id, ch) - } - - fn glyph_raster_bounds( - &self, - params: &RenderGlyphParams, - ) -> anyhow::Result> { - self.state.read().raster_bounds(&self.components, params) - } - - fn rasterize_glyph( - &self, - params: &RenderGlyphParams, - raster_bounds: Bounds, - ) -> anyhow::Result<(Size, Vec)> { - self.state - .read() - .rasterize_glyph(&self.components, params, raster_bounds) - } - - fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout { - self.state - .write() - .layout_line(&self.components, text, font_size, runs) - .log_err() - .unwrap_or(LineLayout { - font_size, - ..Default::default() - }) - } - - fn recommended_rendering_mode( - &self, - _font_id: FontId, - _font_size: Pixels, - ) -> TextRenderingMode { - if self.components.system_subpixel_rendering { - TextRenderingMode::Subpixel - } else { - TextRenderingMode::Grayscale - } - } -} - -impl DirectWriteState { - fn select_and_cache_font( - &mut self, - components: &DirectWriteComponents, - font: &Font, - ) -> Option { - let select_font = |this: &mut DirectWriteState, font: &Font| -> Option { - let info = [&this.custom_font_collection, &this.system_font_collection] - .into_iter() - .find_map(|font_collection| unsafe { - DirectWriteState::make_font_from_font_collection( - font, - font_collection, - &components.factory, - &this.system_font_collection, - &components.system_ui_font_name, - ) - })?; - - let font_id = FontId(this.fonts.len()); - let font_face_key = info.font_face.cast::().unwrap().as_raw().addr(); - this.fonts.push(info); - this.font_info_cache.insert(font_face_key, font_id); - Some(font_id) - }; - - let mut font_id = select_font(self, font); - if font_id.is_none() { - // try updating system fonts and reselect - let mut collection = None; - let font_collection_updated = unsafe { - components - .factory - .GetSystemFontCollection(false, &mut collection, true) - } - .log_err() - .is_some(); - if font_collection_updated && let Some(collection) = collection { - self.system_font_collection = collection; - } - font_id = select_font(self, font); - }; - let font_id = font_id?; - self.font_to_font_id.insert(font.clone(), font_id); - Some(font_id) - } - - fn add_fonts( - &mut self, - components: &DirectWriteComponents, - fonts: Vec>, - ) -> Result<()> { - for font_data in fonts { - match font_data { - Cow::Borrowed(data) => unsafe { - let font_file = components - .in_memory_loader - .CreateInMemoryFontFileReference( - &components.factory, - data.as_ptr().cast(), - data.len() as _, - None, - )?; - components.builder.AddFontFile(&font_file)?; - }, - Cow::Owned(data) => unsafe { - let font_file = components - .in_memory_loader - .CreateInMemoryFontFileReference( - &components.factory, - data.as_ptr().cast(), - data.len() as _, - None, - )?; - components.builder.AddFontFile(&font_file)?; - }, - } - } - let set = unsafe { components.builder.CreateFontSet()? }; - let collection = unsafe { components.factory.CreateFontCollectionFromFontSet(&set)? }; - self.custom_font_collection = collection; - - Ok(()) - } - - fn generate_font_fallbacks( - fallbacks: &FontFallbacks, - factory: &IDWriteFactory5, - system_font_collection: &IDWriteFontCollection1, - ) -> Result> { - let fallback_list = fallbacks.fallback_list(); - if fallback_list.is_empty() { - return Ok(None); - } - unsafe { - let builder = factory.CreateFontFallbackBuilder()?; - let font_set = &system_font_collection.GetFontSet()?; - let mut unicode_ranges = Vec::new(); - for family_name in fallback_list { - let family_name = HSTRING::from(family_name); - let Some(fonts) = font_set - .GetMatchingFonts( - &family_name, - DWRITE_FONT_WEIGHT_NORMAL, - DWRITE_FONT_STRETCH_NORMAL, - DWRITE_FONT_STYLE_NORMAL, - ) - .log_err() - else { - continue; - }; - let Ok(font_face) = fonts.GetFontFaceReference(0) else { - continue; - }; - let font = font_face.CreateFontFace()?; - let mut count = 0; - font.GetUnicodeRanges(None, &mut count).ok(); - if count == 0 { - continue; - } - unicode_ranges.clear(); - unicode_ranges.resize_with(count as usize, DWRITE_UNICODE_RANGE::default); - let Some(_) = font - .GetUnicodeRanges(Some(&mut unicode_ranges), &mut count) - .log_err() - else { - continue; - }; - builder.AddMapping( - &unicode_ranges, - &[family_name.as_ptr()], - None, - None, - None, - 1.0, - )?; - } - let system_fallbacks = factory.GetSystemFontFallback()?; - builder.AddMappings(&system_fallbacks)?; - Ok(Some(builder.CreateFontFallback()?)) - } - } - - unsafe fn generate_font_features( - factory: &IDWriteFactory5, - font_features: &FontFeatures, - ) -> Result { - let direct_write_features = unsafe { factory.CreateTypography()? }; - apply_font_features(&direct_write_features, font_features)?; - Ok(direct_write_features) - } - - unsafe fn make_font_from_font_collection( - &Font { - ref family, - ref features, - ref fallbacks, - weight, - style, - }: &Font, - collection: &IDWriteFontCollection1, - factory: &IDWriteFactory5, - system_font_collection: &IDWriteFontCollection1, - system_ui_font_name: &SharedString, - ) -> Option { - const SYSTEM_UI_FONT_NAME: &str = ".SystemUIFont"; - let family = if family == SYSTEM_UI_FONT_NAME { - system_ui_font_name - } else { - gpui::font_name_with_fallbacks_shared(&family, &system_ui_font_name) - }; - let fontset = unsafe { collection.GetFontSet().log_err()? }; - let font_family_h = HSTRING::from(family.as_str()); - let font = unsafe { - fontset - .GetMatchingFonts( - &font_family_h, - font_weight_to_dwrite(weight), - DWRITE_FONT_STRETCH_NORMAL, - font_style_to_dwrite(style), - ) - .log_err()? - }; - let total_number = unsafe { font.GetFontCount() }; - for index in 0..total_number { - let res = maybe!({ - let font_face_ref = unsafe { font.GetFontFaceReference(index).log_err()? }; - let font_face = unsafe { font_face_ref.CreateFontFace().log_err()? }; - let direct_write_features = - unsafe { Self::generate_font_features(factory, features).log_err()? }; - let fallbacks = fallbacks.as_ref().and_then(|fallbacks| { - Self::generate_font_fallbacks(fallbacks, factory, system_font_collection) - .log_err() - .flatten() - }); - let font_info = FontInfo { - font_family_h: font_family_h.clone(), - font_face, - features: direct_write_features, - fallbacks, - font_collection: collection.clone(), - }; - Some(font_info) - }); - if res.is_some() { - return res; - } - } - None - } - - fn layout_line( - &mut self, - components: &DirectWriteComponents, - text: &str, - font_size: Pixels, - font_runs: &[FontRun], - ) -> Result { - if font_runs.is_empty() { - return Ok(LineLayout { - font_size, - ..Default::default() - }); - } - unsafe { - self.layout_line_scratch.clear(); - self.layout_line_scratch.extend(text.encode_utf16()); - let text_wide = &*self.layout_line_scratch; - - let mut utf8_offset = 0usize; - let mut utf16_offset = 0u32; - let text_layout = { - let first_run = &font_runs[0]; - let font_info = &self.fonts[first_run.font_id.0]; - let collection = &font_info.font_collection; - let format: IDWriteTextFormat1 = components - .factory - .CreateTextFormat( - &font_info.font_family_h, - collection, - font_info.font_face.GetWeight(), - font_info.font_face.GetStyle(), - DWRITE_FONT_STRETCH_NORMAL, - font_size.as_f32(), - &components.locale, - )? - .cast()?; - if let Some(ref fallbacks) = font_info.fallbacks { - format.SetFontFallback(fallbacks)?; - } - - let layout = components.factory.CreateTextLayout( - text_wide, - &format, - f32::INFINITY, - f32::INFINITY, - )?; - let current_text = &text[utf8_offset..(utf8_offset + first_run.len)]; - utf8_offset += first_run.len; - let current_text_utf16_length = current_text.encode_utf16().count() as u32; - let text_range = DWRITE_TEXT_RANGE { - startPosition: utf16_offset, - length: current_text_utf16_length, - }; - layout.SetTypography(&font_info.features, text_range)?; - utf16_offset += current_text_utf16_length; - - layout - }; - - let (ascent, descent) = { - let mut first_metrics = [DWRITE_LINE_METRICS::default(); 4]; - let mut line_count = 0u32; - text_layout.GetLineMetrics(Some(&mut first_metrics), &mut line_count)?; - ( - px(first_metrics[0].baseline), - px(first_metrics[0].height - first_metrics[0].baseline), - ) - }; - let mut break_ligatures = true; - for run in &font_runs[1..] { - let font_info = &self.fonts[run.font_id.0]; - let current_text = &text[utf8_offset..(utf8_offset + run.len)]; - utf8_offset += run.len; - let current_text_utf16_length = current_text.encode_utf16().count() as u32; - - let collection = &font_info.font_collection; - let text_range = DWRITE_TEXT_RANGE { - startPosition: utf16_offset, - length: current_text_utf16_length, - }; - utf16_offset += current_text_utf16_length; - text_layout.SetFontCollection(collection, text_range)?; - text_layout.SetFontFamilyName(&font_info.font_family_h, text_range)?; - let font_size = if break_ligatures { - font_size.as_f32().next_up() - } else { - font_size.as_f32() - }; - text_layout.SetFontSize(font_size, text_range)?; - text_layout.SetFontStyle(font_info.font_face.GetStyle(), text_range)?; - text_layout.SetFontWeight(font_info.font_face.GetWeight(), text_range)?; - text_layout.SetTypography(&font_info.features, text_range)?; - - break_ligatures = !break_ligatures; - } - - let mut runs = Vec::new(); - let mut renderer_context = RendererContext { - text_system: self, - components, - index_converter: StringIndexConverter::new(text), - runs: &mut runs, - width: 0.0, - }; - text_layout.Draw( - Some((&raw mut renderer_context).cast::().cast_const()), - &components.text_renderer.0, - 0.0, - 0.0, - )?; - let width = px(renderer_context.width); - - Ok(LineLayout { - font_size, - width, - ascent, - descent, - runs, - len: text.len(), - }) - } - } - - fn font_metrics(&self, font_id: FontId) -> FontMetrics { - unsafe { - let font_info = &self.fonts[font_id.0]; - let mut metrics = std::mem::zeroed(); - font_info.font_face.GetMetrics(&mut metrics); - - FontMetrics { - units_per_em: metrics.Base.designUnitsPerEm as _, - ascent: metrics.Base.ascent as _, - descent: -(metrics.Base.descent as f32), - line_gap: metrics.Base.lineGap as _, - underline_position: metrics.Base.underlinePosition as _, - underline_thickness: metrics.Base.underlineThickness as _, - cap_height: metrics.Base.capHeight as _, - x_height: metrics.Base.xHeight as _, - bounding_box: Bounds { - origin: Point { - x: metrics.glyphBoxLeft as _, - y: metrics.glyphBoxBottom as _, - }, - size: Size { - width: (metrics.glyphBoxRight - metrics.glyphBoxLeft) as _, - height: (metrics.glyphBoxTop - metrics.glyphBoxBottom) as _, - }, - }, - } - } - } - - fn create_glyph_run_analysis( - &self, - components: &DirectWriteComponents, - params: &RenderGlyphParams, - ) -> Result { - let font = &self.fonts[params.font_id.0]; - let glyph_id = [params.glyph_id.0 as u16]; - let advance = [0.0]; - let offset = [DWRITE_GLYPH_OFFSET::default()]; - let glyph_run = DWRITE_GLYPH_RUN { - fontFace: ManuallyDrop::new(Some(unsafe { std::ptr::read(&***font.font_face) })), - fontEmSize: params.font_size.as_f32(), - glyphCount: 1, - glyphIndices: glyph_id.as_ptr(), - glyphAdvances: advance.as_ptr(), - glyphOffsets: offset.as_ptr(), - isSideways: BOOL(0), - bidiLevel: 0, - }; - let transform = DWRITE_MATRIX { - m11: params.scale_factor, - m12: 0.0, - m21: 0.0, - m22: params.scale_factor, - dx: 0.0, - dy: 0.0, - }; - let baseline_origin_x = - params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor; - let baseline_origin_y = params.subpixel_variant.y as f32 - / gpui::SUBPIXEL_VARIANTS_Y as f32 - / params.scale_factor; - - let mut rendering_mode = DWRITE_RENDERING_MODE1::default(); - let mut grid_fit_mode = DWRITE_GRID_FIT_MODE::default(); - unsafe { - font.font_face.GetRecommendedRenderingMode( - params.font_size.as_f32(), - // Using 96 as scale is applied by the transform - 96.0, - 96.0, - Some(&transform), - false, - DWRITE_OUTLINE_THRESHOLD_ANTIALIASED, - DWRITE_MEASURING_MODE_NATURAL, - None, - &mut rendering_mode, - &mut grid_fit_mode, - )?; - } - let rendering_mode = match rendering_mode { - DWRITE_RENDERING_MODE1_OUTLINE => DWRITE_RENDERING_MODE1_NATURAL_SYMMETRIC, - m => m, - }; - - let antialias_mode = if params.subpixel_rendering { - DWRITE_TEXT_ANTIALIAS_MODE_CLEARTYPE - } else { - DWRITE_TEXT_ANTIALIAS_MODE_GRAYSCALE - }; - - let glyph_analysis = unsafe { - components.factory.CreateGlyphRunAnalysis( - &glyph_run, - Some(&transform), - rendering_mode, - DWRITE_MEASURING_MODE_NATURAL, - grid_fit_mode, - antialias_mode, - baseline_origin_x, - baseline_origin_y, - ) - }?; - Ok(glyph_analysis) - } - - fn raster_bounds( - &self, - components: &DirectWriteComponents, - params: &RenderGlyphParams, - ) -> Result> { - let glyph_analysis = self.create_glyph_run_analysis(components, params)?; - - let texture_type = if params.subpixel_rendering { - DWRITE_TEXTURE_CLEARTYPE_3x1 - } else { - DWRITE_TEXTURE_ALIASED_1x1 - }; - - let bounds = unsafe { glyph_analysis.GetAlphaTextureBounds(texture_type)? }; - - if bounds.right < bounds.left { - Ok(Bounds { - origin: point(0.into(), 0.into()), - size: size(0.into(), 0.into()), - }) - } else { - Ok(Bounds { - origin: point(bounds.left.into(), bounds.top.into()), - size: size( - (bounds.right - bounds.left).into(), - (bounds.bottom - bounds.top).into(), - ), - }) - } - } - - fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { - let font_info = &self.fonts[font_id.0]; - let codepoints = ch as u32; - let mut glyph_indices = 0u16; - unsafe { - font_info - .font_face - .GetGlyphIndices(&raw const codepoints, 1, &raw mut glyph_indices) - .log_err() - } - .map(|_| GlyphId(glyph_indices as u32)) - } - - fn rasterize_glyph( - &self, - components: &DirectWriteComponents, - params: &RenderGlyphParams, - glyph_bounds: Bounds, - ) -> Result<(Size, Vec)> { - if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 { - anyhow::bail!("glyph bounds are empty"); - } - - let bitmap_data = if params.is_emoji { - if let Ok(color) = self.rasterize_color(components, params, glyph_bounds) { - color - } else { - let monochrome = self.rasterize_monochrome(components, params, glyph_bounds)?; - monochrome - .into_iter() - .flat_map(|pixel| [0, 0, 0, pixel]) - .collect::>() - } - } else { - self.rasterize_monochrome(components, params, glyph_bounds)? - }; - - Ok((glyph_bounds.size, bitmap_data)) - } - - fn rasterize_monochrome( - &self, - components: &DirectWriteComponents, - params: &RenderGlyphParams, - glyph_bounds: Bounds, - ) -> Result> { - let glyph_analysis = self.create_glyph_run_analysis(components, params)?; - if !params.subpixel_rendering { - let mut bitmap_data = - vec![0u8; glyph_bounds.size.width.0 as usize * glyph_bounds.size.height.0 as usize]; - unsafe { - glyph_analysis.CreateAlphaTexture( - DWRITE_TEXTURE_ALIASED_1x1, - &RECT { - left: glyph_bounds.origin.x.0, - top: glyph_bounds.origin.y.0, - right: glyph_bounds.size.width.0 + glyph_bounds.origin.x.0, - bottom: glyph_bounds.size.height.0 + glyph_bounds.origin.y.0, - }, - &mut bitmap_data, - )?; - } - - return Ok(bitmap_data); - } - - let width = glyph_bounds.size.width.0 as usize; - let height = glyph_bounds.size.height.0 as usize; - let pixel_count = width * height; - - let mut bitmap_data = vec![0u8; pixel_count * 4]; - - unsafe { - glyph_analysis.CreateAlphaTexture( - DWRITE_TEXTURE_CLEARTYPE_3x1, - &RECT { - left: glyph_bounds.origin.x.0, - top: glyph_bounds.origin.y.0, - right: glyph_bounds.size.width.0 + glyph_bounds.origin.x.0, - bottom: glyph_bounds.size.height.0 + glyph_bounds.origin.y.0, - }, - &mut bitmap_data[..pixel_count * 3], - )?; - } - - // The output buffer expects RGBA data, so pad the alpha channel with zeros. - for pixel_ix in (0..pixel_count).rev() { - let src = pixel_ix * 3; - let dst = pixel_ix * 4; - ( - bitmap_data[dst], - bitmap_data[dst + 1], - bitmap_data[dst + 2], - bitmap_data[dst + 3], - ) = ( - bitmap_data[src], - bitmap_data[src + 1], - bitmap_data[src + 2], - 0, - ); - } - - Ok(bitmap_data) - } - - fn rasterize_color( - &self, - components: &DirectWriteComponents, - params: &RenderGlyphParams, - glyph_bounds: Bounds, - ) -> Result> { - // INVARIANT: the code below drives the *shared* D3D11 immediate context - // (`Map`/`Unmap`/`Draw`/`CopyResource`), which `DirectXRenderer` and `DirectXAtlas` also - // touch. An immediate `ID3D11DeviceContext` is not thread-safe, so this must only run on - // the main UI thread (which it always is; text rasterization never leaves that thread). - let bitmap_size = glyph_bounds.size; - let subpixel_shift = params - .subpixel_variant - .map(|v| v as f32 / SUBPIXEL_VARIANTS_X as f32); - let baseline_origin_x = subpixel_shift.x / params.scale_factor; - let baseline_origin_y = subpixel_shift.y / params.scale_factor; - - let transform = DWRITE_MATRIX { - m11: params.scale_factor, - m12: 0.0, - m21: 0.0, - m22: params.scale_factor, - dx: 0.0, - dy: 0.0, - }; - - let font = &self.fonts[params.font_id.0]; - let glyph_id = [params.glyph_id.0 as u16]; - let advance = [glyph_bounds.size.width.0 as f32]; - let offset = [DWRITE_GLYPH_OFFSET { - advanceOffset: -glyph_bounds.origin.x.0 as f32 / params.scale_factor, - ascenderOffset: glyph_bounds.origin.y.0 as f32 / params.scale_factor, - }]; - let glyph_run = DWRITE_GLYPH_RUN { - fontFace: ManuallyDrop::new(Some(unsafe { std::ptr::read(&***font.font_face) })), - fontEmSize: params.font_size.as_f32(), - glyphCount: 1, - glyphIndices: glyph_id.as_ptr(), - glyphAdvances: advance.as_ptr(), - glyphOffsets: offset.as_ptr(), - isSideways: BOOL(0), - bidiLevel: 0, - }; - - // todo: support formats other than COLR - let color_enumerator = unsafe { - components.factory.TranslateColorGlyphRun( - Vector2::new(baseline_origin_x, baseline_origin_y), - &glyph_run, - None, - DWRITE_GLYPH_IMAGE_FORMATS_COLR, - DWRITE_MEASURING_MODE_NATURAL, - Some(&transform), - 0, - ) - }?; - - let mut glyph_layers = Vec::new(); - let mut alpha_data = Vec::new(); - loop { - let color_run = unsafe { color_enumerator.GetCurrentRun() }?; - let color_run = unsafe { &*color_run }; - let image_format = color_run.glyphImageFormat & !DWRITE_GLYPH_IMAGE_FORMATS_TRUETYPE; - if image_format == DWRITE_GLYPH_IMAGE_FORMATS_COLR { - let color_analysis = unsafe { - components.factory.CreateGlyphRunAnalysis( - &color_run.Base.glyphRun as *const _, - Some(&transform), - DWRITE_RENDERING_MODE1_NATURAL_SYMMETRIC, - DWRITE_MEASURING_MODE_NATURAL, - DWRITE_GRID_FIT_MODE_DEFAULT, - DWRITE_TEXT_ANTIALIAS_MODE_GRAYSCALE, - baseline_origin_x, - baseline_origin_y, - ) - }?; - - let color_bounds = - unsafe { color_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_ALIASED_1x1) }?; - - let color_size = size( - color_bounds.right - color_bounds.left, - color_bounds.bottom - color_bounds.top, - ); - if color_size.width > 0 && color_size.height > 0 { - alpha_data.clear(); - alpha_data.resize((color_size.width * color_size.height) as usize, 0); - unsafe { - color_analysis.CreateAlphaTexture( - DWRITE_TEXTURE_ALIASED_1x1, - &color_bounds, - &mut alpha_data, - ) - }?; - - let run_color = { - let run_color = color_run.Base.runColor; - Rgba { - r: run_color.r, - g: run_color.g, - b: run_color.b, - a: run_color.a, - } - }; - let bounds = bounds(point(color_bounds.left, color_bounds.top), color_size); - glyph_layers.push(GlyphLayerTexture::new( - &self.gpu_state, - run_color, - bounds, - &alpha_data, - )?); - } - } - - let has_next = unsafe { color_enumerator.MoveNext() } - .map(|e| e.as_bool()) - .unwrap_or(false); - if !has_next { - break; - } - } - - let gpu_state = &self.gpu_state; - - let render_target_texture = { - let mut texture = None; - let desc = D3D11_TEXTURE2D_DESC { - Width: bitmap_size.width.0 as u32, - Height: bitmap_size.height.0 as u32, - MipLevels: 1, - ArraySize: 1, - Format: DXGI_FORMAT_B8G8R8A8_UNORM, - SampleDesc: DXGI_SAMPLE_DESC { - Count: 1, - Quality: 0, - }, - Usage: D3D11_USAGE_DEFAULT, - BindFlags: D3D11_BIND_RENDER_TARGET.0 as u32, - CPUAccessFlags: 0, - MiscFlags: 0, - }; - unsafe { - gpu_state - .device - .CreateTexture2D(&desc, None, Some(&mut texture)) - }?; - texture.unwrap() - }; - - let render_target_view = { - let desc = D3D11_RENDER_TARGET_VIEW_DESC { - Format: DXGI_FORMAT_B8G8R8A8_UNORM, - ViewDimension: D3D11_RTV_DIMENSION_TEXTURE2D, - Anonymous: D3D11_RENDER_TARGET_VIEW_DESC_0 { - Texture2D: D3D11_TEX2D_RTV { MipSlice: 0 }, - }, - }; - let mut rtv = None; - unsafe { - gpu_state.device.CreateRenderTargetView( - &render_target_texture, - Some(&desc), - Some(&mut rtv), - ) - }?; - rtv - }; - - Self::composite_color_layers( - gpu_state, - &glyph_layers, - bitmap_size, - &render_target_texture, - &render_target_view, - ) - } - - fn composite_color_layers( - gpu_state: &GPUState, - glyph_layers: &[GlyphLayerTexture], - bitmap_size: Size, - render_target_texture: &ID3D11Texture2D, - render_target_view: &Option, - ) -> Result> { - let params_buffer = { - let desc = D3D11_BUFFER_DESC { - ByteWidth: std::mem::size_of::() as u32, - Usage: D3D11_USAGE_DYNAMIC, - BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32, - CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, - MiscFlags: 0, - StructureByteStride: 0, - }; - - let mut buffer = None; - unsafe { - gpu_state - .device - .CreateBuffer(&desc, None, Some(&mut buffer)) - }?; - buffer - }; - - let staging_texture = { - let mut texture = None; - let desc = D3D11_TEXTURE2D_DESC { - Width: bitmap_size.width.0 as u32, - Height: bitmap_size.height.0 as u32, - MipLevels: 1, - ArraySize: 1, - Format: DXGI_FORMAT_B8G8R8A8_UNORM, - SampleDesc: DXGI_SAMPLE_DESC { - Count: 1, - Quality: 0, - }, - Usage: D3D11_USAGE_STAGING, - BindFlags: 0, - CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32, - MiscFlags: 0, - }; - unsafe { - gpu_state - .device - .CreateTexture2D(&desc, None, Some(&mut texture)) - }?; - texture.unwrap() - }; - - let device_context = &gpu_state.device_context; - unsafe { device_context.IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP) }; - unsafe { device_context.VSSetShader(&gpu_state.vertex_shader, None) }; - unsafe { device_context.PSSetShader(&gpu_state.pixel_shader, None) }; - unsafe { - device_context.VSSetConstantBuffers(0, Some(std::slice::from_ref(¶ms_buffer))) - }; - unsafe { - device_context.PSSetConstantBuffers(0, Some(std::slice::from_ref(¶ms_buffer))) - }; - unsafe { - device_context.OMSetRenderTargets(Some(std::slice::from_ref(render_target_view)), None) - }; - unsafe { - if let Some(render_target_view) = render_target_view.as_ref() { - device_context.ClearRenderTargetView(render_target_view, &[0.0, 0.0, 0.0, 0.0]); - } - } - unsafe { device_context.PSSetSamplers(0, Some(std::slice::from_ref(&gpu_state.sampler))) }; - unsafe { device_context.OMSetBlendState(&gpu_state.blend_state, None, 0xffffffff) }; - - let crate::FontInfo { - gamma_ratios, - grayscale_enhanced_contrast, - .. - } = DirectXRenderer::get_font_info(); - - for layer in glyph_layers { - let params = GlyphLayerTextureParams { - run_color: layer.run_color, - bounds: layer.bounds, - gamma_ratios: *gamma_ratios, - grayscale_enhanced_contrast: *grayscale_enhanced_contrast, - _pad: [0f32; 3], - }; - unsafe { - let mut dest = std::mem::zeroed(); - gpu_state.device_context.Map( - params_buffer.as_ref().unwrap(), - 0, - D3D11_MAP_WRITE_DISCARD, - 0, - Some(&mut dest), - )?; - std::ptr::copy_nonoverlapping(¶ms as *const _, dest.pData as *mut _, 1); - gpu_state - .device_context - .Unmap(params_buffer.as_ref().unwrap(), 0); - }; - - let texture = [Some(layer.texture_view.clone())]; - unsafe { device_context.PSSetShaderResources(0, Some(&texture)) }; - - let viewport = [D3D11_VIEWPORT { - TopLeftX: layer.bounds.origin.x as f32, - TopLeftY: layer.bounds.origin.y as f32, - Width: layer.bounds.size.width as f32, - Height: layer.bounds.size.height as f32, - MinDepth: 0.0, - MaxDepth: 1.0, - }]; - unsafe { device_context.RSSetViewports(Some(&viewport)) }; - - unsafe { device_context.Draw(4, 0) }; - } - - unsafe { device_context.CopyResource(&staging_texture, render_target_texture) }; - - let mapped_data = { - let mut mapped_data = D3D11_MAPPED_SUBRESOURCE::default(); - unsafe { - device_context.Map( - &staging_texture, - 0, - D3D11_MAP_READ, - 0, - Some(&mut mapped_data), - ) - }?; - mapped_data - }; - let mut rasterized = - vec![0u8; (bitmap_size.width.0 as u32 * bitmap_size.height.0 as u32 * 4) as usize]; - - for y in 0..bitmap_size.height.0 as usize { - let width = bitmap_size.width.0 as usize; - unsafe { - std::ptr::copy_nonoverlapping::( - (mapped_data.pData as *const u8).byte_add(mapped_data.RowPitch as usize * y), - rasterized - .as_mut_ptr() - .byte_add(width * y * std::mem::size_of::()), - width * std::mem::size_of::(), - ) - }; - } - - // Release the mapping now that the rows have been copied out; leaving `staging_texture` - // mapped would leak the mapping and keep the resource pinned for later reuse. - unsafe { device_context.Unmap(&staging_texture, 0) }; - - // Convert from premultiplied to straight alpha - for chunk in rasterized.chunks_exact_mut(4) { - let b = chunk[0] as f32; - let g = chunk[1] as f32; - let r = chunk[2] as f32; - let a = chunk[3] as f32; - if a > 0.0 { - let inv_a = 255.0 / a; - chunk[0] = (b * inv_a).clamp(0.0, 255.0) as u8; - chunk[1] = (g * inv_a).clamp(0.0, 255.0) as u8; - chunk[2] = (r * inv_a).clamp(0.0, 255.0) as u8; - } - } - - Ok(rasterized) - } - - fn get_typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { - unsafe { - let font = &self.fonts[font_id.0].font_face; - let glyph_indices = [glyph_id.0 as u16]; - let mut metrics = [DWRITE_GLYPH_METRICS::default()]; - font.GetDesignGlyphMetrics(glyph_indices.as_ptr(), 1, metrics.as_mut_ptr(), false)?; - - let metrics = &metrics[0]; - let advance_width = metrics.advanceWidth as i32; - let advance_height = metrics.advanceHeight as i32; - let left_side_bearing = metrics.leftSideBearing; - let right_side_bearing = metrics.rightSideBearing; - let top_side_bearing = metrics.topSideBearing; - let bottom_side_bearing = metrics.bottomSideBearing; - let vertical_origin_y = metrics.verticalOriginY; - - let y_offset = vertical_origin_y + bottom_side_bearing - advance_height; - let width = advance_width - (left_side_bearing + right_side_bearing); - let height = advance_height - (top_side_bearing + bottom_side_bearing); - - Ok(Bounds { - origin: Point { - x: left_side_bearing as f32, - y: y_offset as f32, - }, - size: Size { - width: width as f32, - height: height as f32, - }, - }) - } - } - - fn get_advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { - unsafe { - let font = &self.fonts[font_id.0].font_face; - let glyph_indices = [glyph_id.0 as u16]; - let mut metrics = [DWRITE_GLYPH_METRICS::default()]; - font.GetDesignGlyphMetrics(glyph_indices.as_ptr(), 1, metrics.as_mut_ptr(), false)?; - - let metrics = &metrics[0]; - - Ok(Size { - width: metrics.advanceWidth as f32, - height: 0.0, - }) - } - } - - fn all_font_names(&self, components: &DirectWriteComponents) -> Vec { - let mut result = - get_font_names_from_collection(&self.system_font_collection, &components.locale); - result.extend(get_font_names_from_collection( - &self.custom_font_collection, - &components.locale, - )); - result - } - - fn handle_gpu_lost(&mut self, directx_devices: &DirectXDevices) -> Result<()> { - try_to_recover_from_device_lost(|| { - GPUState::new(directx_devices).context("Recreating GPU state for DirectWrite") - }) - .map(|gpu_state| self.gpu_state = gpu_state) - } -} - -struct GlyphLayerTexture { - run_color: Rgba, - bounds: Bounds, - texture_view: ID3D11ShaderResourceView, - // holding on to the texture to not RAII drop it - _texture: ID3D11Texture2D, -} - -impl GlyphLayerTexture { - fn new( - gpu_state: &GPUState, - run_color: Rgba, - bounds: Bounds, - alpha_data: &[u8], - ) -> Result { - let texture_size = bounds.size; - - let desc = D3D11_TEXTURE2D_DESC { - Width: texture_size.width as u32, - Height: texture_size.height as u32, - MipLevels: 1, - ArraySize: 1, - Format: DXGI_FORMAT_R8_UNORM, - SampleDesc: DXGI_SAMPLE_DESC { - Count: 1, - Quality: 0, - }, - Usage: D3D11_USAGE_DEFAULT, - BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32, - CPUAccessFlags: 0, - MiscFlags: 0, - }; - - let texture = { - let mut texture: Option = None; - unsafe { - gpu_state - .device - .CreateTexture2D(&desc, None, Some(&mut texture))? - }; - texture.unwrap() - }; - let texture_view = { - let mut view: Option = None; - unsafe { - gpu_state - .device - .CreateShaderResourceView(&texture, None, Some(&mut view))? - }; - view.unwrap() - }; - - unsafe { - gpu_state.device_context.UpdateSubresource( - &texture, - 0, - None, - alpha_data.as_ptr() as _, - texture_size.width as u32, - 0, - ) - }; - - Ok(GlyphLayerTexture { - run_color, - bounds, - texture_view, - _texture: texture, - }) - } -} - -#[repr(C)] -struct GlyphLayerTextureParams { - bounds: Bounds, - run_color: Rgba, - gamma_ratios: [f32; 4], - grayscale_enhanced_contrast: f32, - _pad: [f32; 3], -} - -struct TextRendererWrapper(IDWriteTextRenderer); - -impl TextRendererWrapper { - fn new(locale_str: HSTRING) -> Self { - let inner = TextRenderer::new(locale_str); - TextRendererWrapper(inner.into()) - } -} - -#[implement(IDWriteTextRenderer)] -struct TextRenderer { - locale: HSTRING, -} - -impl TextRenderer { - fn new(locale: HSTRING) -> Self { - TextRenderer { locale } - } -} - -struct RendererContext<'t, 'a, 'b> { - text_system: &'t mut DirectWriteState, - components: &'a DirectWriteComponents, - index_converter: StringIndexConverter<'a>, - runs: &'b mut Vec, - width: f32, -} - -#[derive(Debug)] -struct ClusterAnalyzer<'t> { - utf16_idx: usize, - glyph_idx: usize, - glyph_count: usize, - cluster_map: &'t [u16], -} - -impl<'t> ClusterAnalyzer<'t> { - fn new(cluster_map: &'t [u16], glyph_count: usize) -> Self { - ClusterAnalyzer { - utf16_idx: 0, - glyph_idx: 0, - glyph_count, - cluster_map, - } - } -} - -impl Iterator for ClusterAnalyzer<'_> { - type Item = (usize, usize); - - fn next(&mut self) -> Option<(usize, usize)> { - if self.utf16_idx >= self.cluster_map.len() { - return None; // No more clusters - } - let start_utf16_idx = self.utf16_idx; - let current_glyph = self.cluster_map[start_utf16_idx] as usize; - - // Find the end of current cluster (where glyph index changes) - let mut end_utf16_idx = start_utf16_idx + 1; - while end_utf16_idx < self.cluster_map.len() - && self.cluster_map[end_utf16_idx] as usize == current_glyph - { - end_utf16_idx += 1; - } - - let utf16_len = end_utf16_idx - start_utf16_idx; - - // Calculate glyph count for this cluster - let next_glyph = if end_utf16_idx < self.cluster_map.len() { - self.cluster_map[end_utf16_idx] as usize - } else { - self.glyph_count - }; - - let glyph_count = next_glyph - current_glyph; - - // Update state for next call - self.utf16_idx = end_utf16_idx; - self.glyph_idx = next_glyph; - - Some((utf16_len, glyph_count)) - } -} - -#[allow(non_snake_case)] -impl IDWritePixelSnapping_Impl for TextRenderer_Impl { - fn IsPixelSnappingDisabled( - &self, - _clientdrawingcontext: *const ::core::ffi::c_void, - ) -> windows::core::Result { - Ok(BOOL(0)) - } - - fn GetCurrentTransform( - &self, - _clientdrawingcontext: *const ::core::ffi::c_void, - transform: *mut DWRITE_MATRIX, - ) -> windows::core::Result<()> { - unsafe { - *transform = DWRITE_MATRIX { - m11: 1.0, - m12: 0.0, - m21: 0.0, - m22: 1.0, - dx: 0.0, - dy: 0.0, - }; - } - Ok(()) - } - - fn GetPixelsPerDip( - &self, - _clientdrawingcontext: *const ::core::ffi::c_void, - ) -> windows::core::Result { - Ok(1.0) - } -} - -#[allow(non_snake_case)] -impl IDWriteTextRenderer_Impl for TextRenderer_Impl { - fn DrawGlyphRun( - &self, - clientdrawingcontext: *const ::core::ffi::c_void, - _baselineoriginx: f32, - _baselineoriginy: f32, - _measuringmode: DWRITE_MEASURING_MODE, - glyphrun: *const DWRITE_GLYPH_RUN, - glyphrundescription: *const DWRITE_GLYPH_RUN_DESCRIPTION, - _clientdrawingeffect: windows::core::Ref, - ) -> windows::core::Result<()> { - let glyphrun = unsafe { &*glyphrun }; - let glyph_count = glyphrun.glyphCount as usize; - if glyph_count == 0 { - return Ok(()); - } - let desc = unsafe { &*glyphrundescription }; - let context = unsafe { &mut *(clientdrawingcontext.cast::().cast_mut()) }; - let Some(font_face) = glyphrun.fontFace.as_ref() else { - return Ok(()); - }; - // This `cast()` action here should never fail since we are running on Win10+, and - // `IDWriteFontFace3` requires Win10 - let Ok(font_face) = &font_face.cast::() else { - return Err(Error::new( - DWRITE_E_UNSUPPORTEDOPERATION, - "Failed to cast font face", - )); - }; - - let font_face_key = font_face.cast::().unwrap().as_raw().addr(); - let font_id = context - .text_system - .font_info_cache - .get(&font_face_key) - .copied() - // in some circumstances, we might be getting served a FontFace that we did not create ourselves - // so create a new font from it and cache it accordingly. The usual culprit here seems to be Segoe UI Symbol - .map_or_else( - || { - let font = font_face_to_font(font_face, &self.locale) - .ok_or_else(|| Error::new(DWRITE_E_NOFONT, "Failed to create font"))?; - let font_id = match context.text_system.font_to_font_id.get(&font) { - Some(&font_id) => font_id, - None => context - .text_system - .select_and_cache_font(context.components, &font) - .ok_or_else(|| Error::new(DWRITE_E_NOFONT, "Failed to create font"))?, - }; - context - .text_system - .font_info_cache - .insert(font_face_key, font_id); - windows::core::Result::Ok(font_id) - }, - Ok, - )?; - - let color_font = unsafe { font_face.IsColorFont().as_bool() }; - - let glyph_ids = unsafe { - slice_from_nullable( - glyphrun.glyphIndices, - glyph_count, - "DirectWrite returned a null glyph indices array", - )? - }; - let glyph_advances = unsafe { - slice_from_nullable( - glyphrun.glyphAdvances, - glyph_count, - "DirectWrite returned a null glyph advances array", - )? - }; - let glyph_offsets = unsafe { - slice_from_nullable( - glyphrun.glyphOffsets, - glyph_count, - "DirectWrite returned a null glyph offsets array", - )? - }; - let cluster_map = unsafe { - slice_from_nullable( - desc.clusterMap, - desc.stringLength as usize, - "DirectWrite returned a null cluster map", - )? - }; - - let cluster_analyzer = ClusterAnalyzer::new(cluster_map, glyph_count); - let mut utf16_idx = desc.textPosition as usize; - let mut glyph_idx = 0; - let mut glyphs = Vec::with_capacity(glyph_count); - for (cluster_utf16_len, cluster_glyph_count) in cluster_analyzer { - context.index_converter.advance_to_utf16_ix(utf16_idx); - utf16_idx += cluster_utf16_len; - for (cluster_glyph_idx, glyph_id) in glyph_ids - [glyph_idx..(glyph_idx + cluster_glyph_count)] - .iter() - .enumerate() - { - let id = GlyphId(*glyph_id as u32); - let is_emoji = - color_font && is_color_glyph(font_face, id, &context.components.factory); - let this_glyph_idx = glyph_idx + cluster_glyph_idx; - glyphs.push(ShapedGlyph { - id, - position: point( - px(context.width + glyph_offsets[this_glyph_idx].advanceOffset), - px(-glyph_offsets[this_glyph_idx].ascenderOffset), - ), - index: context.index_converter.utf8_ix, - is_emoji, - }); - context.width += glyph_advances[this_glyph_idx]; - } - glyph_idx += cluster_glyph_count; - } - context.runs.push(ShapedRun { font_id, glyphs }); - Ok(()) - } - - fn DrawUnderline( - &self, - _clientdrawingcontext: *const ::core::ffi::c_void, - _baselineoriginx: f32, - _baselineoriginy: f32, - _underline: *const DWRITE_UNDERLINE, - _clientdrawingeffect: windows::core::Ref, - ) -> windows::core::Result<()> { - Err(windows::core::Error::new( - E_NOTIMPL, - "DrawUnderline unimplemented", - )) - } - - fn DrawStrikethrough( - &self, - _clientdrawingcontext: *const ::core::ffi::c_void, - _baselineoriginx: f32, - _baselineoriginy: f32, - _strikethrough: *const DWRITE_STRIKETHROUGH, - _clientdrawingeffect: windows::core::Ref, - ) -> windows::core::Result<()> { - Err(windows::core::Error::new( - E_NOTIMPL, - "DrawStrikethrough unimplemented", - )) - } - - fn DrawInlineObject( - &self, - _clientdrawingcontext: *const ::core::ffi::c_void, - _originx: f32, - _originy: f32, - _inlineobject: windows::core::Ref, - _issideways: BOOL, - _isrighttoleft: BOOL, - _clientdrawingeffect: windows::core::Ref, - ) -> windows::core::Result<()> { - Err(windows::core::Error::new( - E_NOTIMPL, - "DrawInlineObject unimplemented", - )) - } -} - -/// Interprets an optional DirectWrite array pointer as a slice, treating a -/// null pointer with a zero length as an empty slice. A null pointer with a -/// nonzero length fails with `null_error_message`. -/// -/// # Safety -/// -/// When `ptr` is non-null, the caller must guarantee that it points to a valid -/// array of at least `len` elements that outlives the returned slice. -unsafe fn slice_from_nullable<'a, T>( - ptr: *const T, - len: usize, - null_error_message: &str, -) -> windows::core::Result<&'a [T]> { - if ptr.is_null() { - if len != 0 { - return Err(Error::new(E_INVALIDARG, null_error_message)); - } - Ok(&[]) - } else { - Ok(unsafe { std::slice::from_raw_parts(ptr, len) }) - } -} - -struct StringIndexConverter<'a> { - text: &'a str, - utf8_ix: usize, - utf16_ix: usize, -} - -impl<'a> StringIndexConverter<'a> { - fn new(text: &'a str) -> Self { - Self { - text, - utf8_ix: 0, - utf16_ix: 0, - } - } - - #[allow(dead_code)] - fn advance_to_utf8_ix(&mut self, utf8_target: usize) { - for (ix, c) in self.text[self.utf8_ix..].char_indices() { - if self.utf8_ix + ix >= utf8_target { - self.utf8_ix += ix; - return; - } - self.utf16_ix += c.len_utf16(); - } - self.utf8_ix = self.text.len(); - } - - fn advance_to_utf16_ix(&mut self, utf16_target: usize) { - for (ix, c) in self.text[self.utf8_ix..].char_indices() { - if self.utf16_ix >= utf16_target { - self.utf8_ix += ix; - return; - } - self.utf16_ix += c.len_utf16(); - } - self.utf8_ix = self.text.len(); - } -} - -fn font_style_to_dwrite(style: FontStyle) -> DWRITE_FONT_STYLE { - match style { - FontStyle::Normal => DWRITE_FONT_STYLE_NORMAL, - FontStyle::Italic => DWRITE_FONT_STYLE_ITALIC, - FontStyle::Oblique => DWRITE_FONT_STYLE_OBLIQUE, - } -} - -fn font_style_from_dwrite(value: DWRITE_FONT_STYLE) -> FontStyle { - match value.0 { - 0 => FontStyle::Normal, - 1 => FontStyle::Italic, - 2 => FontStyle::Oblique, - _ => unreachable!(), - } -} - -fn font_weight_to_dwrite(weight: FontWeight) -> DWRITE_FONT_WEIGHT { - DWRITE_FONT_WEIGHT(weight.0 as i32) -} - -fn font_weight_from_dwrite(value: DWRITE_FONT_WEIGHT) -> FontWeight { - FontWeight(value.0 as f32) -} - -fn get_font_names_from_collection( - collection: &IDWriteFontCollection1, - locale: &HSTRING, -) -> Vec { - unsafe { - let mut result = Vec::new(); - let family_count = collection.GetFontFamilyCount(); - for index in 0..family_count { - let Some(font_family) = collection.GetFontFamily(index).log_err() else { - continue; - }; - let Some(localized_family_name) = font_family.GetFamilyNames().log_err() else { - continue; - }; - let Some(family_name) = get_name(localized_family_name, locale).log_err() else { - continue; - }; - result.push(family_name); - } - - result - } -} - -fn font_face_to_font(font_face: &IDWriteFontFace3, locale: &HSTRING) -> Option { - let localized_family_name = unsafe { font_face.GetFamilyNames().log_err() }?; - let family_name = get_name(localized_family_name, locale).log_err()?; - let weight = unsafe { font_face.GetWeight() }; - let style = unsafe { font_face.GetStyle() }; - Some(Font { - family: family_name.into(), - features: FontFeatures::default(), - weight: font_weight_from_dwrite(weight), - style: font_style_from_dwrite(style), - fallbacks: None, - }) -} - -// https://learn.microsoft.com/en-us/windows/win32/api/dwrite/ne-dwrite-dwrite_font_feature_tag -fn apply_font_features( - direct_write_features: &IDWriteTypography, - features: &FontFeatures, -) -> Result<()> { - let tag_values = features.tag_value_list(); - if tag_values.is_empty() { - return Ok(()); - } - - // All of these features are enabled by default by DirectWrite. - // If you want to (and can) peek into the source of DirectWrite - let mut feature_liga = make_direct_write_feature("liga", 1); - let mut feature_clig = make_direct_write_feature("clig", 1); - let mut feature_calt = make_direct_write_feature("calt", 1); - - for (tag, value) in tag_values { - if tag.as_str() == "liga" && *value == 0 { - feature_liga.parameter = 0; - continue; - } - if tag.as_str() == "clig" && *value == 0 { - feature_clig.parameter = 0; - continue; - } - if tag.as_str() == "calt" && *value == 0 { - feature_calt.parameter = 0; - continue; - } - - unsafe { - direct_write_features.AddFontFeature(make_direct_write_feature(tag, *value))?; - } - } - unsafe { - direct_write_features.AddFontFeature(feature_liga)?; - direct_write_features.AddFontFeature(feature_clig)?; - direct_write_features.AddFontFeature(feature_calt)?; - } - - Ok(()) -} - -#[inline] -const fn make_direct_write_feature(feature_name: &str, parameter: u32) -> DWRITE_FONT_FEATURE { - let tag = make_direct_write_tag(feature_name); - DWRITE_FONT_FEATURE { - nameTag: tag, - parameter, - } -} - -#[inline] -const fn make_open_type_tag(tag_name: &str) -> u32 { - let bytes = tag_name.as_bytes(); - debug_assert!(bytes.len() == 4); - u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) -} - -#[inline] -const fn make_direct_write_tag(tag_name: &str) -> DWRITE_FONT_FEATURE_TAG { - DWRITE_FONT_FEATURE_TAG(make_open_type_tag(tag_name)) -} - -#[inline] -fn get_name(string: IDWriteLocalizedStrings, locale: &HSTRING) -> Result { - let mut locale_name_index = 0u32; - let mut exists = BOOL(0); - unsafe { string.FindLocaleName(locale, &mut locale_name_index, &mut exists as _)? }; - if !exists.as_bool() { - unsafe { - string.FindLocaleName( - DEFAULT_LOCALE_NAME, - &mut locale_name_index as _, - &mut exists as _, - )? - }; - anyhow::ensure!(exists.as_bool(), "No localised string for {locale}"); - } - - let name_length = unsafe { string.GetStringLength(locale_name_index) }? as usize; - let mut name_vec = vec![0u16; name_length + 1]; - unsafe { - string.GetString(locale_name_index, &mut name_vec)?; - } - - Ok(String::from_utf16_lossy(&name_vec[..name_length])) -} - -fn get_system_subpixel_rendering() -> bool { - let mut value = c_uint::default(); - let result = unsafe { - SystemParametersInfoW( - SPI_GETFONTSMOOTHINGTYPE, - 0, - Some((&mut value) as *mut c_uint as *mut c_void), - SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS::default(), - ) - }; - if result.log_err().is_some() { - value == FE_FONTSMOOTHINGCLEARTYPE - } else { - true - } -} - -fn get_system_ui_font_name() -> SharedString { - unsafe { - let mut info: LOGFONTW = std::mem::zeroed(); - let font_family = if SystemParametersInfoW( - SPI_GETICONTITLELOGFONT, - std::mem::size_of::() as u32, - Some(&mut info as *mut _ as _), - SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0), - ) - .log_err() - .is_none() - { - // https://learn.microsoft.com/en-us/windows/win32/uxguide/vis-fonts - // Segoe UI is the Windows font intended for user interface text strings. - "Segoe UI".into() - } else { - let font_name = String::from_utf16_lossy(&info.lfFaceName); - font_name.trim_matches(char::from(0)).to_owned().into() - }; - log::info!("Use {} as UI font.", font_family); - font_family - } -} - -// One would think that with newer DirectWrite method: IDWriteFontFace4::GetGlyphImageFormats -// but that doesn't seem to work for some glyphs, say ❤ -fn is_color_glyph( - font_face: &IDWriteFontFace3, - glyph_id: GlyphId, - factory: &IDWriteFactory5, -) -> bool { - let glyph_run = DWRITE_GLYPH_RUN { - fontFace: ManuallyDrop::new(Some(unsafe { std::ptr::read(&****font_face) })), - fontEmSize: 14.0, - glyphCount: 1, - glyphIndices: &(glyph_id.0 as u16), - glyphAdvances: &0.0, - glyphOffsets: &DWRITE_GLYPH_OFFSET { - advanceOffset: 0.0, - ascenderOffset: 0.0, - }, - isSideways: BOOL(0), - bidiLevel: 0, - }; - unsafe { - factory.TranslateColorGlyphRun( - Vector2::default(), - &glyph_run as _, - None, - DWRITE_GLYPH_IMAGE_FORMATS_COLR - | DWRITE_GLYPH_IMAGE_FORMATS_SVG - | DWRITE_GLYPH_IMAGE_FORMATS_PNG - | DWRITE_GLYPH_IMAGE_FORMATS_JPEG - | DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8, - DWRITE_MEASURING_MODE_NATURAL, - None, - 0, - ) - } - .is_ok() -} - -const DEFAULT_LOCALE_NAME: PCWSTR = windows::core::w!("en-US"); - -#[cfg(test)] -mod tests { - use super::{DirectWriteState, DirectWriteTextSystem, GPUState, GlyphLayerTexture}; - use crate::direct_write::ClusterAnalyzer; - use crate::directx_devices::DirectXDevices; - use anyhow::Result; - use gpui::{ - DevicePixels, Font, PlatformTextSystem, RenderGlyphParams, Rgba, bounds, point, px, size, - }; - use std::ffi::c_void; - use windows::Win32::Graphics::Direct3D11::{ - D3D11_BIND_RENDER_TARGET, D3D11_RENDER_TARGET_VIEW_DESC, D3D11_RENDER_TARGET_VIEW_DESC_0, - D3D11_RTV_DIMENSION_TEXTURE2D, D3D11_SUBRESOURCE_DATA, D3D11_TEX2D_RTV, - D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, - }; - use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_SAMPLE_DESC}; - - #[test] - fn test_cluster_map() { - let cluster_map = [0]; - let mut analyzer = ClusterAnalyzer::new(&cluster_map, 1); - let next = analyzer.next(); - assert_eq!(next, Some((1, 1))); - let next = analyzer.next(); - assert_eq!(next, None); - - let cluster_map = [0, 1, 2]; - let mut analyzer = ClusterAnalyzer::new(&cluster_map, 3); - let next = analyzer.next(); - assert_eq!(next, Some((1, 1))); - let next = analyzer.next(); - assert_eq!(next, Some((1, 1))); - let next = analyzer.next(); - assert_eq!(next, Some((1, 1))); - let next = analyzer.next(); - assert_eq!(next, None); - // 👨‍👩‍👧‍👦👩‍💻 - let cluster_map = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 4]; - let mut analyzer = ClusterAnalyzer::new(&cluster_map, 5); - let next = analyzer.next(); - assert_eq!(next, Some((11, 4))); - let next = analyzer.next(); - assert_eq!(next, Some((5, 1))); - let next = analyzer.next(); - assert_eq!(next, None); - // 👩‍💻 - let cluster_map = [0, 0, 0, 0, 0]; - let mut analyzer = ClusterAnalyzer::new(&cluster_map, 1); - let next = analyzer.next(); - assert_eq!(next, Some((5, 1))); - let next = analyzer.next(); - assert_eq!(next, None); - } - - #[test] - fn color_emoji_rasterization_is_stable_across_batches() -> Result<()> { - let devices = DirectXDevices::new()?; - let text_system = DirectWriteTextSystem::new(&devices)?; - - let font = Font { - family: "Segoe UI Emoji".into(), - ..Default::default() - }; - let font_id = text_system.font_id(&font)?; - - let mut params_list = Vec::new(); - for ch in ['🫠', '🥹', '🧗', '🏋', '🚀', '🥺'] { - let Some(glyph_id) = text_system.glyph_for_char(font_id, ch) else { - log::info!("no glyph found for {ch}"); - continue; - }; - let params = RenderGlyphParams { - font_id, - glyph_id, - font_size: px(48.0), - subpixel_variant: point(0u8, 0u8), - scale_factor: 1.0, - is_emoji: true, - subpixel_rendering: false, - dilation: 0, - }; - let raster_bounds = text_system.glyph_raster_bounds(¶ms)?; - if raster_bounds.size.width.0 == 0 || raster_bounds.size.height.0 == 0 { - log::info!("raster bounds are empty for {ch}"); - continue; - } - params_list.push((params, raster_bounds)); - } - assert!(!params_list.is_empty()); - - let first: Vec<_> = params_list - .iter() - .map(|(params, bounds)| text_system.rasterize_glyph(params, *bounds)) - .collect::>()?; - - // Churn the texture heap with further rasterization passes. If the color - // compositing leaks leftover texture data (the render target is not cleared), - // the second batch can pick up different contents and differ from the first. - // With an explicit clear both batches are deterministic and identical. - for _ in 0..3 { - for (params, bounds) in ¶ms_list { - text_system.rasterize_glyph(params, *bounds)?; - } - } - let second: Vec<_> = params_list - .iter() - .map(|(params, bounds)| text_system.rasterize_glyph(params, *bounds)) - .collect::>()?; - - assert_eq!( - first, second, - "color glyph rasterization changed between batches; \ - render target contents are leaking into the glyph bitmaps" - ); - Ok(()) - } - - #[test] - fn color_emoji_composites_over_cleared_texture() -> Result<()> { - let devices = DirectXDevices::new()?; - let gpu_state = GPUState::new(&devices)?; - - const SIZE: u32 = 32; - // Seed the render target with solid red so that any texel which the - // compositing pass fails to clear/overwrite remains identifiable after - // the readback. - let poison = { - let mut v = vec![0u8; (SIZE * SIZE * 4) as usize]; - for pixel in v.chunks_exact_mut(4) { - pixel[2] = 255; - pixel[3] = 255; - } - v - }; - let desc = D3D11_TEXTURE2D_DESC { - Width: SIZE, - Height: SIZE, - MipLevels: 1, - ArraySize: 1, - Format: DXGI_FORMAT_B8G8R8A8_UNORM, - SampleDesc: DXGI_SAMPLE_DESC { - Count: 1, - Quality: 0, - }, - Usage: D3D11_USAGE_DEFAULT, - BindFlags: D3D11_BIND_RENDER_TARGET.0 as u32, - CPUAccessFlags: 0, - MiscFlags: 0, - }; - let initial_data = D3D11_SUBRESOURCE_DATA { - pSysMem: poison.as_ptr() as *const c_void, - SysMemPitch: SIZE * 4, - SysMemSlicePitch: 0, - }; - let texture = unsafe { - let mut texture = None; - gpu_state - .device - .CreateTexture2D(&desc, Some(&initial_data), Some(&mut texture))?; - texture.unwrap() - }; - let render_target_view = unsafe { - let desc = D3D11_RENDER_TARGET_VIEW_DESC { - Format: DXGI_FORMAT_B8G8R8A8_UNORM, - ViewDimension: D3D11_RTV_DIMENSION_TEXTURE2D, - Anonymous: D3D11_RENDER_TARGET_VIEW_DESC_0 { - Texture2D: D3D11_TEX2D_RTV { MipSlice: 0 }, - }, - }; - let mut rtv = None; - gpu_state - .device - .CreateRenderTargetView(&texture, Some(&desc), Some(&mut rtv))?; - rtv.unwrap() - }; - - // A single opaque layer in the top-left corner; the bottom-right corner - // of the texture is covered by no layer at all. - let layer_alpha = vec![255u8; 4 * 4]; - let layer = GlyphLayerTexture::new( - &gpu_state, - Rgba { - r: 1.0, - g: 1.0, - b: 1.0, - a: 1.0, - }, - bounds(point(0, 0), size(4, 4)), - &layer_alpha, - )?; - - let rasterized = DirectWriteState::composite_color_layers( - &gpu_state, - std::slice::from_ref(&layer), - size(DevicePixels(SIZE as i32), DevicePixels(SIZE as i32)), - &texture, - &Some(render_target_view), - )?; - - let corner = (SIZE as usize - 1 + (SIZE as usize - 1) * SIZE as usize) * 4; - assert_eq!( - &rasterized[corner..corner + 4], - &[0, 0, 0, 0], - "uncovered texel retained the poison from the uninitialized render target" - ); - Ok(()) - } -} diff --git a/crates/gpui_pre_windows/src/directx_atlas.rs b/crates/gpui_pre_windows/src/directx_atlas.rs deleted file mode 100644 index 6238699..0000000 --- a/crates/gpui_pre_windows/src/directx_atlas.rs +++ /dev/null @@ -1,420 +0,0 @@ -use collections::FxHashMap; -use etagere::BucketedAtlasAllocator; -use parking_lot::Mutex; -use windows::Win32::Graphics::{ - Direct3D11::{ - D3D11_BIND_SHADER_RESOURCE, D3D11_BOX, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, - ID3D11Device, ID3D11DeviceContext, ID3D11ShaderResourceView, ID3D11Texture2D, - }, - Dxgi::Common::*, -}; - -use gpui::{ - AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTextureList, AtlasTile, Bounds, DevicePixels, - PlatformAtlas, Point, Size, -}; - -pub(crate) struct DirectXAtlas(Mutex); - -struct DirectXAtlasState { - device: ID3D11Device, - device_context: ID3D11DeviceContext, - monochrome_textures: AtlasTextureList, - polychrome_textures: AtlasTextureList, - subpixel_textures: AtlasTextureList, - tiles_by_key: FxHashMap, -} - -struct DirectXAtlasTexture { - id: AtlasTextureId, - bytes_per_pixel: u32, - allocator: BucketedAtlasAllocator, - texture: ID3D11Texture2D, - view: [Option; 1], - live_atlas_keys: u32, -} - -impl DirectXAtlas { - pub(crate) fn new(device: &ID3D11Device, device_context: &ID3D11DeviceContext) -> Self { - DirectXAtlas(Mutex::new(DirectXAtlasState { - device: device.clone(), - device_context: device_context.clone(), - monochrome_textures: Default::default(), - polychrome_textures: Default::default(), - subpixel_textures: Default::default(), - tiles_by_key: Default::default(), - })) - } - - pub(crate) fn get_texture_view( - &self, - id: AtlasTextureId, - ) -> [Option; 1] { - let lock = self.0.lock(); - let tex = lock.texture(id); - tex.view.clone() - } - - pub(crate) fn handle_device_lost( - &self, - device: &ID3D11Device, - device_context: &ID3D11DeviceContext, - ) { - let mut lock = self.0.lock(); - lock.device = device.clone(); - lock.device_context = device_context.clone(); - lock.monochrome_textures = AtlasTextureList::default(); - lock.polychrome_textures = AtlasTextureList::default(); - lock.subpixel_textures = AtlasTextureList::default(); - lock.tiles_by_key.clear(); - } -} - -impl PlatformAtlas for DirectXAtlas { - fn get_or_insert_with<'a>( - &self, - key: &AtlasKey, - build: &mut dyn FnMut() -> anyhow::Result< - Option<(Size, std::borrow::Cow<'a, [u8]>)>, - >, - ) -> anyhow::Result> { - let mut lock = self.0.lock(); - if let Some(tile) = lock.tiles_by_key.get(key) { - Ok(Some(*tile)) - } else { - let Some((size, bytes)) = build()? else { - return Ok(None); - }; - let tile = lock - .allocate(size, key.texture_kind()) - .ok_or_else(|| anyhow::anyhow!("failed to allocate"))?; - let texture = lock.texture(tile.texture_id); - texture.upload(&lock.device_context, tile.bounds, &bytes); - lock.tiles_by_key.insert(key.clone(), tile); - Ok(Some(tile)) - } - } - - fn remove(&self, key: &AtlasKey) { - let mut lock = self.0.lock(); - - let Some(tile) = lock.tiles_by_key.remove(key) else { - return; - }; - let id = tile.texture_id; - - let textures = match id.kind { - AtlasTextureKind::Monochrome => &mut lock.monochrome_textures, - AtlasTextureKind::Polychrome => &mut lock.polychrome_textures, - AtlasTextureKind::Subpixel => &mut lock.subpixel_textures, - }; - - let Some(texture_slot) = textures.textures.get_mut(id.index as usize) else { - return; - }; - - if let Some(mut texture) = texture_slot.take() { - texture.allocator.deallocate(tile.tile_id.into()); - texture.decrement_ref_count(); - if texture.is_unreferenced() { - textures.free_list.push(texture.id.index as usize); - } else { - *texture_slot = Some(texture); - } - } - } -} - -impl DirectXAtlasState { - fn allocate( - &mut self, - size: Size, - texture_kind: AtlasTextureKind, - ) -> Option { - { - let textures = match texture_kind { - AtlasTextureKind::Monochrome => &mut self.monochrome_textures, - AtlasTextureKind::Polychrome => &mut self.polychrome_textures, - AtlasTextureKind::Subpixel => &mut self.subpixel_textures, - }; - - if let Some(tile) = textures - .iter_mut() - .rev() - .find_map(|texture| texture.allocate(size)) - { - return Some(tile); - } - } - - let texture = self.push_texture(size, texture_kind)?; - texture.allocate(size) - } - - fn push_texture( - &mut self, - min_size: Size, - kind: AtlasTextureKind, - ) -> Option<&mut DirectXAtlasTexture> { - const DEFAULT_ATLAS_SIZE: Size = Size { - width: DevicePixels(1024), - height: DevicePixels(1024), - }; - // Max texture size for DirectX. See: - // https://learn.microsoft.com/en-us/windows/win32/direct3d11/overviews-direct3d-11-resources-limits - const MAX_ATLAS_SIZE: Size = Size { - width: DevicePixels(16384), - height: DevicePixels(16384), - }; - let size = min_size.min(&MAX_ATLAS_SIZE).max(&DEFAULT_ATLAS_SIZE); - let pixel_format; - let bind_flag; - let bytes_per_pixel; - match kind { - AtlasTextureKind::Monochrome => { - pixel_format = DXGI_FORMAT_R8_UNORM; - bind_flag = D3D11_BIND_SHADER_RESOURCE; - bytes_per_pixel = 1; - } - AtlasTextureKind::Polychrome => { - pixel_format = DXGI_FORMAT_B8G8R8A8_UNORM; - bind_flag = D3D11_BIND_SHADER_RESOURCE; - bytes_per_pixel = 4; - } - AtlasTextureKind::Subpixel => { - pixel_format = DXGI_FORMAT_R8G8B8A8_UNORM; - bind_flag = D3D11_BIND_SHADER_RESOURCE; - bytes_per_pixel = 4; - } - } - let texture_desc = D3D11_TEXTURE2D_DESC { - Width: size.width.0 as u32, - Height: size.height.0 as u32, - MipLevels: 1, - ArraySize: 1, - Format: pixel_format, - SampleDesc: DXGI_SAMPLE_DESC { - Count: 1, - Quality: 0, - }, - Usage: D3D11_USAGE_DEFAULT, - BindFlags: bind_flag.0 as u32, - CPUAccessFlags: 0, - MiscFlags: 0, - }; - let mut texture: Option = None; - unsafe { - // This only returns None if the device is lost, which we will recreate later. - // So it's ok to return None here. - self.device - .CreateTexture2D(&texture_desc, None, Some(&mut texture)) - .ok()?; - } - let texture = texture.unwrap(); - - let texture_list = match kind { - AtlasTextureKind::Monochrome => &mut self.monochrome_textures, - AtlasTextureKind::Polychrome => &mut self.polychrome_textures, - AtlasTextureKind::Subpixel => &mut self.subpixel_textures, - }; - let index = texture_list.free_list.pop(); - let view = unsafe { - let mut view = None; - self.device - .CreateShaderResourceView(&texture, None, Some(&mut view)) - .ok()?; - [view] - }; - let atlas_texture = DirectXAtlasTexture { - id: AtlasTextureId { - index: index.unwrap_or(texture_list.textures.len()) as u32, - kind, - }, - bytes_per_pixel, - allocator: etagere::BucketedAtlasAllocator::new(device_size_to_etagere(size)), - texture, - view, - live_atlas_keys: 0, - }; - if let Some(ix) = index { - texture_list.textures[ix] = Some(atlas_texture); - texture_list.textures.get_mut(ix).unwrap().as_mut() - } else { - texture_list.textures.push(Some(atlas_texture)); - texture_list.textures.last_mut().unwrap().as_mut() - } - } - - fn texture(&self, id: AtlasTextureId) -> &DirectXAtlasTexture { - match id.kind { - AtlasTextureKind::Monochrome => &self.monochrome_textures[id.index as usize] - .as_ref() - .unwrap(), - AtlasTextureKind::Polychrome => &self.polychrome_textures[id.index as usize] - .as_ref() - .unwrap(), - AtlasTextureKind::Subpixel => { - &self.subpixel_textures[id.index as usize].as_ref().unwrap() - } - } - } -} - -impl DirectXAtlasTexture { - fn allocate(&mut self, size: Size) -> Option { - let allocation = self.allocator.allocate(device_size_to_etagere(size))?; - let tile = AtlasTile { - texture_id: self.id, - tile_id: allocation.id.into(), - bounds: Bounds { - origin: etagere_point_to_device(allocation.rectangle.min), - size, - }, - padding: 0, - }; - self.live_atlas_keys += 1; - Some(tile) - } - - fn upload( - &self, - device_context: &ID3D11DeviceContext, - bounds: Bounds, - bytes: &[u8], - ) { - // `UpdateSubresource` reads `row_pitch * height` bytes from `bytes` based on the - // `D3D11_BOX` below. If the caller hands us a slice shorter than that, the driver would - // over-read past the end of the source buffer (potentially by multiple megabytes), so bail - // out instead. This is a first-insert path rather than a per-frame one, so the check is - // effectively free. - let row_bytes = bounds.size.width.to_bytes(self.bytes_per_pixel as u8) as usize; - let expected = row_bytes * bounds.size.height.0.max(0) as usize; - if bytes.len() < expected { - log::error!( - "DirectXAtlasTexture::upload: source slice is {} bytes but the {}x{} region \ - requires {} bytes; skipping upload to avoid a driver over-read", - bytes.len(), - bounds.size.width.0, - bounds.size.height.0, - expected, - ); - return; - } - unsafe { - device_context.UpdateSubresource( - &self.texture, - 0, - Some(&D3D11_BOX { - left: bounds.left().0 as u32, - top: bounds.top().0 as u32, - front: 0, - right: bounds.right().0 as u32, - bottom: bounds.bottom().0 as u32, - back: 1, - }), - bytes.as_ptr() as _, - bounds.size.width.to_bytes(self.bytes_per_pixel as u8), - 0, - ); - } - } - - fn decrement_ref_count(&mut self) { - self.live_atlas_keys -= 1; - } - - fn is_unreferenced(&mut self) -> bool { - self.live_atlas_keys == 0 - } -} - -fn device_size_to_etagere(size: Size) -> etagere::Size { - etagere::Size::new(size.width.into(), size.height.into()) -} - -fn etagere_point_to_device(value: etagere::Point) -> Point { - Point { - x: DevicePixels::from(value.x), - y: DevicePixels::from(value.y), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::{ImageId, RenderImageParams}; - use std::borrow::Cow; - use windows::Win32::{ - Foundation::HMODULE, - Graphics::{ - Direct3D::D3D_DRIVER_TYPE_WARP, - Direct3D11::{D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_SDK_VERSION, D3D11CreateDevice}, - }, - }; - - fn create_atlas() -> Option { - let mut device: Option = None; - let mut device_context: Option = None; - unsafe { - D3D11CreateDevice( - None, - D3D_DRIVER_TYPE_WARP, - HMODULE::default(), - D3D11_CREATE_DEVICE_BGRA_SUPPORT, - None, - D3D11_SDK_VERSION, - Some(&mut device), - None, - Some(&mut device_context), - ) - } - .ok()?; - Some(DirectXAtlas::new(&device?, &device_context?)) - } - - fn make_image_key(image_id: usize) -> AtlasKey { - AtlasKey::Image(RenderImageParams { - image_id: ImageId(image_id), - frame_index: 0, - }) - } - - fn insert_tile(atlas: &DirectXAtlas, key: &AtlasKey, size: Size) -> AtlasTile { - atlas - .get_or_insert_with(key, &mut || { - let byte_count = (size.width.0 as usize) * (size.height.0 as usize) * 4; - Ok(Some((size, Cow::Owned(vec![0u8; byte_count])))) - }) - .expect("allocation should succeed") - .expect("callback returns Some") - } - - #[test] - fn test_remove_deallocates_tile_space_for_reuse() { - let Some(atlas) = create_atlas() else { - return; - }; - - let small = Size { - width: DevicePixels(64), - height: DevicePixels(64), - }; - let big = Size { - width: DevicePixels(700), - height: DevicePixels(700), - }; - - let keeper_key = make_image_key(1); - let big_key_a = make_image_key(2); - let big_key_b = make_image_key(3); - - let keeper_tile = insert_tile(&atlas, &keeper_key, small); - let tile_a = insert_tile(&atlas, &big_key_a, big); - assert_eq!(keeper_tile.texture_id, tile_a.texture_id); - - atlas.remove(&big_key_a); - - let tile_b = insert_tile(&atlas, &big_key_b, big); - assert_eq!(tile_b.texture_id, keeper_tile.texture_id); - } -} diff --git a/crates/gpui_pre_windows/src/directx_devices.rs b/crates/gpui_pre_windows/src/directx_devices.rs deleted file mode 100644 index 8e65e6e..0000000 --- a/crates/gpui_pre_windows/src/directx_devices.rs +++ /dev/null @@ -1,194 +0,0 @@ -use anyhow::{Context, Result}; -use gpui_util::ResultExt; -use itertools::Itertools; -use windows::Win32::{ - Foundation::HMODULE, - Graphics::{ - Direct3D::{ - D3D_DRIVER_TYPE_UNKNOWN, D3D_FEATURE_LEVEL, D3D_FEATURE_LEVEL_10_1, - D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_11_1, - }, - Direct3D11::{ - D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_CREATE_DEVICE_DEBUG, - D3D11_FEATURE_D3D10_X_HARDWARE_OPTIONS, D3D11_FEATURE_DATA_D3D10_X_HARDWARE_OPTIONS, - D3D11_SDK_VERSION, D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, - }, - Dxgi::{ - CreateDXGIFactory2, DXGI_CREATE_FACTORY_DEBUG, DXGI_CREATE_FACTORY_FLAGS, - IDXGIAdapter1, IDXGIFactory6, - }, - }, -}; -use windows::core::Interface; - -pub(crate) fn try_to_recover_from_device_lost(mut f: impl FnMut() -> Result) -> Result { - (0..5) - .map(|i| { - if i > 0 { - // Add a small delay before retrying - std::thread::sleep(std::time::Duration::from_millis(100 + i * 10)); - } - f() - }) - .find_or_last(Result::is_ok) - .unwrap() - .context("DirectXRenderer failed to recover from lost device after multiple attempts") -} - -#[derive(Clone)] -pub(crate) struct DirectXDevices { - pub(crate) adapter: IDXGIAdapter1, - pub(crate) dxgi_factory: IDXGIFactory6, - pub(crate) device: ID3D11Device, - pub(crate) device_context: ID3D11DeviceContext, -} - -impl DirectXDevices { - pub(crate) fn new() -> Result { - let debug_layer_available = check_debug_layer_available(); - let dxgi_factory = - get_dxgi_factory(debug_layer_available).context("Creating DXGI factory")?; - let (adapter, device, device_context, feature_level) = - get_adapter(&dxgi_factory, debug_layer_available).context("Getting DXGI adapter")?; - match feature_level { - D3D_FEATURE_LEVEL_11_1 => { - log::info!("Created device with Direct3D 11.1 feature level.") - } - D3D_FEATURE_LEVEL_11_0 => { - log::info!("Created device with Direct3D 11.0 feature level.") - } - D3D_FEATURE_LEVEL_10_1 => { - log::info!("Created device with Direct3D 10.1 feature level.") - } - _ => unreachable!(), - } - - Ok(Self { - adapter, - dxgi_factory, - device, - device_context, - }) - } -} - -#[inline] -fn check_debug_layer_available() -> bool { - #[cfg(debug_assertions)] - { - use windows::Win32::Graphics::Dxgi::{DXGIGetDebugInterface1, IDXGIInfoQueue}; - - unsafe { DXGIGetDebugInterface1::(0) } - .log_err() - .is_some() - } - #[cfg(not(debug_assertions))] - { - false - } -} - -#[inline] -fn get_dxgi_factory(debug_layer_available: bool) -> Result { - let factory_flag = if debug_layer_available { - DXGI_CREATE_FACTORY_DEBUG - } else { - #[cfg(debug_assertions)] - log::warn!( - "Failed to get DXGI debug interface. DirectX debugging features will be disabled." - ); - DXGI_CREATE_FACTORY_FLAGS::default() - }; - unsafe { Ok(CreateDXGIFactory2(factory_flag)?) } -} - -#[inline] -fn get_adapter( - dxgi_factory: &IDXGIFactory6, - debug_layer_available: bool, -) -> Result<( - IDXGIAdapter1, - ID3D11Device, - ID3D11DeviceContext, - D3D_FEATURE_LEVEL, -)> { - for adapter_index in 0.. { - let adapter: IDXGIAdapter1 = unsafe { dxgi_factory.EnumAdapters(adapter_index)?.cast()? }; - if let Ok(desc) = unsafe { adapter.GetDesc1() } { - let gpu_name = String::from_utf16_lossy(&desc.Description) - .trim_matches(char::from(0)) - .to_string(); - log::info!("Using GPU: {}", gpu_name); - } - // Check to see whether the adapter supports Direct3D 11 and create - // the device if it does. - let mut context: Option = None; - let mut feature_level = D3D_FEATURE_LEVEL::default(); - if let Some(device) = get_device( - &adapter, - Some(&mut context), - Some(&mut feature_level), - debug_layer_available, - ) - .log_err() - { - return Ok((adapter, device, context.unwrap(), feature_level)); - } - } - - unreachable!() -} - -#[inline] -fn get_device( - adapter: &IDXGIAdapter1, - context: Option<*mut Option>, - feature_level: Option<*mut D3D_FEATURE_LEVEL>, - debug_layer_available: bool, -) -> Result { - let mut device: Option = None; - let device_flags = if debug_layer_available { - D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_DEBUG - } else { - D3D11_CREATE_DEVICE_BGRA_SUPPORT - }; - unsafe { - D3D11CreateDevice( - adapter, - D3D_DRIVER_TYPE_UNKNOWN, - HMODULE::default(), - device_flags, - // 4x MSAA is required for Direct3D Feature Level 10.1 or better - Some(&[ - D3D_FEATURE_LEVEL_11_1, - D3D_FEATURE_LEVEL_11_0, - D3D_FEATURE_LEVEL_10_1, - ]), - D3D11_SDK_VERSION, - Some(&mut device), - feature_level, - context, - )?; - } - let device = device.unwrap(); - let mut data = D3D11_FEATURE_DATA_D3D10_X_HARDWARE_OPTIONS::default(); - unsafe { - device - .CheckFeatureSupport( - D3D11_FEATURE_D3D10_X_HARDWARE_OPTIONS, - &mut data as *mut _ as _, - std::mem::size_of::() as u32, - ) - .context("Checking GPU device feature support")?; - } - if data - .ComputeShaders_Plus_RawAndStructuredBuffers_Via_Shader_4_x - .as_bool() - { - Ok(device) - } else { - Err(anyhow::anyhow!( - "Required feature StructuredBuffer is not supported by GPU/driver" - )) - } -} diff --git a/crates/gpui_pre_windows/src/directx_renderer.rs b/crates/gpui_pre_windows/src/directx_renderer.rs deleted file mode 100644 index 4221e0a..0000000 --- a/crates/gpui_pre_windows/src/directx_renderer.rs +++ /dev/null @@ -1,2102 +0,0 @@ -use std::{ - slice, - sync::{Arc, OnceLock}, -}; - -use anyhow::{Context, Result}; -use gpui_util::ResultExt; -use windows::{ - core::{Interface, HSTRING}, - Win32::{ - Foundation::HWND, - Graphics::{ - Direct3D::*, - Direct3D11::*, - DirectComposition::*, - DirectWrite::*, - Dxgi::{Common::*, *}, - }, - }, -}; - -use crate::directx_renderer::shader_resources::{RawShaderBytes, ShaderModule, ShaderTarget}; -use crate::*; -use gpui::*; - -pub(crate) const DISABLE_DIRECT_COMPOSITION: &str = "GPUI_DISABLE_DIRECT_COMPOSITION"; -const RENDER_TARGET_FORMAT: DXGI_FORMAT = DXGI_FORMAT_B8G8R8A8_UNORM; -// This configuration is used for MSAA rendering on paths only, and it's guaranteed to be supported by DirectX 11. -const PATH_MULTISAMPLE_COUNT: u32 = 4; -const MAX_INSTANCE_BUFFER_SIZE: usize = 256 * 1024 * 1024; - -pub(crate) struct FontInfo { - pub gamma_ratios: [f32; 4], - pub grayscale_enhanced_contrast: f32, - pub subpixel_enhanced_contrast: f32, - pub is_bgr: bool, -} - -pub(crate) struct DirectXRenderer { - hwnd: HWND, - atlas: Arc, - devices: Option, - resources: Option, - globals: DirectXGlobalElements, - pipelines: DirectXRenderPipelines, - direct_composition: Option, - font_info: &'static FontInfo, - - width: u32, - height: u32, - - /// Whether we want to skip drwaing due to device lost events. - /// - /// In that case we want to discard the first frame that we draw as we got reset in the middle of a frame - /// meaning we lost all the allocated gpu textures and scene resources. - skip_draws: bool, -} - -/// Direct3D objects -#[derive(Clone)] -pub(crate) struct DirectXRendererDevices { - pub(crate) adapter: IDXGIAdapter1, - pub(crate) dxgi_factory: IDXGIFactory6, - pub(crate) device: ID3D11Device, - pub(crate) device_context: ID3D11DeviceContext, - dxgi_device: Option, - annotation: Option, -} - -struct DirectXResources { - // Direct3D rendering objects - swap_chain: IDXGISwapChain1, - render_target: Option, - render_target_view: Option, - - // Path intermediate textures (with MSAA) - path_intermediate_texture: ID3D11Texture2D, - path_intermediate_srv: Option, - path_intermediate_msaa_texture: ID3D11Texture2D, - path_intermediate_msaa_view: Option, - - // Cached viewport - viewport: D3D11_VIEWPORT, -} - -struct DirectXRenderPipelines { - shadow_pipeline: PipelineState, - quad_pipeline: PipelineState, - path_rasterization_pipeline: PipelineState, - path_sprite_pipeline: PipelineState, - underline_pipeline: PipelineState, - mono_sprites: PipelineState, - subpixel_sprites: PipelineState, - poly_sprites: PipelineState, -} - -struct DirectXGlobalElements { - clip_buffer: ID3D11Buffer, - clip_view: Option, - clip_capacity: usize, - global_params_buffer: Option, - batch_params_buffer: Option, - sampler: Option, -} - -struct Annotation<'a>(&'a ID3DUserDefinedAnnotation); - -impl<'a> Annotation<'a> { - fn new(annotation: &'a ID3DUserDefinedAnnotation, label: HSTRING) -> Self { - unsafe { annotation.BeginEvent(&label) }; - Self(annotation) - } -} - -impl Drop for Annotation<'_> { - fn drop(&mut self) { - unsafe { self.0.EndEvent() }; - } -} - -struct DirectComposition { - comp_device: IDCompositionDevice, - comp_target: IDCompositionTarget, - comp_visual: IDCompositionVisual, -} - -impl DirectXRendererDevices { - pub(crate) fn new( - directx_devices: &DirectXDevices, - disable_direct_composition: bool, - ) -> Result { - let DirectXDevices { - adapter, - dxgi_factory, - device, - device_context, - } = directx_devices; - let dxgi_device = if disable_direct_composition { - None - } else { - Some(device.cast().context("Creating DXGI device")?) - }; - let annotation = device_context.cast().ok(); - - Ok(Self { - adapter: adapter.clone(), - dxgi_factory: dxgi_factory.clone(), - device: device.clone(), - device_context: device_context.clone(), - dxgi_device, - annotation, - }) - } -} - -impl DirectXRenderer { - pub(crate) fn new( - hwnd: HWND, - directx_devices: &DirectXDevices, - disable_direct_composition: bool, - ) -> Result { - if disable_direct_composition { - log::info!("Direct Composition is disabled."); - } - - let devices = DirectXRendererDevices::new(directx_devices, disable_direct_composition) - .context("Creating DirectX devices")?; - let atlas = Arc::new(DirectXAtlas::new(&devices.device, &devices.device_context)); - - let resources = DirectXResources::new(&devices, 1, 1, hwnd, disable_direct_composition) - .context("Creating DirectX resources")?; - let globals = DirectXGlobalElements::new(&devices.device) - .context("Creating DirectX global elements")?; - let pipelines = DirectXRenderPipelines::new(&devices.device) - .context("Creating DirectX render pipelines")?; - - let direct_composition = if disable_direct_composition { - None - } else { - let composition = DirectComposition::new(devices.dxgi_device.as_ref().unwrap(), hwnd) - .context("Creating DirectComposition")?; - composition - .set_swap_chain(&resources.swap_chain) - .context("Setting swap chain for DirectComposition")?; - Some(composition) - }; - - Ok(DirectXRenderer { - hwnd, - atlas, - devices: Some(devices), - resources: Some(resources), - globals, - pipelines, - direct_composition, - font_info: Self::get_font_info(), - width: 1, - height: 1, - skip_draws: false, - }) - } - - pub(crate) fn sprite_atlas(&self) -> Arc { - self.atlas.clone() - } - - fn pre_draw(&self, clear_color: &[f32; 4]) -> Result<()> { - let resources = self.resources.as_ref().expect("resources missing"); - let device_context = &self - .devices - .as_ref() - .expect("devices missing") - .device_context; - update_buffer( - device_context, - self.globals.global_params_buffer.as_ref().unwrap(), - &[GlobalParams { - gamma_ratios: self.font_info.gamma_ratios, - viewport_size: [resources.viewport.Width, resources.viewport.Height], - grayscale_enhanced_contrast: self.font_info.grayscale_enhanced_contrast, - subpixel_enhanced_contrast: self.font_info.subpixel_enhanced_contrast, - is_bgr: self.font_info.is_bgr as u32, - _pad: [0; 3], - }], - )?; - unsafe { - device_context.ClearRenderTargetView( - resources - .render_target_view - .as_ref() - .context("missing render target view")?, - clear_color, - ); - device_context - .OMSetRenderTargets(Some(slice::from_ref(&resources.render_target_view)), None); - device_context.RSSetViewports(Some(slice::from_ref(&resources.viewport))); - device_context - .VSSetConstantBuffers(0, Some(slice::from_ref(&self.globals.global_params_buffer))); - device_context - .VSSetConstantBuffers(1, Some(slice::from_ref(&self.globals.batch_params_buffer))); - device_context - .PSSetConstantBuffers(0, Some(slice::from_ref(&self.globals.global_params_buffer))); - } - Ok(()) - } - - #[inline] - fn present(&mut self) -> Result<()> { - let result = unsafe { - self.resources - .as_ref() - .expect("resources missing") - .swap_chain - .Present(0, DXGI_PRESENT(0)) - }; - result.ok().context("Presenting swap chain failed") - } - - pub(crate) fn handle_device_lost(&mut self, directx_devices: &DirectXDevices) -> Result<()> { - try_to_recover_from_device_lost(|| { - self.handle_device_lost_impl(directx_devices) - .context("DirectXRenderer handling device lost") - }) - } - - fn handle_device_lost_impl(&mut self, directx_devices: &DirectXDevices) -> Result<()> { - let disable_direct_composition = self.direct_composition.is_none(); - - unsafe { - #[cfg(debug_assertions)] - if let Some(devices) = &self.devices { - report_live_objects(&devices.device) - .context("Failed to report live objects after device lost") - .log_err(); - } - - self.resources.take(); - if let Some(devices) = &self.devices { - devices.device_context.OMSetRenderTargets(None, None); - devices.device_context.ClearState(); - devices.device_context.Flush(); - #[cfg(debug_assertions)] - report_live_objects(&devices.device) - .context("Failed to report live objects after device lost") - .log_err(); - } - - self.direct_composition.take(); - self.devices.take(); - } - - let devices = DirectXRendererDevices::new(directx_devices, disable_direct_composition) - .context("Recreating DirectX devices")?; - let resources = DirectXResources::new( - &devices, - self.width, - self.height, - self.hwnd, - disable_direct_composition, - ) - .context("Creating DirectX resources")?; - let globals = DirectXGlobalElements::new(&devices.device) - .context("Creating DirectXGlobalElements")?; - let pipelines = DirectXRenderPipelines::new(&devices.device) - .context("Creating DirectXRenderPipelines")?; - - let direct_composition = if disable_direct_composition { - None - } else { - let composition = - DirectComposition::new(devices.dxgi_device.as_ref().unwrap(), self.hwnd)?; - composition.set_swap_chain(&resources.swap_chain)?; - Some(composition) - }; - - self.atlas - .handle_device_lost(&devices.device, &devices.device_context); - - unsafe { - devices - .device_context - .OMSetRenderTargets(Some(slice::from_ref(&resources.render_target_view)), None); - } - self.devices = Some(devices); - self.resources = Some(resources); - self.globals = globals; - self.pipelines = pipelines; - self.direct_composition = direct_composition; - self.skip_draws = true; - Ok(()) - } - - pub(crate) fn draw( - &mut self, - scene: &Scene, - background_appearance: WindowBackgroundAppearance, - ) -> Result<()> { - if self.skip_draws { - // skip drawing this frame, we just recovered from a device lost event - // and so likely do not have the textures anymore that are required for drawing - return Ok(()); - } - self.render(scene, background_appearance)?; - self.present() - } - - /// Clear the render target for `background_appearance` and encode every - /// primitive batch of `scene` into it, without presenting. Shared by - /// [`draw`](Self::draw) (which then presents) and - /// [`render_to_image`](Self::render_to_image) (which reads the target back - /// instead), so the two cannot drift. - fn render( - &mut self, - scene: &Scene, - background_appearance: WindowBackgroundAppearance, - ) -> Result<()> { - self.pre_draw(&match background_appearance { - WindowBackgroundAppearance::Opaque => [1.0f32; 4], - _ => [0.0f32; 4], - })?; - self.upload_scene_buffers(scene)?; - - let annotation = self - .devices - .as_ref() - .and_then(|devices| devices.annotation.clone()) - .filter(|annotation| unsafe { annotation.GetStatus().as_bool() }); - for batch in scene.batches() { - let _annotation = annotation - .as_ref() - .map(|annotation| Annotation::new(annotation, HSTRING::from(batch.label()))); - match batch { - PrimitiveBatch::Shadows(range) => self.draw_shadows(range.start, range.len()), - PrimitiveBatch::Quads(range) => self.draw_quads(range.start, range.len()), - PrimitiveBatch::Paths(range) => { - let paths = &scene.paths[range]; - self.draw_paths_to_intermediate(paths)?; - self.draw_paths_from_intermediate(paths) - } - PrimitiveBatch::Underlines(range) => self.draw_underlines(range.start, range.len()), - PrimitiveBatch::MonochromeSprites { texture_id, range } => { - self.draw_monochrome_sprites(texture_id, range.start, range.len()) - } - PrimitiveBatch::SubpixelSprites { texture_id, range } => { - self.draw_subpixel_sprites(texture_id, range.start, range.len()) - } - PrimitiveBatch::PolychromeSprites { texture_id, range } => { - self.draw_polychrome_sprites(texture_id, range.start, range.len()) - } - PrimitiveBatch::Surfaces(range) => self.draw_surfaces(&scene.surfaces[range]), - } - .with_context(|| { - format!( - "scene too large:\ - {} paths, {} shadows, {} quads, {} underlines, {} mono, {} subpixel, {} poly, {} surfaces", - scene.paths.len(), - scene.shadows.len(), - scene.quads.len(), - scene.underlines.len(), - scene.monochrome_sprites.len(), - scene.subpixel_sprites.len(), - scene.polychrome_sprites.len(), - scene.surfaces.len(), - ) - })?; - } - Ok(()) - } - - /// Render `scene` to an offscreen CPU image **without presenting** so - /// the window need never be shown or visible (the macOS headless path - /// goes through MetalRenderer; this is the Windows analogue). Draws into - /// the existing render target, copies it into a `D3D11_USAGE_STAGING` - /// texture, maps it, and converts BGRA to RGBA. - #[cfg(any(test, feature = "test-support"))] - pub(crate) fn render_to_image( - &mut self, - scene: &Scene, - background_appearance: WindowBackgroundAppearance, - ) -> Result { - // A pending device-lost recovery (`skip_draws`) leaves the atlas holding - // tile references from the previous device; drawing before the forced - // re-render rebuilds them panics in `DirectXAtlasState::texture`. - anyhow::ensure!( - !self.skip_draws, - "render_to_image unavailable while recovering from a lost device" - ); - self.render(scene, background_appearance)?; - - let devices = self.devices.as_ref().context("devices missing")?; - let device = &devices.device; - let context = &devices.device_context; - let resources = self.resources.as_ref().context("resources missing")?; - let render_target = resources - .render_target - .as_ref() - .context("render target missing")?; - - // A CPU-readable copy of the render target. - let mut desc = D3D11_TEXTURE2D_DESC::default(); - unsafe { render_target.GetDesc(&mut desc) }; - let width = desc.Width; - let height = desc.Height; - let staging_desc = D3D11_TEXTURE2D_DESC { - Usage: D3D11_USAGE_STAGING, - BindFlags: 0, - CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32, - MiscFlags: 0, - MipLevels: 1, - ArraySize: 1, - SampleDesc: DXGI_SAMPLE_DESC { - Count: 1, - Quality: 0, - }, - ..desc - }; - let mut staging: Option = None; - unsafe { device.CreateTexture2D(&staging_desc, None, Some(&mut staging))? }; - let staging = staging.context("creating staging texture")?; - unsafe { context.CopyResource(&staging, render_target) }; - - let mut mapped = D3D11_MAPPED_SUBRESOURCE::default(); - unsafe { context.Map(&staging, 0, D3D11_MAP_READ, 0, Some(&mut mapped))? }; - let row_bytes = (width as usize) * 4; - let mut pixels = vec![0u8; row_bytes * height as usize]; - // SAFETY: `Map` succeeded, so `pData` points at `RowPitch * height` - // readable bytes for as long as the mapping is held, and `RowPitch >= - // row_bytes` (it only ever adds trailing padding). `pixels` is sized - // `row_bytes * height`, so every copy stays in bounds on both sides, - // and the regions cannot overlap (`pixels` is a fresh allocation). - unsafe { - let src = mapped.pData as *const u8; - for row in 0..height as usize { - let s = src.add(row * mapped.RowPitch as usize); - let d = pixels.as_mut_ptr().add(row * row_bytes); - std::ptr::copy_nonoverlapping(s, d, row_bytes); - } - context.Unmap(&staging, 0); - } - // The render target is BGRA; image::RgbaImage expects RGBA. - for px in pixels.chunks_exact_mut(4) { - px.swap(0, 2); - } - image::RgbaImage::from_raw(width, height, pixels) - .context("Failed to build RgbaImage from staging readback") - } - - pub(crate) fn resize(&mut self, new_size: Size) -> Result<()> { - let width = new_size.width.0.max(1) as u32; - let height = new_size.height.0.max(1) as u32; - if self.width == width && self.height == height { - return Ok(()); - } - self.width = width; - self.height = height; - - // Clear the render target before resizing - let devices = self.devices.as_ref().context("devices missing")?; - unsafe { devices.device_context.OMSetRenderTargets(None, None) }; - let resources = self.resources.as_mut().context("resources missing")?; - resources.render_target.take(); - resources.render_target_view.take(); - - // Resizing the swap chain requires a call to the underlying DXGI adapter, which can return the device removed error. - // The app might have moved to a monitor that's attached to a different graphics device. - // When a graphics device is removed or reset, the desktop resolution often changes, resulting in a window size change. - // But here we just return the error, because we are handling device lost scenarios elsewhere. - unsafe { - resources - .swap_chain - .ResizeBuffers( - BUFFER_COUNT as u32, - width, - height, - RENDER_TARGET_FORMAT, - DXGI_SWAP_CHAIN_FLAG(0), - ) - .context("Failed to resize swap chain")?; - } - - resources.recreate_resources(devices, width, height)?; - - unsafe { - devices - .device_context - .OMSetRenderTargets(Some(slice::from_ref(&resources.render_target_view)), None); - } - - Ok(()) - } - - fn upload_scene_buffers(&mut self, scene: &Scene) -> Result<()> { - let devices = self.devices.as_ref().context("devices missing")?; - let empty_clip = RoundedClip::::default(); - let clips = if scene.rounded_clips.is_empty() { - slice::from_ref(&empty_clip) - } else { - &scene.rounded_clips - }; - let element_size = std::mem::size_of::>(); - anyhow::ensure!( - std::mem::size_of_val(clips) <= MAX_INSTANCE_BUFFER_SIZE, - "rounded clip buffer exceeds renderer limit" - ); - if clips.len() > self.globals.clip_capacity { - let capacity = clips - .len() - .next_power_of_two() - .min(MAX_INSTANCE_BUFFER_SIZE / element_size); - let buffer = create_buffer(&devices.device, element_size, capacity)?; - self.globals.clip_view = create_buffer_view(&devices.device, &buffer)?; - self.globals.clip_buffer = buffer; - self.globals.clip_capacity = capacity; - } - update_buffer(&devices.device_context, &self.globals.clip_buffer, clips)?; - unsafe { - devices - .device_context - .PSSetShaderResources(2, Some(slice::from_ref(&self.globals.clip_view))); - } - - if !scene.shadows.is_empty() { - self.pipelines.shadow_pipeline.update_buffer( - &devices.device, - &devices.device_context, - &scene.shadows, - )?; - } - - if !scene.quads.is_empty() { - self.pipelines.quad_pipeline.update_buffer( - &devices.device, - &devices.device_context, - &scene.quads, - )?; - } - - if !scene.underlines.is_empty() { - self.pipelines.underline_pipeline.update_buffer( - &devices.device, - &devices.device_context, - &scene.underlines, - )?; - } - - if !scene.monochrome_sprites.is_empty() { - self.pipelines.mono_sprites.update_buffer( - &devices.device, - &devices.device_context, - &scene.monochrome_sprites, - )?; - } - - if !scene.subpixel_sprites.is_empty() { - self.pipelines.subpixel_sprites.update_buffer( - &devices.device, - &devices.device_context, - &scene.subpixel_sprites, - )?; - } - - if !scene.polychrome_sprites.is_empty() { - self.pipelines.poly_sprites.update_buffer( - &devices.device, - &devices.device_context, - &scene.polychrome_sprites, - )?; - } - - Ok(()) - } - - fn draw_shadows(&mut self, start: usize, len: usize) -> Result<()> { - if len == 0 { - return Ok(()); - } - let devices = self.devices.as_ref().context("devices missing")?; - self.pipelines.shadow_pipeline.draw_range( - &devices.device_context, - self.globals - .batch_params_buffer - .as_ref() - .context("batch params buffer missing")?, - start as u32, - len as u32, - ) - } - - fn draw_quads(&mut self, start: usize, len: usize) -> Result<()> { - if len == 0 { - return Ok(()); - } - let devices = self.devices.as_ref().context("devices missing")?; - self.pipelines.quad_pipeline.draw_range( - &devices.device_context, - self.globals - .batch_params_buffer - .as_ref() - .context("batch params buffer missing")?, - start as u32, - len as u32, - ) - } - - fn draw_paths_to_intermediate(&mut self, paths: &[Path]) -> Result<()> { - if paths.is_empty() { - return Ok(()); - } - - let devices = self.devices.as_ref().context("devices missing")?; - let resources = self.resources.as_ref().context("resources missing")?; - // Clear intermediate MSAA texture - unsafe { - devices.device_context.ClearRenderTargetView( - resources.path_intermediate_msaa_view.as_ref().unwrap(), - &[0.0; 4], - ); - // Set intermediate MSAA texture as render target - devices.device_context.OMSetRenderTargets( - Some(slice::from_ref(&resources.path_intermediate_msaa_view)), - None, - ); - } - - // Collect all vertices and sprites for a single draw call - let mut vertices = Vec::new(); - - for path in paths { - vertices.extend(path.vertices.iter().map(|v| PathRasterizationSprite { - xy_position: v.xy_position, - st_position: v.st_position, - color: path.color, - bounds: path.clipped_bounds(), - content_mask: path.content_mask, - })); - } - - self.pipelines.path_rasterization_pipeline.update_buffer( - &devices.device, - &devices.device_context, - &vertices, - )?; - - self.pipelines.path_rasterization_pipeline.draw( - &devices.device_context, - D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST, - vertices.len() as u32, - 1, - )?; - - // Resolve MSAA to non-MSAA intermediate texture - unsafe { - devices.device_context.ResolveSubresource( - &resources.path_intermediate_texture, - 0, - &resources.path_intermediate_msaa_texture, - 0, - RENDER_TARGET_FORMAT, - ); - // Restore main render target - devices - .device_context - .OMSetRenderTargets(Some(slice::from_ref(&resources.render_target_view)), None); - } - - Ok(()) - } - - fn draw_paths_from_intermediate(&mut self, paths: &[Path]) -> Result<()> { - let Some(first_path) = paths.first() else { - return Ok(()); - }; - - // When copying paths from the intermediate texture to the drawable, - // each pixel must only be copied once, in case of transparent paths. - // - // If all paths have the same draw order, then their bounds are all - // disjoint, so we can copy each path's bounds individually. If this - // batch combines different draw orders, we perform a single copy - // for a minimal spanning rect. - let sprites = if paths.last().unwrap().order == first_path.order { - paths - .iter() - .map(|path| PathSprite { - bounds: path.clipped_bounds(), - }) - .collect::>() - } else { - let mut bounds = first_path.clipped_bounds(); - for path in paths.iter().skip(1) { - bounds = bounds.union(&path.clipped_bounds()); - } - vec![PathSprite { bounds }] - }; - - let devices = self.devices.as_ref().context("devices missing")?; - let resources = self.resources.as_ref().context("resources missing")?; - self.pipelines.path_sprite_pipeline.update_buffer( - &devices.device, - &devices.device_context, - &sprites, - )?; - - // Draw the sprites with the path texture - self.pipelines.path_sprite_pipeline.draw_with_texture( - &devices.device_context, - slice::from_ref(&resources.path_intermediate_srv), - slice::from_ref(&self.globals.sampler), - sprites.len() as u32, - ) - } - - fn draw_underlines(&mut self, start: usize, len: usize) -> Result<()> { - if len == 0 { - return Ok(()); - } - let devices = self.devices.as_ref().context("devices missing")?; - self.pipelines.underline_pipeline.draw_range( - &devices.device_context, - self.globals - .batch_params_buffer - .as_ref() - .context("batch params buffer missing")?, - start as u32, - len as u32, - ) - } - - fn draw_monochrome_sprites( - &mut self, - texture_id: AtlasTextureId, - start: usize, - len: usize, - ) -> Result<()> { - if len == 0 { - return Ok(()); - } - let devices = self.devices.as_ref().context("devices missing")?; - let texture_view = self.atlas.get_texture_view(texture_id); - self.pipelines.mono_sprites.draw_range_with_texture( - &devices.device_context, - &texture_view, - self.globals - .batch_params_buffer - .as_ref() - .context("batch params buffer missing")?, - slice::from_ref(&self.globals.sampler), - start as u32, - len as u32, - ) - } - - fn draw_subpixel_sprites( - &mut self, - texture_id: AtlasTextureId, - start: usize, - len: usize, - ) -> Result<()> { - if len == 0 { - return Ok(()); - } - let devices = self.devices.as_ref().context("devices missing")?; - let texture_view = self.atlas.get_texture_view(texture_id); - self.pipelines.subpixel_sprites.draw_range_with_texture( - &devices.device_context, - &texture_view, - self.globals - .batch_params_buffer - .as_ref() - .context("batch params buffer missing")?, - slice::from_ref(&self.globals.sampler), - start as u32, - len as u32, - ) - } - - fn draw_polychrome_sprites( - &mut self, - texture_id: AtlasTextureId, - start: usize, - len: usize, - ) -> Result<()> { - if len == 0 { - return Ok(()); - } - let devices = self.devices.as_ref().context("devices missing")?; - let texture_view = self.atlas.get_texture_view(texture_id); - self.pipelines.poly_sprites.draw_range_with_texture( - &devices.device_context, - &texture_view, - self.globals - .batch_params_buffer - .as_ref() - .context("batch params buffer missing")?, - slice::from_ref(&self.globals.sampler), - start as u32, - len as u32, - ) - } - - fn draw_surfaces(&mut self, surfaces: &[PaintSurface]) -> Result<()> { - if surfaces.is_empty() { - return Ok(()); - } - Ok(()) - } - - pub(crate) fn gpu_specs(&self) -> Result { - let devices = self.devices.as_ref().context("devices missing")?; - let desc = unsafe { devices.adapter.GetDesc1() }?; - let is_software_emulated = (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE.0 as u32) != 0; - let device_name = String::from_utf16_lossy(&desc.Description) - .trim_matches(char::from(0)) - .to_string(); - let driver_name = match desc.VendorId { - 0x10DE => "NVIDIA Corporation".to_string(), - 0x1002 => "AMD Corporation".to_string(), - 0x8086 => "Intel Corporation".to_string(), - id => format!("Unknown Vendor (ID: {:#X})", id), - }; - let driver_version = match desc.VendorId { - 0x10DE => nvidia::get_driver_version(), - 0x1002 => amd::get_driver_version(), - // For Intel and other vendors, we use the DXGI API to get the driver version. - _ => dxgi::get_driver_version(&devices.adapter), - } - .context("Failed to get gpu driver info") - .log_err() - .unwrap_or("Unknown Driver".to_string()); - Ok(GpuSpecs { - is_software_emulated, - device_name, - driver_name, - driver_info: driver_version, - }) - } - - pub(crate) fn get_font_info() -> &'static FontInfo { - static CACHED_FONT_INFO: OnceLock = OnceLock::new(); - CACHED_FONT_INFO.get_or_init(|| unsafe { - let factory: IDWriteFactory5 = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED).unwrap(); - let render_params: IDWriteRenderingParams1 = - factory.CreateRenderingParams().unwrap().cast().unwrap(); - FontInfo { - gamma_ratios: gpui::get_gamma_correction_ratios(render_params.GetGamma()), - grayscale_enhanced_contrast: render_params.GetGrayscaleEnhancedContrast(), - subpixel_enhanced_contrast: render_params.GetEnhancedContrast(), - is_bgr: render_params.GetPixelGeometry() == DWRITE_PIXEL_GEOMETRY_BGR, - } - }) - } - - pub(crate) fn mark_drawable(&mut self) { - self.skip_draws = false; - } -} - -impl DirectXResources { - pub fn new( - devices: &DirectXRendererDevices, - width: u32, - height: u32, - hwnd: HWND, - disable_direct_composition: bool, - ) -> Result { - let swap_chain = if disable_direct_composition { - create_swap_chain(&devices.dxgi_factory, &devices.device, hwnd, width, height)? - } else { - create_swap_chain_for_composition( - &devices.dxgi_factory, - &devices.device, - width, - height, - )? - }; - - let ( - render_target, - render_target_view, - path_intermediate_texture, - path_intermediate_srv, - path_intermediate_msaa_texture, - path_intermediate_msaa_view, - viewport, - ) = create_resources(devices, &swap_chain, width, height)?; - set_rasterizer_state(&devices.device, &devices.device_context)?; - - Ok(Self { - swap_chain, - render_target: Some(render_target), - render_target_view, - path_intermediate_texture, - path_intermediate_msaa_texture, - path_intermediate_msaa_view, - path_intermediate_srv, - viewport, - }) - } - - #[inline] - fn recreate_resources( - &mut self, - devices: &DirectXRendererDevices, - width: u32, - height: u32, - ) -> Result<()> { - let ( - render_target, - render_target_view, - path_intermediate_texture, - path_intermediate_srv, - path_intermediate_msaa_texture, - path_intermediate_msaa_view, - viewport, - ) = create_resources(devices, &self.swap_chain, width, height)?; - self.render_target = Some(render_target); - self.render_target_view = render_target_view; - self.path_intermediate_texture = path_intermediate_texture; - self.path_intermediate_msaa_texture = path_intermediate_msaa_texture; - self.path_intermediate_msaa_view = path_intermediate_msaa_view; - self.path_intermediate_srv = path_intermediate_srv; - self.viewport = viewport; - Ok(()) - } -} - -impl DirectXRenderPipelines { - pub fn new(device: &ID3D11Device) -> Result { - let shadow_pipeline = PipelineState::new( - device, - "shadow_pipeline", - ShaderModule::Shadow, - 4, - create_blend_state(device)?, - )?; - let quad_pipeline = PipelineState::new( - device, - "quad_pipeline", - ShaderModule::Quad, - 64, - create_blend_state(device)?, - )?; - let path_rasterization_pipeline = PipelineState::new( - device, - "path_rasterization_pipeline", - ShaderModule::PathRasterization, - 32, - create_blend_state_for_path_rasterization(device)?, - )?; - let path_sprite_pipeline = PipelineState::new( - device, - "path_sprite_pipeline", - ShaderModule::PathSprite, - 4, - create_blend_state_for_path_sprite(device)?, - )?; - let underline_pipeline = PipelineState::new( - device, - "underline_pipeline", - ShaderModule::Underline, - 4, - create_blend_state(device)?, - )?; - let mono_sprites = PipelineState::new( - device, - "monochrome_sprite_pipeline", - ShaderModule::MonochromeSprite, - 512, - create_blend_state(device)?, - )?; - let subpixel_sprites = PipelineState::new( - device, - "subpixel_sprite_pipeline", - ShaderModule::SubpixelSprite, - 512, - create_blend_state_for_subpixel_rendering(device)?, - )?; - let poly_sprites = PipelineState::new( - device, - "polychrome_sprite_pipeline", - ShaderModule::PolychromeSprite, - 16, - create_blend_state(device)?, - )?; - - Ok(Self { - shadow_pipeline, - quad_pipeline, - path_rasterization_pipeline, - path_sprite_pipeline, - underline_pipeline, - mono_sprites, - subpixel_sprites, - poly_sprites, - }) - } -} - -impl DirectComposition { - pub fn new(dxgi_device: &IDXGIDevice, hwnd: HWND) -> Result { - let comp_device = get_comp_device(dxgi_device)?; - let comp_target = unsafe { comp_device.CreateTargetForHwnd(hwnd, true) }?; - let comp_visual = unsafe { comp_device.CreateVisual() }?; - - Ok(Self { - comp_device, - comp_target, - comp_visual, - }) - } - - pub fn set_swap_chain(&self, swap_chain: &IDXGISwapChain1) -> Result<()> { - unsafe { - self.comp_visual.SetContent(swap_chain)?; - self.comp_target.SetRoot(&self.comp_visual)?; - self.comp_device.Commit()?; - } - Ok(()) - } -} - -impl DirectXGlobalElements { - pub fn new(device: &ID3D11Device) -> Result { - let clip_buffer = - create_buffer(device, std::mem::size_of::>(), 1)?; - let clip_view = create_buffer_view(device, &clip_buffer)?; - let global_params_buffer = create_constant_buffer::(device)?; - let batch_params_buffer = create_constant_buffer::(device)?; - - let sampler = unsafe { - let desc = D3D11_SAMPLER_DESC { - Filter: D3D11_FILTER_MIN_MAG_MIP_LINEAR, - AddressU: D3D11_TEXTURE_ADDRESS_WRAP, - AddressV: D3D11_TEXTURE_ADDRESS_WRAP, - AddressW: D3D11_TEXTURE_ADDRESS_WRAP, - MipLODBias: 0.0, - MaxAnisotropy: 1, - ComparisonFunc: D3D11_COMPARISON_ALWAYS, - BorderColor: [0.0; 4], - MinLOD: 0.0, - MaxLOD: D3D11_FLOAT32_MAX, - }; - let mut output = None; - device.CreateSamplerState(&desc, Some(&mut output))?; - output - }; - - Ok(Self { - clip_buffer, - clip_view, - clip_capacity: 1, - global_params_buffer, - batch_params_buffer, - sampler, - }) - } -} - -#[derive(Debug, Default)] -#[repr(C)] -struct GlobalParams { - gamma_ratios: [f32; 4], - viewport_size: [f32; 2], - grayscale_enhanced_contrast: f32, - subpixel_enhanced_contrast: f32, - is_bgr: u32, - _pad: [u32; 3], -} - -#[derive(Clone, Copy, Debug, Default)] -#[repr(C, align(16))] -struct BatchParams { - start_index: u32, - _padding: [u32; 3], -} - -const _: () = assert!(std::mem::size_of::() == 16); - -struct PipelineState { - label: &'static str, - vertex: ID3D11VertexShader, - fragment: ID3D11PixelShader, - buffer: ID3D11Buffer, - buffer_size: usize, - view: Option, - blend_state: ID3D11BlendState, - _marker: std::marker::PhantomData, -} - -impl PipelineState { - fn new( - device: &ID3D11Device, - label: &'static str, - shader_module: ShaderModule, - buffer_size: usize, - blend_state: ID3D11BlendState, - ) -> Result { - let vertex = { - let raw_shader = RawShaderBytes::new(shader_module, ShaderTarget::Vertex)?; - create_vertex_shader(device, raw_shader.as_bytes())? - }; - let fragment = { - let raw_shader = RawShaderBytes::new(shader_module, ShaderTarget::Fragment)?; - create_fragment_shader(device, raw_shader.as_bytes())? - }; - let buffer = create_buffer(device, std::mem::size_of::(), buffer_size)?; - let view = create_buffer_view(device, &buffer)?; - - Ok(PipelineState { - label, - vertex, - fragment, - buffer, - buffer_size, - view, - blend_state, - _marker: std::marker::PhantomData, - }) - } - - fn update_buffer( - &mut self, - device: &ID3D11Device, - device_context: &ID3D11DeviceContext, - data: &[T], - ) -> Result<()> { - if self.buffer_size < data.len() { - let element_size = std::mem::size_of::(); - let required_size = std::mem::size_of_val(data); - anyhow::ensure!( - required_size <= MAX_INSTANCE_BUFFER_SIZE, - "{} buffer needs {required_size} bytes, above the maximum of {MAX_INSTANCE_BUFFER_SIZE}", - self.label - ); - let new_buffer_size = data - .len() - .next_power_of_two() - .min(MAX_INSTANCE_BUFFER_SIZE / element_size); - log::debug!( - "Updating {} buffer size from {} to {}", - self.label, - self.buffer_size, - new_buffer_size - ); - let buffer = create_buffer(device, std::mem::size_of::(), new_buffer_size)?; - let view = create_buffer_view(device, &buffer)?; - self.buffer = buffer; - self.view = view; - self.buffer_size = new_buffer_size; - } - update_buffer(device_context, &self.buffer, data) - } - - fn draw( - &self, - device_context: &ID3D11DeviceContext, - topology: D3D_PRIMITIVE_TOPOLOGY, - vertex_count: u32, - instance_count: u32, - ) -> Result<()> { - set_pipeline_state( - device_context, - slice::from_ref(&self.view), - topology, - &self.vertex, - &self.fragment, - &self.blend_state, - ); - unsafe { - device_context.DrawInstanced(vertex_count, instance_count, 0, 0); - } - Ok(()) - } - - fn draw_with_texture( - &self, - device_context: &ID3D11DeviceContext, - texture: &[Option], - sampler: &[Option], - instance_count: u32, - ) -> Result<()> { - set_pipeline_state( - device_context, - slice::from_ref(&self.view), - D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, - &self.vertex, - &self.fragment, - &self.blend_state, - ); - unsafe { - device_context.PSSetSamplers(0, Some(sampler)); - device_context.VSSetShaderResources(0, Some(texture)); - device_context.PSSetShaderResources(0, Some(texture)); - - device_context.DrawInstanced(4, instance_count, 0, 0); - } - Ok(()) - } - - fn draw_range( - &self, - device_context: &ID3D11DeviceContext, - batch_params_buffer: &ID3D11Buffer, - first_instance: u32, - instance_count: u32, - ) -> Result<()> { - anyhow::ensure!( - first_instance as usize + instance_count as usize <= self.buffer_size, - "DirectX instance range exceeds the {} buffer", - self.label - ); - update_batch_start(device_context, batch_params_buffer, first_instance)?; - set_pipeline_state( - device_context, - slice::from_ref(&self.view), - D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, - &self.vertex, - &self.fragment, - &self.blend_state, - ); - unsafe { - device_context.DrawInstanced(4, instance_count, 0, 0); - } - Ok(()) - } - - fn draw_range_with_texture( - &self, - device_context: &ID3D11DeviceContext, - texture: &[Option], - batch_params_buffer: &ID3D11Buffer, - sampler: &[Option], - first_instance: u32, - instance_count: u32, - ) -> Result<()> { - anyhow::ensure!( - first_instance as usize + instance_count as usize <= self.buffer_size, - "DirectX instance range exceeds the {} buffer", - self.label - ); - update_batch_start(device_context, batch_params_buffer, first_instance)?; - set_pipeline_state( - device_context, - slice::from_ref(&self.view), - D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, - &self.vertex, - &self.fragment, - &self.blend_state, - ); - unsafe { - device_context.PSSetSamplers(0, Some(sampler)); - device_context.VSSetShaderResources(0, Some(texture)); - device_context.PSSetShaderResources(0, Some(texture)); - device_context.DrawInstanced(4, instance_count, 0, 0); - } - Ok(()) - } -} - -#[derive(Clone, Copy)] -#[repr(C)] -struct PathRasterizationSprite { - xy_position: Point, - st_position: Point, - color: Background, - bounds: Bounds, - content_mask: ContentMask, -} - -#[derive(Clone, Copy)] -#[repr(C)] -struct PathSprite { - bounds: Bounds, -} - -impl Drop for DirectXRenderer { - fn drop(&mut self) { - #[cfg(debug_assertions)] - if let Some(devices) = &self.devices { - report_live_objects(&devices.device).ok(); - } - } -} - -#[inline] -fn get_comp_device(dxgi_device: &IDXGIDevice) -> Result { - Ok(unsafe { DCompositionCreateDevice(dxgi_device)? }) -} - -fn create_swap_chain_for_composition( - dxgi_factory: &IDXGIFactory6, - device: &ID3D11Device, - width: u32, - height: u32, -) -> Result { - let desc = DXGI_SWAP_CHAIN_DESC1 { - Width: width, - Height: height, - Format: RENDER_TARGET_FORMAT, - Stereo: false.into(), - SampleDesc: DXGI_SAMPLE_DESC { - Count: 1, - Quality: 0, - }, - BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT, - BufferCount: BUFFER_COUNT as u32, - // Composition SwapChains only support the DXGI_SCALING_STRETCH Scaling. - Scaling: DXGI_SCALING_STRETCH, - SwapEffect: DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL, - AlphaMode: DXGI_ALPHA_MODE_PREMULTIPLIED, - Flags: 0, - }; - Ok(unsafe { dxgi_factory.CreateSwapChainForComposition(device, &desc, None)? }) -} - -fn create_swap_chain( - dxgi_factory: &IDXGIFactory6, - device: &ID3D11Device, - hwnd: HWND, - width: u32, - height: u32, -) -> Result { - use windows::Win32::Graphics::Dxgi::DXGI_MWA_NO_ALT_ENTER; - - let desc = DXGI_SWAP_CHAIN_DESC1 { - Width: width, - Height: height, - Format: RENDER_TARGET_FORMAT, - Stereo: false.into(), - SampleDesc: DXGI_SAMPLE_DESC { - Count: 1, - Quality: 0, - }, - BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT, - BufferCount: BUFFER_COUNT as u32, - Scaling: DXGI_SCALING_NONE, - SwapEffect: DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL, - AlphaMode: DXGI_ALPHA_MODE_IGNORE, - Flags: 0, - }; - let swap_chain = - unsafe { dxgi_factory.CreateSwapChainForHwnd(device, hwnd, &desc, None, None) }?; - unsafe { dxgi_factory.MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER) }?; - Ok(swap_chain) -} - -#[inline] -fn create_resources( - devices: &DirectXRendererDevices, - swap_chain: &IDXGISwapChain1, - width: u32, - height: u32, -) -> Result<( - ID3D11Texture2D, - Option, - ID3D11Texture2D, - Option, - ID3D11Texture2D, - Option, - D3D11_VIEWPORT, -)> { - let (render_target, render_target_view) = - create_render_target_and_its_view(swap_chain, &devices.device)?; - let (path_intermediate_texture, path_intermediate_srv) = - create_path_intermediate_texture(&devices.device, width, height)?; - let (path_intermediate_msaa_texture, path_intermediate_msaa_view) = - create_path_intermediate_msaa_texture_and_view(&devices.device, width, height)?; - let viewport = D3D11_VIEWPORT { - TopLeftX: 0.0, - TopLeftY: 0.0, - Width: width as f32, - Height: height as f32, - MinDepth: 0.0, - MaxDepth: 1.0, - }; - Ok(( - render_target, - render_target_view, - path_intermediate_texture, - path_intermediate_srv, - path_intermediate_msaa_texture, - path_intermediate_msaa_view, - viewport, - )) -} - -#[inline] -fn create_render_target_and_its_view( - swap_chain: &IDXGISwapChain1, - device: &ID3D11Device, -) -> Result<(ID3D11Texture2D, Option)> { - let render_target: ID3D11Texture2D = unsafe { swap_chain.GetBuffer(0) }?; - let mut render_target_view = None; - unsafe { device.CreateRenderTargetView(&render_target, None, Some(&mut render_target_view))? }; - Ok((render_target, render_target_view)) -} - -#[inline] -fn create_path_intermediate_texture( - device: &ID3D11Device, - width: u32, - height: u32, -) -> Result<(ID3D11Texture2D, Option)> { - let texture = unsafe { - let mut output = None; - let desc = D3D11_TEXTURE2D_DESC { - Width: width, - Height: height, - MipLevels: 1, - ArraySize: 1, - Format: RENDER_TARGET_FORMAT, - SampleDesc: DXGI_SAMPLE_DESC { - Count: 1, - Quality: 0, - }, - Usage: D3D11_USAGE_DEFAULT, - BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32, - CPUAccessFlags: 0, - MiscFlags: 0, - }; - device.CreateTexture2D(&desc, None, Some(&mut output))?; - output.unwrap() - }; - - let mut shader_resource_view = None; - unsafe { device.CreateShaderResourceView(&texture, None, Some(&mut shader_resource_view))? }; - - Ok((texture, Some(shader_resource_view.unwrap()))) -} - -#[inline] -fn create_path_intermediate_msaa_texture_and_view( - device: &ID3D11Device, - width: u32, - height: u32, -) -> Result<(ID3D11Texture2D, Option)> { - let msaa_texture = unsafe { - let mut output = None; - let desc = D3D11_TEXTURE2D_DESC { - Width: width, - Height: height, - MipLevels: 1, - ArraySize: 1, - Format: RENDER_TARGET_FORMAT, - SampleDesc: DXGI_SAMPLE_DESC { - Count: PATH_MULTISAMPLE_COUNT, - Quality: D3D11_STANDARD_MULTISAMPLE_PATTERN.0 as u32, - }, - Usage: D3D11_USAGE_DEFAULT, - BindFlags: D3D11_BIND_RENDER_TARGET.0 as u32, - CPUAccessFlags: 0, - MiscFlags: 0, - }; - device.CreateTexture2D(&desc, None, Some(&mut output))?; - output.unwrap() - }; - let mut msaa_view = None; - unsafe { device.CreateRenderTargetView(&msaa_texture, None, Some(&mut msaa_view))? }; - Ok((msaa_texture, Some(msaa_view.unwrap()))) -} - -#[inline] -fn set_rasterizer_state(device: &ID3D11Device, device_context: &ID3D11DeviceContext) -> Result<()> { - let desc = D3D11_RASTERIZER_DESC { - FillMode: D3D11_FILL_SOLID, - CullMode: D3D11_CULL_NONE, - FrontCounterClockwise: false.into(), - DepthBias: 0, - DepthBiasClamp: 0.0, - SlopeScaledDepthBias: 0.0, - DepthClipEnable: true.into(), - ScissorEnable: false.into(), - MultisampleEnable: true.into(), - AntialiasedLineEnable: false.into(), - }; - let rasterizer_state = unsafe { - let mut state = None; - device.CreateRasterizerState(&desc, Some(&mut state))?; - state.unwrap() - }; - unsafe { device_context.RSSetState(&rasterizer_state) }; - Ok(()) -} - -// https://learn.microsoft.com/en-us/windows/win32/api/d3d11/ns-d3d11-d3d11_blend_desc -#[inline] -fn create_blend_state(device: &ID3D11Device) -> Result { - let mut desc = D3D11_BLEND_DESC::default(); - desc.RenderTarget[0].BlendEnable = true.into(); - desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD; - desc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; - desc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA; - desc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE; - desc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA; - desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ONE; - desc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL.0 as u8; - unsafe { - let mut state = None; - device.CreateBlendState(&desc, Some(&mut state))?; - Ok(state.unwrap()) - } -} - -#[inline] -fn create_blend_state_for_subpixel_rendering(device: &ID3D11Device) -> Result { - let mut desc = D3D11_BLEND_DESC::default(); - desc.RenderTarget[0].BlendEnable = true.into(); - desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD; - desc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; - desc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC1_COLOR; - desc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC1_COLOR; - // It does not make sense to draw transparent subpixel-rendered text, since it cannot be meaningfully alpha-blended onto anything else. - desc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE; - desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO; - desc.RenderTarget[0].RenderTargetWriteMask = - D3D11_COLOR_WRITE_ENABLE_ALL.0 as u8 & !D3D11_COLOR_WRITE_ENABLE_ALPHA.0 as u8; - - unsafe { - let mut state = None; - device.CreateBlendState(&desc, Some(&mut state))?; - Ok(state.unwrap()) - } -} - -#[inline] -fn create_blend_state_for_path_rasterization(device: &ID3D11Device) -> Result { - // If the feature level is set to greater than D3D_FEATURE_LEVEL_9_3, the display - // device performs the blend in linear space, which is ideal. - let mut desc = D3D11_BLEND_DESC::default(); - desc.RenderTarget[0].BlendEnable = true.into(); - desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD; - desc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; - desc.RenderTarget[0].SrcBlend = D3D11_BLEND_ONE; - desc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE; - desc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA; - desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA; - desc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL.0 as u8; - unsafe { - let mut state = None; - device.CreateBlendState(&desc, Some(&mut state))?; - Ok(state.unwrap()) - } -} - -#[inline] -fn create_blend_state_for_path_sprite(device: &ID3D11Device) -> Result { - // If the feature level is set to greater than D3D_FEATURE_LEVEL_9_3, the display - // device performs the blend in linear space, which is ideal. - let mut desc = D3D11_BLEND_DESC::default(); - desc.RenderTarget[0].BlendEnable = true.into(); - desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD; - desc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; - desc.RenderTarget[0].SrcBlend = D3D11_BLEND_ONE; - desc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE; - desc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA; - desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ONE; - desc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL.0 as u8; - unsafe { - let mut state = None; - device.CreateBlendState(&desc, Some(&mut state))?; - Ok(state.unwrap()) - } -} - -#[inline] -fn create_vertex_shader(device: &ID3D11Device, bytes: &[u8]) -> Result { - unsafe { - let mut shader = None; - device.CreateVertexShader(bytes, None, Some(&mut shader))?; - Ok(shader.unwrap()) - } -} - -#[inline] -fn create_fragment_shader(device: &ID3D11Device, bytes: &[u8]) -> Result { - unsafe { - let mut shader = None; - device.CreatePixelShader(bytes, None, Some(&mut shader))?; - Ok(shader.unwrap()) - } -} - -#[inline] -fn create_constant_buffer(device: &ID3D11Device) -> Result> { - const { assert!(std::mem::size_of::() != 0 && std::mem::size_of::().is_multiple_of(16)) }; - let desc = D3D11_BUFFER_DESC { - ByteWidth: std::mem::size_of::() as u32, - Usage: D3D11_USAGE_DYNAMIC, - BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32, - CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, - MiscFlags: 0, - StructureByteStride: 0, - }; - let mut buffer = None; - unsafe { device.CreateBuffer(&desc, None, Some(&mut buffer)) }?; - Ok(buffer) -} - -#[inline] -fn create_buffer( - device: &ID3D11Device, - element_size: usize, - buffer_size: usize, -) -> Result { - let desc = D3D11_BUFFER_DESC { - ByteWidth: (element_size * buffer_size) as u32, - Usage: D3D11_USAGE_DYNAMIC, - BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32, - CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, - MiscFlags: D3D11_RESOURCE_MISC_BUFFER_STRUCTURED.0 as u32, - StructureByteStride: element_size as u32, - }; - let mut buffer = None; - unsafe { device.CreateBuffer(&desc, None, Some(&mut buffer)) }?; - Ok(buffer.unwrap()) -} - -#[inline] -fn create_buffer_view( - device: &ID3D11Device, - buffer: &ID3D11Buffer, -) -> Result> { - let mut view = None; - unsafe { device.CreateShaderResourceView(buffer, None, Some(&mut view)) }?; - Ok(view) -} - -#[inline] -fn update_buffer( - device_context: &ID3D11DeviceContext, - buffer: &ID3D11Buffer, - data: &[T], -) -> Result<()> { - unsafe { - let mut dest = std::mem::zeroed(); - device_context.Map(buffer, 0, D3D11_MAP_WRITE_DISCARD, 0, Some(&mut dest))?; - std::ptr::copy_nonoverlapping(data.as_ptr(), dest.pData as _, data.len()); - device_context.Unmap(buffer, 0); - } - Ok(()) -} - -#[inline] -fn update_batch_start( - device_context: &ID3D11DeviceContext, - buffer: &ID3D11Buffer, - first_instance: u32, -) -> Result<()> { - update_buffer( - device_context, - buffer, - &[BatchParams { - start_index: first_instance, - _padding: [0; 3], - }], - ) -} - -#[inline] -fn set_pipeline_state( - device_context: &ID3D11DeviceContext, - buffer_view: &[Option], - topology: D3D_PRIMITIVE_TOPOLOGY, - vertex_shader: &ID3D11VertexShader, - fragment_shader: &ID3D11PixelShader, - blend_state: &ID3D11BlendState, -) { - unsafe { - device_context.VSSetShaderResources(1, Some(buffer_view)); - device_context.PSSetShaderResources(1, Some(buffer_view)); - device_context.IASetPrimitiveTopology(topology); - device_context.VSSetShader(vertex_shader, None); - device_context.PSSetShader(fragment_shader, None); - device_context.OMSetBlendState(blend_state, None, 0xFFFFFFFF); - } -} - -#[cfg(debug_assertions)] -fn report_live_objects(device: &ID3D11Device) -> Result<()> { - let debug_device: ID3D11Debug = device.cast()?; - unsafe { - debug_device.ReportLiveDeviceObjects(D3D11_RLDO_DETAIL)?; - } - Ok(()) -} - -const BUFFER_COUNT: usize = 3; - -pub(crate) mod shader_resources { - use anyhow::Result; - - #[cfg(debug_assertions)] - use windows::{ - core::{HSTRING, PCSTR}, - Win32::Graphics::Direct3D::{ - Fxc::{D3DCompileFromFile, D3DCOMPILE_DEBUG, D3DCOMPILE_SKIP_OPTIMIZATION}, - ID3DBlob, - }, - }; - - #[derive(Copy, Clone, Debug, Eq, PartialEq)] - pub(crate) enum ShaderModule { - Quad, - Shadow, - Underline, - PathRasterization, - PathSprite, - MonochromeSprite, - SubpixelSprite, - PolychromeSprite, - EmojiRasterization, - } - - #[derive(Copy, Clone, Debug, Eq, PartialEq)] - pub(crate) enum ShaderTarget { - Vertex, - Fragment, - } - - pub(crate) struct RawShaderBytes<'t> { - inner: &'t [u8], - - #[cfg(debug_assertions)] - _blob: ID3DBlob, - } - - impl<'t> RawShaderBytes<'t> { - pub(crate) fn new(module: ShaderModule, target: ShaderTarget) -> Result { - #[cfg(not(debug_assertions))] - { - Ok(Self::from_bytes(module, target)) - } - #[cfg(debug_assertions)] - { - let blob = build_shader_blob(module, target)?; - let inner = unsafe { - std::slice::from_raw_parts( - blob.GetBufferPointer() as *const u8, - blob.GetBufferSize(), - ) - }; - Ok(Self { inner, _blob: blob }) - } - } - - pub(crate) fn as_bytes(&'t self) -> &'t [u8] { - self.inner - } - - #[cfg(not(debug_assertions))] - fn from_bytes(module: ShaderModule, target: ShaderTarget) -> Self { - let bytes = match module { - ShaderModule::Quad => match target { - ShaderTarget::Vertex => QUAD_VERTEX_BYTES, - ShaderTarget::Fragment => QUAD_FRAGMENT_BYTES, - }, - ShaderModule::Shadow => match target { - ShaderTarget::Vertex => SHADOW_VERTEX_BYTES, - ShaderTarget::Fragment => SHADOW_FRAGMENT_BYTES, - }, - ShaderModule::Underline => match target { - ShaderTarget::Vertex => UNDERLINE_VERTEX_BYTES, - ShaderTarget::Fragment => UNDERLINE_FRAGMENT_BYTES, - }, - ShaderModule::PathRasterization => match target { - ShaderTarget::Vertex => PATH_RASTERIZATION_VERTEX_BYTES, - ShaderTarget::Fragment => PATH_RASTERIZATION_FRAGMENT_BYTES, - }, - ShaderModule::PathSprite => match target { - ShaderTarget::Vertex => PATH_SPRITE_VERTEX_BYTES, - ShaderTarget::Fragment => PATH_SPRITE_FRAGMENT_BYTES, - }, - ShaderModule::MonochromeSprite => match target { - ShaderTarget::Vertex => MONOCHROME_SPRITE_VERTEX_BYTES, - ShaderTarget::Fragment => MONOCHROME_SPRITE_FRAGMENT_BYTES, - }, - ShaderModule::SubpixelSprite => match target { - ShaderTarget::Vertex => SUBPIXEL_SPRITE_VERTEX_BYTES, - ShaderTarget::Fragment => SUBPIXEL_SPRITE_FRAGMENT_BYTES, - }, - ShaderModule::PolychromeSprite => match target { - ShaderTarget::Vertex => POLYCHROME_SPRITE_VERTEX_BYTES, - ShaderTarget::Fragment => POLYCHROME_SPRITE_FRAGMENT_BYTES, - }, - ShaderModule::EmojiRasterization => match target { - ShaderTarget::Vertex => EMOJI_RASTERIZATION_VERTEX_BYTES, - ShaderTarget::Fragment => EMOJI_RASTERIZATION_FRAGMENT_BYTES, - }, - }; - Self { inner: bytes } - } - } - - #[cfg(debug_assertions)] - pub(super) fn build_shader_blob(entry: ShaderModule, target: ShaderTarget) -> Result { - unsafe { - use windows::Win32::Graphics::{ - Direct3D::ID3DInclude, Hlsl::D3D_COMPILE_STANDARD_FILE_INCLUDE, - }; - - let shader_name = if matches!(entry, ShaderModule::EmojiRasterization) { - "color_text_raster.hlsl" - } else { - "shaders.hlsl" - }; - - let entry = format!( - "{}_{}\0", - entry.as_str(), - match target { - ShaderTarget::Vertex => "vertex", - ShaderTarget::Fragment => "fragment", - } - ); - let target = match target { - ShaderTarget::Vertex => "vs_4_1\0", - ShaderTarget::Fragment => "ps_4_1\0", - }; - - let mut compile_blob = None; - let mut error_blob = None; - let shader_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join(&format!("src/{}", shader_name)) - .canonicalize()?; - - let entry_point = PCSTR::from_raw(entry.as_ptr()); - let target_cstr = PCSTR::from_raw(target.as_ptr()); - - // really dirty trick because winapi bindings are unhappy otherwise - let include_handler = &std::mem::transmute::( - D3D_COMPILE_STANDARD_FILE_INCLUDE as usize, - ); - - let ret = D3DCompileFromFile( - &HSTRING::from(shader_path.to_str().unwrap()), - None, - include_handler, - entry_point, - target_cstr, - D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION, - 0, - &mut compile_blob, - Some(&mut error_blob), - ); - if ret.is_err() { - let Some(error_blob) = error_blob else { - return Err(anyhow::anyhow!("{ret:?}")); - }; - - let error_string = - std::ffi::CStr::from_ptr(error_blob.GetBufferPointer() as *const i8) - .to_string_lossy(); - log::error!("Shader compile error: {}", error_string); - return Err(anyhow::anyhow!("Compile error: {}", error_string)); - } - Ok(compile_blob.unwrap()) - } - } - - #[cfg(not(debug_assertions))] - include!(concat!(env!("OUT_DIR"), "/shaders_bytes.rs")); - - #[cfg(debug_assertions)] - impl ShaderModule { - pub fn as_str(self) -> &'static str { - match self { - ShaderModule::Quad => "quad", - ShaderModule::Shadow => "shadow", - ShaderModule::Underline => "underline", - ShaderModule::PathRasterization => "path_rasterization", - ShaderModule::PathSprite => "path_sprite", - ShaderModule::MonochromeSprite => "monochrome_sprite", - ShaderModule::SubpixelSprite => "subpixel_sprite", - ShaderModule::PolychromeSprite => "polychrome_sprite", - ShaderModule::EmojiRasterization => "emoji_rasterization", - } - } - } -} - -mod nvidia { - use std::{ - ffi::CStr, - os::raw::{c_char, c_int, c_uint}, - }; - - use anyhow::Result; - use windows::{core::s, Win32::System::LibraryLoader::GetProcAddress}; - - use crate::with_dll_library; - - // https://github.com/NVIDIA/nvapi/blob/7cb76fce2f52de818b3da497af646af1ec16ce27/nvapi_lite_common.h#L180 - const NVAPI_SHORT_STRING_MAX: usize = 64; - - // https://github.com/NVIDIA/nvapi/blob/7cb76fce2f52de818b3da497af646af1ec16ce27/nvapi_lite_common.h#L235 - #[allow(non_camel_case_types)] - type NvAPI_ShortString = [c_char; NVAPI_SHORT_STRING_MAX]; - - // https://github.com/NVIDIA/nvapi/blob/7cb76fce2f52de818b3da497af646af1ec16ce27/nvapi_lite_common.h#L447 - #[allow(non_camel_case_types)] - type NvAPI_SYS_GetDriverAndBranchVersion_t = unsafe extern "C" fn( - driver_version: *mut c_uint, - build_branch_string: *mut NvAPI_ShortString, - ) -> c_int; - - pub(super) fn get_driver_version() -> Result { - #[cfg(target_pointer_width = "64")] - let nvidia_dll_name = s!("nvapi64.dll"); - #[cfg(target_pointer_width = "32")] - let nvidia_dll_name = s!("nvapi.dll"); - - with_dll_library(nvidia_dll_name, |nvidia_dll| unsafe { - let nvapi_query_addr = GetProcAddress(nvidia_dll, s!("nvapi_QueryInterface")) - .ok_or_else(|| anyhow::anyhow!("Failed to get nvapi_QueryInterface address"))?; - let nvapi_query: extern "C" fn(u32) -> *mut () = std::mem::transmute(nvapi_query_addr); - - // https://github.com/NVIDIA/nvapi/blob/7cb76fce2f52de818b3da497af646af1ec16ce27/nvapi_interface.h#L41 - let nvapi_get_driver_version_ptr = nvapi_query(0x2926aaad); - if nvapi_get_driver_version_ptr.is_null() { - anyhow::bail!("Failed to get NVIDIA driver version function pointer"); - } - let nvapi_get_driver_version: NvAPI_SYS_GetDriverAndBranchVersion_t = - std::mem::transmute(nvapi_get_driver_version_ptr); - - let mut driver_version: c_uint = 0; - let mut build_branch_string: NvAPI_ShortString = [0; NVAPI_SHORT_STRING_MAX]; - let result = nvapi_get_driver_version( - &mut driver_version as *mut c_uint, - &mut build_branch_string as *mut NvAPI_ShortString, - ); - - if result != 0 { - anyhow::bail!( - "Failed to get NVIDIA driver version, error code: {}", - result - ); - } - let major = driver_version / 100; - let minor = driver_version % 100; - let branch_string = CStr::from_ptr(build_branch_string.as_ptr()); - Ok(format!( - "{}.{} {}", - major, - minor, - branch_string.to_string_lossy() - )) - }) - } -} - -mod amd { - use std::os::raw::{c_char, c_int, c_void}; - - use anyhow::Result; - use windows::{core::s, Win32::System::LibraryLoader::GetProcAddress}; - - use crate::with_dll_library; - - // https://github.com/GPUOpen-LibrariesAndSDKs/AGS_SDK/blob/5d8812d703d0335741b6f7ffc37838eeb8b967f7/ags_lib/inc/amd_ags.h#L145 - const AGS_CURRENT_VERSION: i32 = (6 << 22) | (3 << 12); - - // https://github.com/GPUOpen-LibrariesAndSDKs/AGS_SDK/blob/5d8812d703d0335741b6f7ffc37838eeb8b967f7/ags_lib/inc/amd_ags.h#L204 - // This is an opaque type, using struct to represent it properly for FFI - #[repr(C)] - struct AGSContext { - _private: [u8; 0], - } - - #[repr(C)] - pub struct AGSGPUInfo { - pub driver_version: *const c_char, - pub radeon_software_version: *const c_char, - pub num_devices: c_int, - pub devices: *mut c_void, - } - - // https://github.com/GPUOpen-LibrariesAndSDKs/AGS_SDK/blob/5d8812d703d0335741b6f7ffc37838eeb8b967f7/ags_lib/inc/amd_ags.h#L429 - #[allow(non_camel_case_types)] - type agsInitialize_t = unsafe extern "C" fn( - version: c_int, - config: *const c_void, - context: *mut *mut AGSContext, - gpu_info: *mut AGSGPUInfo, - ) -> c_int; - - // https://github.com/GPUOpen-LibrariesAndSDKs/AGS_SDK/blob/5d8812d703d0335741b6f7ffc37838eeb8b967f7/ags_lib/inc/amd_ags.h#L436 - #[allow(non_camel_case_types)] - type agsDeInitialize_t = unsafe extern "C" fn(context: *mut AGSContext) -> c_int; - - pub(super) fn get_driver_version() -> Result { - #[cfg(target_pointer_width = "64")] - let amd_dll_name = s!("amd_ags_x64.dll"); - #[cfg(target_pointer_width = "32")] - let amd_dll_name = s!("amd_ags_x86.dll"); - - with_dll_library(amd_dll_name, |amd_dll| unsafe { - let ags_initialize_addr = GetProcAddress(amd_dll, s!("agsInitialize")) - .ok_or_else(|| anyhow::anyhow!("Failed to get agsInitialize address"))?; - let ags_deinitialize_addr = GetProcAddress(amd_dll, s!("agsDeInitialize")) - .ok_or_else(|| anyhow::anyhow!("Failed to get agsDeInitialize address"))?; - - let ags_initialize: agsInitialize_t = std::mem::transmute(ags_initialize_addr); - let ags_deinitialize: agsDeInitialize_t = std::mem::transmute(ags_deinitialize_addr); - - let mut context: *mut AGSContext = std::ptr::null_mut(); - let mut gpu_info: AGSGPUInfo = AGSGPUInfo { - driver_version: std::ptr::null(), - radeon_software_version: std::ptr::null(), - num_devices: 0, - devices: std::ptr::null_mut(), - }; - - let result = ags_initialize( - AGS_CURRENT_VERSION, - std::ptr::null(), - &mut context, - &mut gpu_info, - ); - if result != 0 { - anyhow::bail!("Failed to initialize AMD AGS, error code: {}", result); - } - - // Vulkan actually returns this as the driver version - let software_version = if !gpu_info.radeon_software_version.is_null() { - std::ffi::CStr::from_ptr(gpu_info.radeon_software_version) - .to_string_lossy() - .into_owned() - } else { - "Unknown Radeon Software Version".to_string() - }; - - let driver_version = if !gpu_info.driver_version.is_null() { - std::ffi::CStr::from_ptr(gpu_info.driver_version) - .to_string_lossy() - .into_owned() - } else { - "Unknown Radeon Driver Version".to_string() - }; - - ags_deinitialize(context); - Ok(format!("{} ({})", software_version, driver_version)) - }) - } -} - -mod dxgi { - use windows::{ - core::Interface, - Win32::Graphics::Dxgi::{IDXGIAdapter1, IDXGIDevice}, - }; - - pub(super) fn get_driver_version(adapter: &IDXGIAdapter1) -> anyhow::Result { - let number = unsafe { adapter.CheckInterfaceSupport(&IDXGIDevice::IID as _) }?; - Ok(format!( - "{}.{}.{}.{}", - number >> 48, - (number >> 32) & 0xFFFF, - (number >> 16) & 0xFFFF, - number & 0xFFFF - )) - } -} diff --git a/crates/gpui_pre_windows/src/dispatcher.rs b/crates/gpui_pre_windows/src/dispatcher.rs deleted file mode 100644 index 55f18b5..0000000 --- a/crates/gpui_pre_windows/src/dispatcher.rs +++ /dev/null @@ -1,191 +0,0 @@ -use std::{ - ffi::c_void, - ptr::NonNull, - sync::atomic::{AtomicBool, Ordering}, - thread::{ThreadId, current}, - time::Duration, -}; - -use anyhow::Context; -use gpui_util::ResultExt; -use windows::Win32::{ - Foundation::{FILETIME, LPARAM, WPARAM}, - Media::{timeBeginPeriod, timeEndPeriod}, - System::Threading::{ - CloseThreadpoolTimer, CreateThreadpoolTimer, GetCurrentThread, PTP_CALLBACK_INSTANCE, - PTP_TIMER, SetThreadPriority, SetThreadpoolTimer, THREAD_PRIORITY_TIME_CRITICAL, - TP_CALLBACK_ENVIRON_V3, TP_CALLBACK_PRIORITY, TP_CALLBACK_PRIORITY_HIGH, - TP_CALLBACK_PRIORITY_LOW, TP_CALLBACK_PRIORITY_NORMAL, TrySubmitThreadpoolCallback, - }, - UI::WindowsAndMessaging::PostMessageW, -}; - -use crate::{HWND, SafeHwnd, WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD}; -use gpui::{ - PlatformDispatcher, Priority, PriorityQueueSender, RunnableVariant, TimerResolutionGuard, -}; - -pub(crate) struct WindowsDispatcher { - pub(crate) wake_posted: AtomicBool, - main_sender: PriorityQueueSender, - main_thread_id: ThreadId, - pub(crate) platform_window_handle: SafeHwnd, - validation_number: usize, -} - -impl WindowsDispatcher { - pub(crate) fn new( - main_sender: PriorityQueueSender, - platform_window_handle: HWND, - validation_number: usize, - ) -> Self { - let main_thread_id = current().id(); - let platform_window_handle = platform_window_handle.into(); - - WindowsDispatcher { - main_sender, - main_thread_id, - platform_window_handle, - validation_number, - wake_posted: AtomicBool::new(false), - } - } - - fn dispatch_on_threadpool(&self, priority: TP_CALLBACK_PRIORITY, runnable: RunnableVariant) { - let environ = TP_CALLBACK_ENVIRON_V3 { - Version: 3, - CallbackPriority: priority, - Size: size_of::() as u32, - ..Default::default() - }; - - // If the thread pool never runs our callback, the matching `from_raw` is never called, which leaks the runnable. - // Dropping the scheduled runnable would cancel its task and make the next poll of any awaiter panic. Since we expect - // the scenario to usually happen during shutdown, this leak is acceptable. - let context = runnable.into_raw().as_ptr() as *mut c_void; - - unsafe { - TrySubmitThreadpoolCallback(Some(run_work_callback), Some(context), Some(&environ)) - .log_err(); - } - } - - fn dispatch_on_threadpool_after(&self, runnable: RunnableVariant, duration: Duration) { - let context = runnable.into_raw().as_ptr() as *mut c_void; - - unsafe { - if let Ok(timer) = CreateThreadpoolTimer(Some(run_timer_callback), Some(context), None) - { - // Negative FILETIME expresses a relative delay in 100ns ticks - let ticks = (duration.as_nanos() / 100).min(i64::MAX as u128) as i64; - let due = (-ticks) as u64; - let due_time = FILETIME { - dwLowDateTime: due as u32, - dwHighDateTime: (due >> 32) as u32, - }; - SetThreadpoolTimer(timer, Some(&due_time), 0, None); - } - } - } - - #[inline(always)] - pub(crate) fn execute_runnable(runnable: RunnableVariant) { - let location = runnable.metadata().location; - let spawned = runnable.metadata().spawned; - gpui::profiler::update_running_task(spawned, location); - runnable.run(); - gpui::profiler::save_task_timing(); - } -} - -impl PlatformDispatcher for WindowsDispatcher { - fn is_main_thread(&self) -> bool { - current().id() == self.main_thread_id - } - - fn dispatch(&self, runnable: RunnableVariant, priority: Priority) { - let priority = match priority { - Priority::RealtimeAudio => { - panic!("RealtimeAudio priority should use spawn_realtime, not dispatch") - } - Priority::High => TP_CALLBACK_PRIORITY_HIGH, - Priority::Medium => TP_CALLBACK_PRIORITY_NORMAL, - Priority::Low => TP_CALLBACK_PRIORITY_LOW, - }; - self.dispatch_on_threadpool(priority, runnable); - } - - fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority) { - match self.main_sender.send(priority, runnable) { - Ok(_) => { - if !self.wake_posted.swap(true, Ordering::AcqRel) { - unsafe { - PostMessageW( - Some(self.platform_window_handle.as_raw()), - WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD, - WPARAM(self.validation_number), - LPARAM(0), - ) - .log_err(); - } - } - } - Err(runnable) => { - // NOTE: Runnable may wrap a Future that is !Send. - // - // This is usually safe because we only poll it on the main thread. - // However if the send fails, we know that: - // 1. main_receiver has been dropped (which implies the app is shutting down) - // 2. we are on a background thread. - // It is not safe to drop something !Send on the wrong thread, and - // the app will exit soon anyway, so we must forget the runnable. - std::mem::forget(runnable); - } - } - } - - fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant) { - self.dispatch_on_threadpool_after(runnable, duration); - } - - fn spawn_realtime(&self, f: Box) { - std::thread::spawn(move || { - // SAFETY: always safe to call - let thread_handle = unsafe { GetCurrentThread() }; - - // SAFETY: thread_handle is a valid handle to the current thread - unsafe { SetThreadPriority(thread_handle, THREAD_PRIORITY_TIME_CRITICAL) } - .context("thread priority") - .log_err(); - - f(); - }); - } - - fn increase_timer_resolution(&self) -> TimerResolutionGuard { - unsafe { - timeBeginPeriod(1); - } - gpui_util::defer(Box::new(|| unsafe { - timeEndPeriod(1); - })) - } -} - -unsafe extern "system" fn run_work_callback( - _instance: PTP_CALLBACK_INSTANCE, - context: *mut c_void, -) { - let runnable = unsafe { RunnableVariant::from_raw(NonNull::new_unchecked(context as *mut ())) }; - WindowsDispatcher::execute_runnable(runnable); -} - -unsafe extern "system" fn run_timer_callback( - _instance: PTP_CALLBACK_INSTANCE, - context: *mut c_void, - timer: PTP_TIMER, -) { - let runnable = unsafe { RunnableVariant::from_raw(NonNull::new_unchecked(context as *mut ())) }; - WindowsDispatcher::execute_runnable(runnable); - unsafe { CloseThreadpoolTimer(timer) }; -} diff --git a/crates/gpui_pre_windows/src/display.rs b/crates/gpui_pre_windows/src/display.rs deleted file mode 100644 index ee02309..0000000 --- a/crates/gpui_pre_windows/src/display.rs +++ /dev/null @@ -1,210 +0,0 @@ -use gpui_util::ResultExt; -use itertools::Itertools; -use smallvec::SmallVec; -use std::rc::Rc; -use uuid::Uuid; -use windows::{ - Win32::{ - Foundation::*, - Graphics::Gdi::*, - UI::{ - HiDpi::{GetDpiForMonitor, MDT_EFFECTIVE_DPI}, - WindowsAndMessaging::USER_DEFAULT_SCREEN_DPI, - }, - }, - core::*, -}; - -use crate::logical_point; -use gpui::{Bounds, DevicePixels, DisplayId, Pixels, PlatformDisplay, point, size}; - -#[derive(Debug, Clone, Copy)] -pub(crate) struct WindowsDisplay { - pub handle: HMONITOR, - pub display_id: DisplayId, - scale_factor: f32, - bounds: Bounds, - visible_bounds: Bounds, - physical_bounds: Bounds, - uuid: Uuid, -} - -// The `HMONITOR` is thread-safe. -unsafe impl Send for WindowsDisplay {} -unsafe impl Sync for WindowsDisplay {} - -impl WindowsDisplay { - pub(crate) fn new(display_id: DisplayId) -> Option { - let handle = HMONITOR(u64::from(display_id) as _); - let info = get_monitor_info(handle).log_err()?; - let monitor_size = info.monitorInfo.rcMonitor; - let work_area = info.monitorInfo.rcWork; - let uuid = generate_uuid(&info.szDevice); - let scale_factor = get_scale_factor_for_monitor(handle).log_err()?; - let physical_size = size( - (monitor_size.right - monitor_size.left).into(), - (monitor_size.bottom - monitor_size.top).into(), - ); - - Some(WindowsDisplay { - handle, - display_id, - scale_factor, - bounds: Bounds { - origin: logical_point( - monitor_size.left as f32, - monitor_size.top as f32, - scale_factor, - ), - size: physical_size.to_pixels(scale_factor), - }, - visible_bounds: Bounds { - origin: logical_point(work_area.left as f32, work_area.top as f32, scale_factor), - size: size( - (work_area.right - work_area.left) as f32 / scale_factor, - (work_area.bottom - work_area.top) as f32 / scale_factor, - ) - .map(gpui::px), - }, - physical_bounds: Bounds { - origin: point(monitor_size.left.into(), monitor_size.top.into()), - size: physical_size, - }, - uuid, - }) - } - - pub(crate) fn display_id_for_monitor(monitor: HMONITOR) -> DisplayId { - DisplayId::new(monitor.0 as u64) - } - - pub fn primary_monitor() -> Option { - // https://devblogs.microsoft.com/oldnewthing/20070809-00/?p=25643 - const POINT_ZERO: POINT = POINT { x: 0, y: 0 }; - let monitor = unsafe { MonitorFromPoint(POINT_ZERO, MONITOR_DEFAULTTOPRIMARY) }; - if monitor.is_invalid() { - log::error!( - "can not find the primary monitor: {}", - std::io::Error::last_os_error() - ); - return None; - } - WindowsDisplay::new(Self::display_id_for_monitor(monitor)) - } - - /// The DPI scale factor of this monitor, independent of whatever monitor - /// a not-yet-positioned window currently happens to be on. - pub(crate) fn scale_factor(&self) -> f32 { - self.scale_factor - } - - /// Check if the center point of given bounds is inside this monitor - pub fn check_given_bounds(&self, bounds: Bounds) -> bool { - let center = bounds.center(); - let center = POINT { - x: (center.x.as_f32() * self.scale_factor) as i32, - y: (center.y.as_f32() * self.scale_factor) as i32, - }; - let monitor = unsafe { MonitorFromPoint(center, MONITOR_DEFAULTTONULL) }; - if monitor.is_invalid() { - false - } else { - let Some(display) = WindowsDisplay::new(Self::display_id_for_monitor(monitor)) else { - return false; - }; - display.uuid == self.uuid - } - } - - pub fn displays() -> Vec> { - available_monitors() - .into_iter() - .filter_map(|handle| { - Some( - Rc::new(WindowsDisplay::new(Self::display_id_for_monitor(handle))?) - as Rc, - ) - }) - .collect() - } - - pub fn physical_bounds(&self) -> Bounds { - self.physical_bounds - } -} - -impl PlatformDisplay for WindowsDisplay { - fn id(&self) -> DisplayId { - self.display_id - } - - fn uuid(&self) -> anyhow::Result { - Ok(self.uuid) - } - - fn bounds(&self) -> Bounds { - self.bounds - } - - fn visible_bounds(&self) -> Bounds { - self.visible_bounds - } -} - -fn available_monitors() -> SmallVec<[HMONITOR; 4]> { - let mut monitors: SmallVec<[HMONITOR; 4]> = SmallVec::new(); - unsafe { - EnumDisplayMonitors( - None, - None, - Some(monitor_enum_proc), - LPARAM(&mut monitors as *mut _ as _), - ) - .ok() - .log_err(); - } - monitors -} - -unsafe extern "system" fn monitor_enum_proc( - hmonitor: HMONITOR, - _hdc: HDC, - _place: *mut RECT, - data: LPARAM, -) -> BOOL { - let monitors = data.0 as *mut SmallVec<[HMONITOR; 4]>; - unsafe { (*monitors).push(hmonitor) }; - BOOL(1) -} - -fn get_monitor_info(hmonitor: HMONITOR) -> anyhow::Result { - let mut monitor_info: MONITORINFOEXW = unsafe { std::mem::zeroed() }; - monitor_info.monitorInfo.cbSize = std::mem::size_of::() as u32; - let status = unsafe { - GetMonitorInfoW( - hmonitor, - &mut monitor_info as *mut MONITORINFOEXW as *mut MONITORINFO, - ) - }; - if status.as_bool() { - Ok(monitor_info) - } else { - Err(anyhow::anyhow!(std::io::Error::last_os_error())) - } -} - -fn generate_uuid(device_name: &[u16]) -> Uuid { - let name = device_name - .iter() - .flat_map(|&a| a.to_be_bytes()) - .collect_vec(); - Uuid::new_v5(&Uuid::NAMESPACE_DNS, &name) -} - -fn get_scale_factor_for_monitor(monitor: HMONITOR) -> Result { - let mut dpi_x = 0; - let mut dpi_y = 0; - unsafe { GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) }?; - assert_eq!(dpi_x, dpi_y); - Ok(dpi_x as f32 / USER_DEFAULT_SCREEN_DPI as f32) -} diff --git a/crates/gpui_pre_windows/src/events.rs b/crates/gpui_pre_windows/src/events.rs deleted file mode 100644 index e700cb0..0000000 --- a/crates/gpui_pre_windows/src/events.rs +++ /dev/null @@ -1,1773 +0,0 @@ -use std::{cell::Cell, rc::Rc, sync::atomic::Ordering}; - -use anyhow::Context as _; -use gpui_util::ResultExt; -use windows::{ - Win32::{ - Foundation::*, - Graphics::Gdi::*, - System::SystemServices::*, - UI::{ - Controls::*, - HiDpi::*, - Input::{Ime::*, KeyboardAndMouse::*}, - WindowsAndMessaging::*, - }, - }, - core::PCWSTR, -}; - -use crate::*; -use gpui::*; - -pub(crate) const WM_GPUI_CURSOR_STYLE_CHANGED: u32 = WM_USER + 1; -pub(crate) const WM_GPUI_CLOSE_ONE_WINDOW: u32 = WM_USER + 2; -pub(crate) const WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD: u32 = WM_USER + 3; -pub(crate) const WM_GPUI_DOCK_MENU_ACTION: u32 = WM_USER + 4; -pub(crate) const WM_GPUI_FORCE_UPDATE_WINDOW: u32 = WM_USER + 5; -pub(crate) const WM_GPUI_KEYBOARD_LAYOUT_CHANGED: u32 = WM_USER + 6; -pub(crate) const WM_GPUI_GPU_DEVICE_LOST: u32 = WM_USER + 7; -pub(crate) const WM_GPUI_KEYDOWN: u32 = WM_USER + 8; -pub(crate) const WM_GPUI_END_SESSION: u32 = WM_USER + 9; - -const SIZE_MOVE_LOOP_TIMER_ID: usize = 1; - -/// Coordinates window draws on the UI thread. Owned by the platform and -/// shared with every window (like `WindowsPlatformState::cursor_visible`), -/// because the coordination is inherently cross-window: while window A is -/// drawing, a re-entrant paint request for window B must be deferred. -pub(crate) struct DrawCoordinator { - /// Whether some window is currently inside `draw_window`. Win32 can - /// re-enter the window procedure while a draw is in progress (e.g. - /// cross-thread `SendMessage` dispatch during message pumping, or modal - /// message loops entered by COM calls), and drawing re-entrantly would - /// nest GPUI draws. Nested draws are wasted work whose output is - /// immediately redrawn, so we defer them instead. - drawing: Cell, -} - -impl DrawCoordinator { - pub(crate) fn new() -> Self { - Self { - drawing: Cell::new(false), - } - } - - fn try_begin_draw(&self) -> Option> { - // This only covers the `draw_window` span (which extends past the GPUI - // draw, through presentation and IME updates). Requests that arrive - // re-entrantly during GPUI-initiated draws (e.g. key dispatch or - // opening a window draws synchronously) are deferred by GPUI's - // `on_request_frame` callback itself, which no-ops in that case. - if self.drawing.get() { - None - } else { - self.drawing.set(true); - Some(DrawWindowGuard { coordinator: self }) - } - } -} - -struct DrawWindowGuard<'a> { - coordinator: &'a DrawCoordinator, -} - -impl Drop for DrawWindowGuard<'_> { - fn drop(&mut self) { - self.coordinator.drawing.set(false); - } -} - -impl WindowsWindowInner { - pub(crate) fn handle_msg( - self: &Rc, - handle: HWND, - msg: u32, - wparam: WPARAM, - lparam: LPARAM, - ) -> LRESULT { - let handled = match msg { - // `DefWindowProc` answers `MA_NOACTIVATE` for a left click on `HTCAPTION`. - // The activation is only triggered when `DefWindowProc` handles the following `WM_NCLBUTTONDOWN`. - // The GPUI event is dispatched in between, so a click handler runs while `active_window` is still - // whichever window was active before the click. If that handler consumes the - // press, `DefWindowProc` never sees it, so the window is never activated at all. - // So, let's eagerly activate the window. - WM_MOUSEACTIVATE => Some(MA_ACTIVATE as isize), - WM_ACTIVATE => self.handle_activate_msg(wparam), - WM_CREATE => self.handle_create_msg(handle), - WM_MOVE => self.handle_move_msg(handle, lparam), - WM_SIZE => self.handle_size_msg(wparam, lparam), - WM_GETMINMAXINFO => self.handle_get_min_max_info_msg(lparam), - WM_ENTERSIZEMOVE | WM_ENTERMENULOOP => self.handle_size_move_loop(handle), - WM_EXITSIZEMOVE | WM_EXITMENULOOP => self.handle_size_move_loop_exit(handle), - WM_TIMER => self.handle_timer_msg(handle, wparam), - WM_NCCALCSIZE => self.handle_calc_client_size(handle, wparam, lparam), - WM_DPICHANGED => self.handle_dpi_changed_msg(handle, wparam, lparam), - WM_DISPLAYCHANGE => self.handle_display_change_msg(handle), - WM_NCHITTEST => self.handle_hit_test_msg(handle, lparam), - WM_PAINT => self.handle_paint_msg(handle), - WM_CLOSE => self.handle_close_msg(), - WM_DESTROY => self.handle_destroy_msg(handle), - WM_QUERYENDSESSION => Some(1), - WM_ENDSESSION => self.handle_end_session_msg(wparam), - WM_MOUSEMOVE => self.handle_mouse_move_msg(handle, lparam, wparam), - WM_MOUSELEAVE | WM_NCMOUSELEAVE => self.handle_mouse_leave_msg(), - WM_NCMOUSEMOVE => self.handle_nc_mouse_move_msg(handle, lparam), - // Treat double click as a second single click, since we track the double clicks ourselves. - // If you don't interact with any elements, this will fall through to the windows default - // behavior of toggling whether the window is maximized. - WM_NCLBUTTONDBLCLK | WM_NCLBUTTONDOWN => { - self.handle_nc_mouse_down_msg(handle, MouseButton::Left, wparam, lparam) - } - WM_NCRBUTTONDOWN => { - self.handle_nc_mouse_down_msg(handle, MouseButton::Right, wparam, lparam) - } - WM_NCMBUTTONDOWN => { - self.handle_nc_mouse_down_msg(handle, MouseButton::Middle, wparam, lparam) - } - WM_NCLBUTTONUP => { - self.handle_nc_mouse_up_msg(handle, MouseButton::Left, wparam, lparam) - } - WM_NCRBUTTONUP => { - self.handle_nc_mouse_up_msg(handle, MouseButton::Right, wparam, lparam) - } - WM_NCMBUTTONUP => { - self.handle_nc_mouse_up_msg(handle, MouseButton::Middle, wparam, lparam) - } - WM_LBUTTONDOWN => self.handle_mouse_down_msg(handle, MouseButton::Left, lparam), - WM_RBUTTONDOWN => self.handle_mouse_down_msg(handle, MouseButton::Right, lparam), - WM_MBUTTONDOWN => self.handle_mouse_down_msg(handle, MouseButton::Middle, lparam), - WM_XBUTTONDOWN => { - self.handle_xbutton_msg(handle, wparam, lparam, Self::handle_mouse_down_msg) - } - WM_LBUTTONUP => self.handle_mouse_up_msg(handle, MouseButton::Left, lparam), - WM_RBUTTONUP => self.handle_mouse_up_msg(handle, MouseButton::Right, lparam), - WM_MBUTTONUP => self.handle_mouse_up_msg(handle, MouseButton::Middle, lparam), - WM_XBUTTONUP => { - self.handle_xbutton_msg(handle, wparam, lparam, Self::handle_mouse_up_msg) - } - WM_MOUSEWHEEL => self.handle_mouse_wheel_msg(handle, wparam, lparam), - WM_MOUSEHWHEEL => self.handle_mouse_horizontal_wheel_msg(handle, wparam, lparam), - WM_SYSKEYUP => self.handle_syskeyup_msg(wparam, lparam), - WM_KEYUP => self.handle_keyup_msg(wparam, lparam), - WM_GPUI_KEYDOWN => self.handle_keydown_msg(wparam, lparam), - WM_CHAR => self.handle_char_msg(wparam), - WM_IME_STARTCOMPOSITION => self.handle_ime_position(handle), - WM_IME_COMPOSITION => self.handle_ime_composition(handle, lparam), - WM_SETCURSOR => self.handle_set_cursor(handle, lparam), - WM_SETTINGCHANGE => self.handle_system_settings_changed(handle, wparam, lparam), - WM_INPUTLANGCHANGE => self.handle_input_language_changed(), - WM_SHOWWINDOW => self.handle_window_visibility_changed(handle, wparam), - WM_GPUI_CURSOR_STYLE_CHANGED => self.handle_cursor_changed(lparam), - WM_GPUI_FORCE_UPDATE_WINDOW => self.draw_window(handle, true), - WM_GPUI_GPU_DEVICE_LOST => self.handle_device_lost(lparam), - DM_POINTERHITTEST => self.handle_dm_pointer_hit_test(wparam), - WM_GETOBJECT => self.handle_wm_getobject(wparam, lparam), - _ => None, - }; - if let Some(n) = handled { - LRESULT(n) - } else { - unsafe { DefWindowProcW(handle, msg, wparam, lparam) } - } - } - - fn handle_end_session_msg(&self, wparam: WPARAM) -> Option { - if wparam.0 != 0 { - unsafe { - SendMessageW( - self.platform_window_handle, - WM_GPUI_END_SESSION, - Some(WPARAM(self.validation_number)), - None, - ); - } - } - Some(0) - } - - fn handle_move_msg(&self, handle: HWND, lparam: LPARAM) -> Option { - let origin = logical_point( - lparam.signed_loword() as f32, - lparam.signed_hiword() as f32, - self.state.scale_factor.get(), - ); - self.state.origin.set(origin); - let size = self.state.logical_size.get(); - let center_x = origin.x.as_f32() + size.width.as_f32() / 2.; - let center_y = origin.y.as_f32() + size.height.as_f32() / 2.; - let monitor_bounds = self.state.display.get().bounds(); - if center_x < monitor_bounds.left().as_f32() - || center_x > monitor_bounds.right().as_f32() - || center_y < monitor_bounds.top().as_f32() - || center_y > monitor_bounds.bottom().as_f32() - { - // center of the window may have moved to another monitor - let monitor = unsafe { MonitorFromWindow(handle, MONITOR_DEFAULTTONULL) }; - // minimize the window can trigger this event too, in this case, - // monitor is invalid, we do nothing. - if !monitor.is_invalid() && self.state.display.get().handle != monitor { - // we will get the same monitor if we only have one - self.state.display.set(WindowsDisplay::new( - WindowsDisplay::display_id_for_monitor(monitor), - )?); - } - } - if let Some(mut callback) = self.state.callbacks.moved.take() { - callback(); - self.state.callbacks.moved.set(Some(callback)); - } - Some(0) - } - - fn handle_get_min_max_info_msg(&self, lparam: LPARAM) -> Option { - let min_size = self.state.min_size?; - let scale_factor = self.state.scale_factor.get(); - let boarder_offset = &self.state.border_offset; - - unsafe { - let minmax_info = &mut *(lparam.0 as *mut MINMAXINFO); - minmax_info.ptMinTrackSize.x = min_size.width.scale(scale_factor).as_f32() as i32 - + boarder_offset.width_offset.get(); - minmax_info.ptMinTrackSize.y = min_size.height.scale(scale_factor).as_f32() as i32 - + boarder_offset.height_offset.get(); - } - Some(0) - } - - fn handle_size_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option { - // Don't resize the renderer when the window is minimized, but record that it was minimized so - // that on restore the swap chain can be recreated via `update_drawable_size_even_if_unchanged`. - if wparam.0 == SIZE_MINIMIZED as usize { - self.state - .restore_from_minimized - .set(self.state.callbacks.request_frame.take()); - return Some(0); - } - - let width = lparam.loword().max(1) as i32; - let height = lparam.hiword().max(1) as i32; - let new_size = size(DevicePixels(width), DevicePixels(height)); - - let scale_factor = self.state.scale_factor.get(); - let mut should_resize_renderer = false; - if let Some(restore_from_minimized) = self.state.restore_from_minimized.take() { - self.state - .callbacks - .request_frame - .set(Some(restore_from_minimized)); - } else { - should_resize_renderer = true; - } - - self.handle_size_change(new_size, scale_factor, should_resize_renderer); - Some(0) - } - - fn handle_size_change( - &self, - device_size: Size, - scale_factor: f32, - should_resize_renderer: bool, - ) { - let new_logical_size = device_size.to_pixels(scale_factor); - - self.state.logical_size.set(new_logical_size); - if should_resize_renderer - && let Err(e) = self.state.renderer.borrow_mut().resize(device_size) - { - log::error!("Failed to resize renderer, invalidating devices: {}", e); - self.state - .invalidate_devices - .store(true, std::sync::atomic::Ordering::Release); - } - if let Some(mut callback) = self.state.callbacks.resize.take() { - callback(new_logical_size, scale_factor); - self.state.callbacks.resize.set(Some(callback)); - } - } - - fn handle_size_move_loop(&self, handle: HWND) -> Option { - unsafe { - let ret = SetTimer( - Some(handle), - SIZE_MOVE_LOOP_TIMER_ID, - USER_TIMER_MINIMUM, - None, - ); - if ret == 0 { - log::error!( - "unable to create timer: {}", - std::io::Error::last_os_error() - ); - } - } - None - } - - fn handle_size_move_loop_exit(&self, handle: HWND) -> Option { - unsafe { - KillTimer(Some(handle), SIZE_MOVE_LOOP_TIMER_ID).log_err(); - } - None - } - - fn handle_timer_msg(&self, handle: HWND, wparam: WPARAM) -> Option { - if wparam.0 == SIZE_MOVE_LOOP_TIMER_ID { - let mut runnables = self.main_receiver.clone().try_iter(); - while let Some(Ok(runnable)) = runnables.next() { - WindowsDispatcher::execute_runnable(runnable); - } - self.handle_paint_msg(handle) - } else { - None - } - } - - fn handle_paint_msg(&self, handle: HWND) -> Option { - self.draw_window(handle, false) - } - - fn handle_close_msg(&self) -> Option { - let mut callback = self.state.callbacks.should_close.take()?; - let should_close = callback(); - self.state.callbacks.should_close.set(Some(callback)); - if should_close { None } else { Some(0) } - } - - fn handle_destroy_msg(&self, handle: HWND) -> Option { - let callback = { self.state.callbacks.close.take() }; - // Re-enable parent window if this was a modal dialog - if let Some(parent_hwnd) = self.parent_hwnd { - unsafe { - let _ = EnableWindow(parent_hwnd, true); - let _ = SetForegroundWindow(parent_hwnd); - } - } - - if let Some(callback) = callback { - callback(); - } - unsafe { - PostMessageW( - Some(self.platform_window_handle), - WM_GPUI_CLOSE_ONE_WINDOW, - WPARAM(self.validation_number), - LPARAM(handle.0 as isize), - ) - .log_err(); - } - Some(0) - } - - fn handle_mouse_move_msg(&self, handle: HWND, lparam: LPARAM, wparam: WPARAM) -> Option { - self.start_tracking_mouse(handle, TME_LEAVE); - self.restore_cursor_after_hide(); - - let Some(mut func) = self.state.callbacks.input.take() else { - return Some(1); - }; - let scale_factor = self.state.scale_factor.get(); - - let pressed_button = match MODIFIERKEYS_FLAGS(wparam.loword() as u32) { - flags if flags.contains(MK_LBUTTON) => Some(MouseButton::Left), - flags if flags.contains(MK_RBUTTON) => Some(MouseButton::Right), - flags if flags.contains(MK_MBUTTON) => Some(MouseButton::Middle), - flags if flags.contains(MK_XBUTTON1) => { - Some(MouseButton::Navigate(NavigationDirection::Back)) - } - flags if flags.contains(MK_XBUTTON2) => { - Some(MouseButton::Navigate(NavigationDirection::Forward)) - } - _ => None, - }; - let x = lparam.signed_loword() as f32; - let y = lparam.signed_hiword() as f32; - let input = PlatformInput::MouseMove(MouseMoveEvent { - position: logical_point(x, y, scale_factor), - pressed_button, - modifiers: current_modifiers(), - }); - let handled = !func(input).propagate; - self.state.callbacks.input.set(Some(func)); - - if handled { Some(0) } else { Some(1) } - } - - fn handle_mouse_leave_msg(&self) -> Option { - self.state.hovered.set(false); - // The next window's `WM_SETCURSOR` picks its own cursor, so we just clear - // the flag for tight `is_cursor_visible()` semantics. - self.state.cursor_visible.store(true, Ordering::Relaxed); - if let Some(mut callback) = self.state.callbacks.hovered_status_change.take() { - callback(false); - self.state - .callbacks - .hovered_status_change - .set(Some(callback)); - } - - Some(0) - } - - fn handle_syskeyup_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option { - let input = handle_key_event(wparam, lparam, &self.state, |keystroke, _| { - PlatformInput::KeyUp(KeyUpEvent { keystroke }) - })?; - let mut func = self.state.callbacks.input.take()?; - - func(input); - self.state.callbacks.input.set(Some(func)); - - // Always return 0 to indicate that the message was handled, so we could properly handle `ModifiersChanged` event. - Some(0) - } - - // It's a known bug that you can't trigger `ctrl-shift-0`. See: - // https://superuser.com/questions/1455762/ctrl-shift-number-key-combination-has-stopped-working-for-a-few-numbers - fn handle_keydown_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option { - let Some(input) = handle_key_event( - wparam, - lparam, - &self.state, - |keystroke, prefer_character_input| { - PlatformInput::KeyDown(KeyDownEvent { - keystroke, - is_held: lparam.0 & (0x1 << 30) > 0, - prefer_character_input, - }) - }, - ) else { - return Some(1); - }; - - let Some(mut func) = self.state.callbacks.input.take() else { - return Some(1); - }; - - let handled = !func(input).propagate; - - self.state.callbacks.input.set(Some(func)); - - if handled { Some(0) } else { Some(1) } - } - - fn handle_keyup_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option { - let Some(input) = handle_key_event(wparam, lparam, &self.state, |keystroke, _| { - PlatformInput::KeyUp(KeyUpEvent { keystroke }) - }) else { - return Some(1); - }; - - let Some(mut func) = self.state.callbacks.input.take() else { - return Some(1); - }; - - let handled = !func(input).propagate; - self.state.callbacks.input.set(Some(func)); - - if handled { Some(0) } else { Some(1) } - } - - fn handle_char_msg(&self, wparam: WPARAM) -> Option { - let input = self.parse_char_message(wparam)?; - self.with_input_handler(|input_handler| { - input_handler.replace_text_in_range(None, &input); - }); - - Some(0) - } - - fn handle_mouse_down_msg( - &self, - handle: HWND, - button: MouseButton, - lparam: LPARAM, - ) -> Option { - unsafe { SetCapture(handle) }; - - let Some(mut func) = self.state.callbacks.input.take() else { - return Some(1); - }; - let x = lparam.signed_loword(); - let y = lparam.signed_hiword(); - let physical_point = point(DevicePixels(x as i32), DevicePixels(y as i32)); - let click_count = self.state.click_state.update(button, physical_point); - let scale_factor = self.state.scale_factor.get(); - - let input = PlatformInput::MouseDown(MouseDownEvent { - button, - position: logical_point(x as f32, y as f32, scale_factor), - modifiers: current_modifiers(), - click_count, - first_mouse: false, - }); - let handled = !func(input).propagate; - self.state.callbacks.input.set(Some(func)); - - if handled { Some(0) } else { Some(1) } - } - - fn handle_mouse_up_msg( - &self, - _handle: HWND, - button: MouseButton, - lparam: LPARAM, - ) -> Option { - unsafe { ReleaseCapture().log_err() }; - - let Some(mut func) = self.state.callbacks.input.take() else { - return Some(1); - }; - let x = lparam.signed_loword() as f32; - let y = lparam.signed_hiword() as f32; - let click_count = self.state.click_state.current_count.get(); - let scale_factor = self.state.scale_factor.get(); - - let input = PlatformInput::MouseUp(MouseUpEvent { - button, - position: logical_point(x, y, scale_factor), - modifiers: current_modifiers(), - click_count, - }); - let handled = !func(input).propagate; - self.state.callbacks.input.set(Some(func)); - - if handled { Some(0) } else { Some(1) } - } - - fn handle_xbutton_msg( - &self, - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - handler: impl Fn(&Self, HWND, MouseButton, LPARAM) -> Option, - ) -> Option { - let nav_dir = match wparam.hiword() { - XBUTTON1 => NavigationDirection::Back, - XBUTTON2 => NavigationDirection::Forward, - _ => return Some(1), - }; - handler(self, handle, MouseButton::Navigate(nav_dir), lparam) - } - - fn handle_mouse_wheel_msg( - &self, - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - ) -> Option { - let modifiers = current_modifiers(); - - let Some(mut func) = self.state.callbacks.input.take() else { - return Some(1); - }; - let scale_factor = self.state.scale_factor.get(); - let wheel_scroll_amount = match modifiers.shift { - true => self - .system_settings() - .mouse_wheel_settings - .wheel_scroll_chars - .get(), - false => self - .system_settings() - .mouse_wheel_settings - .wheel_scroll_lines - .get(), - }; - - let wheel_distance = - (wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_amount as f32; - let mut cursor_point = POINT { - x: lparam.signed_loword().into(), - y: lparam.signed_hiword().into(), - }; - unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; - let input = PlatformInput::ScrollWheel(ScrollWheelEvent { - position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), - delta: ScrollDelta::Lines(match modifiers.shift { - true => Point { - x: wheel_distance, - y: 0.0, - }, - false => Point { - y: wheel_distance, - x: 0.0, - }, - }), - modifiers, - touch_phase: TouchPhase::Moved, - }); - let handled = !func(input).propagate; - self.state.callbacks.input.set(Some(func)); - - if handled { Some(0) } else { Some(1) } - } - - fn handle_mouse_horizontal_wheel_msg( - &self, - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - ) -> Option { - let Some(mut func) = self.state.callbacks.input.take() else { - return Some(1); - }; - let scale_factor = self.state.scale_factor.get(); - let wheel_scroll_chars = self - .system_settings() - .mouse_wheel_settings - .wheel_scroll_chars - .get(); - - let wheel_distance = - (-wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_chars as f32; - let mut cursor_point = POINT { - x: lparam.signed_loword().into(), - y: lparam.signed_hiword().into(), - }; - unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; - let event = PlatformInput::ScrollWheel(ScrollWheelEvent { - position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), - delta: ScrollDelta::Lines(Point { - x: wheel_distance, - y: 0.0, - }), - modifiers: current_modifiers(), - touch_phase: TouchPhase::Moved, - }); - let handled = !func(event).propagate; - self.state.callbacks.input.set(Some(func)); - - if handled { Some(0) } else { Some(1) } - } - - fn retrieve_caret_position(&self) -> Option { - self.with_input_handler_and_scale_factor(|input_handler, scale_factor| { - let caret_range = input_handler.selected_text_range(false)?; - let caret_position = input_handler.bounds_for_range(caret_range.range)?; - Some(POINT { - // logical to physical - x: (caret_position.origin.x.as_f32() * scale_factor) as i32, - y: (caret_position.origin.y.as_f32() * scale_factor) as i32 - + ((caret_position.size.height.as_f32() * scale_factor) as i32 / 2), - }) - }) - } - - fn handle_ime_position(&self, handle: HWND) -> Option { - if let Some(caret_position) = self.retrieve_caret_position() { - self.update_ime_position(handle, caret_position); - } - Some(0) - } - - pub(crate) fn update_ime_position(&self, handle: HWND, caret_position: POINT) { - let Some(ctx) = ImeContext::get(handle) else { - return; - }; - unsafe { - ImmSetCompositionWindow( - *ctx, - &COMPOSITIONFORM { - dwStyle: CFS_POINT, - ptCurrentPos: caret_position, - ..Default::default() - }, - ) - .ok() - .log_err(); - - ImmSetCandidateWindow( - *ctx, - &CANDIDATEFORM { - dwStyle: CFS_CANDIDATEPOS, - ptCurrentPos: caret_position, - ..Default::default() - }, - ) - .ok() - .log_err(); - } - } - - fn update_ime_enabled(&self, handle: HWND) { - let ime_enabled = self - .with_input_handler(|input_handler| input_handler.query_accepts_text_input()) - .unwrap_or(false); - if ime_enabled == self.state.ime_enabled.get() { - return; - } - self.state.ime_enabled.set(ime_enabled); - unsafe { - if ime_enabled { - ImmAssociateContextEx(handle, HIMC::default(), IACE_DEFAULT) - .ok() - .log_err(); - } else { - // The IME context is per-thread, so without this check a change in this - // window's text input state could commit an IME composition happening in another window. - if GetFocus() == handle - && let Some(ctx) = ImeContext::get(handle) - { - ImmNotifyIME(*ctx, NI_COMPOSITIONSTR, CPS_COMPLETE, 0) - .ok() - .log_err(); - } - ImmAssociateContextEx(handle, HIMC::default(), 0) - .ok() - .log_err(); - } - } - } - - fn handle_ime_composition(&self, handle: HWND, lparam: LPARAM) -> Option { - let ctx = ImeContext::get(handle)?; - self.handle_ime_composition_inner(*ctx, lparam) - } - - fn handle_ime_composition_inner(&self, ctx: HIMC, lparam: LPARAM) -> Option { - let lparam = lparam.0 as u32; - if lparam == 0 { - // Japanese IME may send this message with lparam = 0, which indicates that - // there is no composition string. - self.with_input_handler(|input_handler| { - input_handler.replace_text_in_range(None, ""); - })?; - Some(0) - } else { - if lparam & GCS_RESULTSTR.0 > 0 { - let comp_result = parse_ime_composition_string(ctx, GCS_RESULTSTR)?; - self.with_input_handler(|input_handler| { - input_handler - .replace_text_in_range(None, &String::from_utf16_lossy(&comp_result)); - })?; - } - if lparam & GCS_COMPSTR.0 > 0 { - let comp_string = parse_ime_composition_string(ctx, GCS_COMPSTR)?; - let caret_pos = - (!comp_string.is_empty() && lparam & GCS_CURSORPOS.0 > 0).then(|| { - let cursor_pos = retrieve_composition_cursor_position(ctx); - let pos = if should_use_ime_cursor_position(ctx, cursor_pos) { - cursor_pos - } else { - comp_string.len() - }; - pos..pos - }); - self.with_input_handler(|input_handler| { - input_handler.replace_and_mark_text_in_range( - None, - &String::from_utf16_lossy(&comp_string), - caret_pos, - ); - })?; - } - if lparam & (GCS_RESULTSTR.0 | GCS_COMPSTR.0) > 0 { - return Some(0); - } - - // currently, we don't care other stuff - None - } - } - - fn handle_calc_client_size( - &self, - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - ) -> Option { - if !self.hide_title_bar || self.state.is_fullscreen() || wparam.0 == 0 { - return None; - } - - unsafe { - let params = lparam.0 as *mut NCCALCSIZE_PARAMS; - let saved_top = (*params).rgrc[0].top; - let result = DefWindowProcW(handle, WM_NCCALCSIZE, wparam, lparam); - (*params).rgrc[0].top = saved_top; - if self.state.is_maximized() { - let dpi = GetDpiForWindow(handle); - (*params).rgrc[0].top += get_frame_thicknessx(dpi); - } - Some(result.0 as isize) - } - } - - fn handle_activate_msg(self: &Rc, wparam: WPARAM) -> Option { - let activated = wparam.loword() > 0; - - let events = self - .state - .a11y - .try_borrow_mut() - .ok() - .and_then(|mut a11y| a11y.as_mut()?.adapter.update_window_focus_state(activated)); - if let Some(events) = events { - events.raise(); - } - - let this = self.clone(); - - if !activated { - this.state.cursor_visible.store(true, Ordering::Relaxed); - } - - // When the window is activated (gains focus), reset the modifier tracking state. - // This fixes the issue where Alt-Tab away and back leaves stale modifier state - // (especially the Alt key) because Windows doesn't always send key-up events to - // windows that have lost focus. - if activated { - this.state.last_reported_modifiers.set(None); - this.state.last_reported_capslock.set(None); - - if let Some(mut func) = this.state.callbacks.input.take() { - let input = PlatformInput::ModifiersChanged(ModifiersChangedEvent { - modifiers: current_modifiers(), - capslock: current_capslock(), - }); - func(input); - this.state.callbacks.input.set(Some(func)); - } - } - - self.executor - .spawn(async move { - if let Some(mut func) = this.state.callbacks.active_status_change.take() { - func(activated); - this.state.callbacks.active_status_change.set(Some(func)); - } - }) - .detach(); - - None - } - - fn handle_wm_getobject(&self, wparam: WPARAM, lparam: LPARAM) -> Option { - let result = { - let mut a11y = self.state.a11y.borrow_mut(); - let a11y = a11y.as_mut()?; - a11y.adapter.handle_wm_getobject( - accesskit_windows::WPARAM(wparam.0), - accesskit_windows::LPARAM(lparam.0), - &mut a11y.activation_handler, - )? - }; - // The borrow above must be dropped before calling `.into()`, because - // it calls `UiaReturnRawElementProvider` which may send a nested - // `WM_GETOBJECT` back into this window procedure. - let lresult: accesskit_windows::LRESULT = result.into(); - Some(lresult.0) - } - - fn handle_create_msg(&self, handle: HWND) -> Option { - if self.hide_title_bar { - notify_frame_changed(handle); - Some(0) - } else { - None - } - } - - fn handle_dpi_changed_msg( - &self, - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - ) -> Option { - let new_dpi = wparam.loword() as f32; - - let is_maximized = self.state.is_maximized(); - let new_scale_factor = new_dpi / USER_DEFAULT_SCREEN_DPI as f32; - self.state.scale_factor.set(new_scale_factor); - self.state.border_offset.update(handle).log_err(); - - self.state - .direct_manipulation - .set_scale_factor(new_scale_factor); - - if is_maximized { - // Get the monitor and its work area at the new DPI - let monitor = unsafe { MonitorFromWindow(handle, MONITOR_DEFAULTTONEAREST) }; - let mut monitor_info: MONITORINFO = unsafe { std::mem::zeroed() }; - monitor_info.cbSize = std::mem::size_of::() as u32; - if unsafe { GetMonitorInfoW(monitor, &mut monitor_info) }.as_bool() { - let work_area = monitor_info.rcWork; - let width = work_area.right - work_area.left; - let height = work_area.bottom - work_area.top; - - // Update the window size to match the new monitor work area - // This will trigger WM_SIZE which will handle the size change - unsafe { - SetWindowPos( - handle, - None, - work_area.left, - work_area.top, - width, - height, - SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED, - ) - .context("unable to set maximized window position after dpi has changed") - .log_err(); - } - - // SetWindowPos may not send WM_SIZE for maximized windows in some cases, - // so we manually update the size to ensure proper rendering - let device_size = size(DevicePixels(width), DevicePixels(height)); - self.handle_size_change(device_size, new_scale_factor, true); - } - } else { - // For non-maximized windows, use the suggested RECT from the system - let rect = unsafe { &*(lparam.0 as *const RECT) }; - let width = rect.right - rect.left; - let height = rect.bottom - rect.top; - // this will emit `WM_SIZE` and `WM_MOVE` right here - // even before this function returns - // the new size is handled in `WM_SIZE` - unsafe { - SetWindowPos( - handle, - None, - rect.left, - rect.top, - width, - height, - SWP_NOZORDER | SWP_NOACTIVATE, - ) - .context("unable to set window position after dpi has changed") - .log_err(); - } - } - - Some(0) - } - - fn handle_display_change_msg(&self, handle: HWND) -> Option { - let new_monitor = unsafe { MonitorFromWindow(handle, MONITOR_DEFAULTTONULL) }; - if new_monitor.is_invalid() { - log::error!("No monitor detected!"); - return None; - } - let new_display = WindowsDisplay::new(WindowsDisplay::display_id_for_monitor(new_monitor))?; - self.state.display.set(new_display); - Some(0) - } - - fn handle_hit_test_msg(&self, handle: HWND, lparam: LPARAM) -> Option { - if self.state.is_fullscreen() { - return None; - } - - let callback = self.state.callbacks.hit_test_window_control.take(); - let drag_area = if let Some(mut callback) = callback { - let area = callback(); - self.state - .callbacks - .hit_test_window_control - .set(Some(callback)); - area.and_then(|area| match area { - WindowControlArea::Drag if self.is_movable => Some(HTCAPTION as _), - WindowControlArea::Drag => None, - WindowControlArea::Close => Some(HTCLOSE as _), - WindowControlArea::Max if self.is_resizable => Some(HTMAXBUTTON as _), - WindowControlArea::Max if self.is_movable => Some(HTCAPTION as _), - WindowControlArea::Max => Some(HTNOWHERE as _), - WindowControlArea::Min if self.is_minimizable => Some(HTMINBUTTON as _), - WindowControlArea::Min if self.is_movable => Some(HTCAPTION as _), - WindowControlArea::Min => Some(HTNOWHERE as _), - }) - } else { - None - }; - - if !self.hide_title_bar { - // If the OS draws the title bar, we don't need to handle hit test messages. - return drag_area; - } - - let dpi = unsafe { GetDpiForWindow(handle) }; - // We do not use the OS title bar, so the default `DefWindowProcW` will only register a 1px edge for resizes - // We need to calculate the frame thickness ourselves and do the hit test manually. - let frame_y = get_frame_thicknessx(dpi); - let frame_x = get_frame_thicknessy(dpi); - let mut cursor_point = POINT { - x: lparam.signed_loword().into(), - y: lparam.signed_hiword().into(), - }; - - unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; - if self.is_resizable - && !self.state.is_maximized() - && 0 <= cursor_point.y - && cursor_point.y <= frame_y - { - // x-axis actually goes from -frame_x to 0 - return Some(if cursor_point.x <= 0 { - HTTOPLEFT - } else { - let mut rect = Default::default(); - unsafe { GetWindowRect(handle, &mut rect) }.log_err(); - // right and bottom bounds of RECT are exclusive, thus `-1` - let right = rect.right - rect.left - 1; - // the bounds include the padding frames, so accommodate for both of them - if right - 2 * frame_x <= cursor_point.x { - HTTOPRIGHT - } else { - HTTOP - } - } as _); - } - - drag_area - } - - fn handle_nc_mouse_move_msg(&self, handle: HWND, lparam: LPARAM) -> Option { - self.start_tracking_mouse(handle, TME_LEAVE | TME_NONCLIENT); - self.restore_cursor_after_hide(); - - let mut func = self.state.callbacks.input.take()?; - let scale_factor = self.state.scale_factor.get(); - - let mut cursor_point = POINT { - x: lparam.signed_loword().into(), - y: lparam.signed_hiword().into(), - }; - unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; - let input = PlatformInput::MouseMove(MouseMoveEvent { - position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), - pressed_button: None, - modifiers: current_modifiers(), - }); - let handled = !func(input).propagate; - self.state.callbacks.input.set(Some(func)); - - if handled { Some(0) } else { None } - } - - fn handle_nc_mouse_down_msg( - &self, - handle: HWND, - button: MouseButton, - wparam: WPARAM, - lparam: LPARAM, - ) -> Option { - if let Some(mut func) = self.state.callbacks.input.take() { - let scale_factor = self.state.scale_factor.get(); - let mut cursor_point = POINT { - x: lparam.signed_loword().into(), - y: lparam.signed_hiword().into(), - }; - unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; - let physical_point = point(DevicePixels(cursor_point.x), DevicePixels(cursor_point.y)); - let click_count = self.state.click_state.update(button, physical_point); - - let input = PlatformInput::MouseDown(MouseDownEvent { - button, - position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), - modifiers: current_modifiers(), - click_count, - first_mouse: false, - }); - let handled = !func(input).propagate; - self.state.callbacks.input.set(Some(func)); - - if handled { - return Some(0); - } - } else { - }; - - // Since these are handled in handle_nc_mouse_up_msg we must prevent the default window proc - if button == MouseButton::Left { - match wparam.0 as u32 { - HTMINBUTTON => self.state.nc_button_pressed.set(Some(HTMINBUTTON)), - HTMAXBUTTON => self.state.nc_button_pressed.set(Some(HTMAXBUTTON)), - HTCLOSE => self.state.nc_button_pressed.set(Some(HTCLOSE)), - _ => return None, - }; - Some(0) - } else { - None - } - } - - fn handle_nc_mouse_up_msg( - &self, - handle: HWND, - button: MouseButton, - wparam: WPARAM, - lparam: LPARAM, - ) -> Option { - if let Some(mut func) = self.state.callbacks.input.take() { - let scale_factor = self.state.scale_factor.get(); - - let mut cursor_point = POINT { - x: lparam.signed_loword().into(), - y: lparam.signed_hiword().into(), - }; - unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; - let input = PlatformInput::MouseUp(MouseUpEvent { - button, - position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), - modifiers: current_modifiers(), - click_count: 1, - }); - let handled = !func(input).propagate; - self.state.callbacks.input.set(Some(func)); - - if handled { - return Some(0); - } - } else { - } - - let last_pressed = self.state.nc_button_pressed.take(); - if button == MouseButton::Left - && let Some(last_pressed) = last_pressed - { - let handled = match (wparam.0 as u32, last_pressed) { - (HTMINBUTTON, HTMINBUTTON) if self.is_minimizable => { - unsafe { ShowWindowAsync(handle, SW_MINIMIZE).ok().log_err() }; - true - } - (HTMINBUTTON, HTMINBUTTON) => true, - (HTMAXBUTTON, HTMAXBUTTON) if self.is_resizable => { - if self.state.is_maximized() { - unsafe { ShowWindowAsync(handle, SW_NORMAL).ok().log_err() }; - } else { - unsafe { ShowWindowAsync(handle, SW_MAXIMIZE).ok().log_err() }; - } - true - } - (HTMAXBUTTON, HTMAXBUTTON) => true, - (HTCLOSE, HTCLOSE) => { - unsafe { - PostMessageW(Some(handle), WM_CLOSE, WPARAM::default(), LPARAM::default()) - .log_err() - }; - true - } - _ => false, - }; - if handled { - return Some(0); - } - } - - None - } - - fn handle_cursor_changed(&self, lparam: LPARAM) -> Option { - let had_cursor = self.state.current_cursor.get().is_some(); - - self.state.current_cursor.set(if lparam.0 == 0 { - None - } else { - Some(HCURSOR(lparam.0 as _)) - }); - - if had_cursor != self.state.current_cursor.get().is_some() { - unsafe { SetCursor(self.state.current_cursor.get()) }; - } - - Some(0) - } - - fn handle_set_cursor(&self, handle: HWND, lparam: LPARAM) -> Option { - if unsafe { !IsWindowEnabled(handle).as_bool() } - || matches!( - lparam.loword() as u32, - HTLEFT - | HTRIGHT - | HTTOP - | HTTOPLEFT - | HTTOPRIGHT - | HTBOTTOM - | HTBOTTOMLEFT - | HTBOTTOMRIGHT - ) - { - return None; - } - let cursor = if self.state.cursor_visible.load(Ordering::Relaxed) { - self.state.current_cursor.get() - } else { - None - }; - unsafe { - SetCursor(cursor); - }; - Some(0) - } - - fn handle_system_settings_changed( - &self, - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - ) -> Option { - if wparam.0 != 0 { - self.state.click_state.system_update(wparam.0); - self.state.border_offset.update(handle).log_err(); - // system settings may emit a window message which wants to take the refcell self.state, so drop it - - self.system_settings().update(wparam.0); - } else { - self.handle_system_theme_changed(handle, lparam)?; - }; - - Some(0) - } - - fn handle_system_theme_changed(&self, handle: HWND, lparam: LPARAM) -> Option { - // lParam is a pointer to a string that indicates the area containing the system parameter - // that was changed. - let parameter = PCWSTR::from_raw(lparam.0 as _); - if unsafe { !parameter.is_null() && !parameter.is_empty() } - && let Some(parameter_string) = unsafe { parameter.to_string() }.log_err() - { - log::info!("System settings changed: {}", parameter_string); - if parameter_string.as_str() == "ImmersiveColorSet" { - let new_appearance = system_appearance() - .context("unable to get system appearance when handling ImmersiveColorSet") - .log_err()?; - - if new_appearance != self.state.appearance.get() { - self.state.appearance.set(new_appearance); - let mut callback = self.state.callbacks.appearance_changed.take()?; - - callback(); - self.state.callbacks.appearance_changed.set(Some(callback)); - configure_dwm_dark_mode(handle, new_appearance); - } - } - } - Some(0) - } - - fn handle_input_language_changed(&self) -> Option { - unsafe { - PostMessageW( - Some(self.platform_window_handle), - WM_GPUI_KEYBOARD_LAYOUT_CHANGED, - WPARAM(self.validation_number), - LPARAM(0), - ) - .log_err(); - } - Some(0) - } - - fn handle_window_visibility_changed(&self, handle: HWND, wparam: WPARAM) -> Option { - if wparam.0 == 1 { - self.draw_window(handle, false); - } - None - } - - fn handle_device_lost(&self, lparam: LPARAM) -> Option { - let devices = lparam.0 as *const DirectXDevices; - let devices = unsafe { &*devices }; - if let Err(err) = self - .state - .renderer - .borrow_mut() - .handle_device_lost(&devices) - { - panic!("Device lost: {err}"); - } - // Make sure the first `draw_window` after recovery (whether it comes - // from the forced WM_GPUI_FORCE_UPDATE_WINDOW or a stray WM_PAINT in - // between) is treated as a forced render so it both clears - // `skip_draws` and bypasses the view cache. - self.state.force_render_pending.set(true); - Some(0) - } - - fn handle_dm_pointer_hit_test(&self, wparam: WPARAM) -> Option { - self.state.direct_manipulation.on_pointer_hit_test(wparam); - None - } - - #[inline] - fn draw_window(&self, handle: HWND, force_render: bool) -> Option { - let Some(_guard) = self.state.draw_coordinator.try_begin_draw() else { - log::debug!("deferring re-entrant draw of window {handle:?}"); - if force_render { - self.state.force_render_pending.set(true); - } - // Validate the region so a nested message pump doesn't keep - // re-dispatching WM_PAINT for the still-invalid region in a busy - // loop until the in-progress draw unwinds. The vsync thread - // re-invalidates every window on each vsync (see - // `begin_vsync_thread`), so the deferred frame still gets drawn, - // at most one vsync late. - unsafe { ValidateRect(Some(handle), None).ok().log_err() }; - return Some(0); - }; - let mut request_frame = self.state.callbacks.request_frame.take()?; - - self.state.direct_manipulation.update(); - - let events = self.state.direct_manipulation.drain_events(); - if !events.is_empty() { - if let Some(mut func) = self.state.callbacks.input.take() { - for event in events { - func(event); - } - self.state.callbacks.input.set(Some(func)); - } - } - - let force_render = force_render || self.state.force_render_pending.take(); - if force_render { - // Re-enable drawing after a device loss recovery. The forced render - // will rebuild the scene with fresh atlas textures. - self.state.renderer.borrow_mut().mark_drawable(); - } - request_frame(RequestFrameOptions { - require_presentation: false, - force_render, - }); - - self.state.callbacks.request_frame.set(Some(request_frame)); - self.update_ime_enabled(handle); - unsafe { ValidateRect(Some(handle), None).ok().log_err() }; - - Some(0) - } - - #[inline] - fn parse_char_message(&self, wparam: WPARAM) -> Option { - let code_point = wparam.loword(); - - // https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-3/#G2630 - match code_point { - 0xD800..=0xDBFF => { - // High surrogate, wait for low surrogate - self.state.pending_surrogate.set(Some(code_point)); - None - } - 0xDC00..=0xDFFF => { - if let Some(high_surrogate) = self.state.pending_surrogate.take() { - // Low surrogate, combine with pending high surrogate - String::from_utf16(&[high_surrogate, code_point]).ok() - } else { - // Invalid low surrogate without a preceding high surrogate - log::warn!( - "Received low surrogate without a preceding high surrogate: {code_point:x}" - ); - None - } - } - _ => { - self.state.pending_surrogate.set(None); - char::from_u32(code_point as u32) - .filter(|c| !c.is_control()) - .map(|c| c.to_string()) - } - } - } - - /// Clear the hidden flag and restore the cursor immediately - fn restore_cursor_after_hide(&self) { - if !self.state.cursor_visible.swap(true, Ordering::Relaxed) { - unsafe { - SetCursor(self.state.current_cursor.get()); - } - } - } - - fn start_tracking_mouse(&self, handle: HWND, flags: TRACKMOUSEEVENT_FLAGS) { - if !self.state.hovered.get() { - self.state.hovered.set(true); - unsafe { - TrackMouseEvent(&mut TRACKMOUSEEVENT { - cbSize: std::mem::size_of::() as u32, - dwFlags: flags, - hwndTrack: handle, - dwHoverTime: HOVER_DEFAULT, - }) - .log_err() - }; - if let Some(mut callback) = self.state.callbacks.hovered_status_change.take() { - callback(true); - self.state - .callbacks - .hovered_status_change - .set(Some(callback)); - } - } - } - - fn with_input_handler(&self, f: F) -> Option - where - F: FnOnce(&mut PlatformInputHandler) -> R, - { - let mut input_handler = self.state.input_handler.take()?; - let result = f(&mut input_handler); - self.state.input_handler.set(Some(input_handler)); - Some(result) - } - - fn with_input_handler_and_scale_factor(&self, f: F) -> Option - where - F: FnOnce(&mut PlatformInputHandler, f32) -> Option, - { - let mut input_handler = self.state.input_handler.take()?; - let scale_factor = self.state.scale_factor.get(); - - let result = f(&mut input_handler, scale_factor); - self.state.input_handler.set(Some(input_handler)); - result - } -} - -struct ImeContext { - hwnd: HWND, - himc: HIMC, -} - -impl ImeContext { - fn get(hwnd: HWND) -> Option { - let himc = unsafe { ImmGetContext(hwnd) }; - if himc.is_invalid() { - return None; - } - Some(Self { hwnd, himc }) - } -} - -impl std::ops::Deref for ImeContext { - type Target = HIMC; - fn deref(&self) -> &HIMC { - &self.himc - } -} - -impl Drop for ImeContext { - fn drop(&mut self) { - unsafe { - ImmReleaseContext(self.hwnd, self.himc).ok().log_err(); - } - } -} - -fn handle_key_event( - wparam: WPARAM, - lparam: LPARAM, - state: &WindowsWindowState, - f: F, -) -> Option -where - F: FnOnce(Keystroke, bool) -> PlatformInput, -{ - let virtual_key = VIRTUAL_KEY(wparam.loword()); - let modifiers = current_modifiers(); - - match virtual_key { - VK_SHIFT | VK_CONTROL | VK_MENU | VK_LMENU | VK_RMENU | VK_LWIN | VK_RWIN => { - if state - .last_reported_modifiers - .get() - .is_some_and(|prev_modifiers| prev_modifiers == modifiers) - { - return None; - } - state.last_reported_modifiers.set(Some(modifiers)); - Some(PlatformInput::ModifiersChanged(ModifiersChangedEvent { - modifiers, - capslock: current_capslock(), - })) - } - VK_PACKET => None, - VK_CAPITAL => { - let capslock = current_capslock(); - if state - .last_reported_capslock - .get() - .is_some_and(|prev_capslock| prev_capslock == capslock) - { - return None; - } - state.last_reported_capslock.set(Some(capslock)); - Some(PlatformInput::ModifiersChanged(ModifiersChangedEvent { - modifiers, - capslock, - })) - } - vkey => { - let keystroke = parse_normal_key(vkey, lparam, modifiers)?; - Some(f(keystroke.0, keystroke.1)) - } - } -} - -fn parse_immutable(vkey: VIRTUAL_KEY) -> Option { - Some( - match vkey { - VK_SPACE => "space", - VK_BACK => "backspace", - VK_RETURN => "enter", - VK_TAB => "tab", - VK_UP => "up", - VK_DOWN => "down", - VK_RIGHT => "right", - VK_LEFT => "left", - VK_HOME => "home", - VK_END => "end", - VK_PRIOR => "pageup", - VK_NEXT => "pagedown", - VK_BROWSER_BACK => "back", - VK_BROWSER_FORWARD => "forward", - VK_ESCAPE => "escape", - VK_INSERT => "insert", - VK_DELETE => "delete", - VK_APPS => "menu", - VK_F1 => "f1", - VK_F2 => "f2", - VK_F3 => "f3", - VK_F4 => "f4", - VK_F5 => "f5", - VK_F6 => "f6", - VK_F7 => "f7", - VK_F8 => "f8", - VK_F9 => "f9", - VK_F10 => "f10", - VK_F11 => "f11", - VK_F12 => "f12", - VK_F13 => "f13", - VK_F14 => "f14", - VK_F15 => "f15", - VK_F16 => "f16", - VK_F17 => "f17", - VK_F18 => "f18", - VK_F19 => "f19", - VK_F20 => "f20", - VK_F21 => "f21", - VK_F22 => "f22", - VK_F23 => "f23", - VK_F24 => "f24", - _ => return None, - } - .to_string(), - ) -} - -fn parse_normal_key( - vkey: VIRTUAL_KEY, - lparam: LPARAM, - mut modifiers: Modifiers, -) -> Option<(Keystroke, bool)> { - let (key_char, prefer_character_input) = process_key(vkey, lparam.hiword()); - - let key = parse_immutable(vkey).or_else(|| { - let scan_code = lparam.hiword() & 0xFF; - get_keystroke_key(vkey, scan_code as u32, &mut modifiers) - })?; - - Some(( - Keystroke { - modifiers, - key, - key_char, - }, - prefer_character_input, - )) -} - -fn process_key(vkey: VIRTUAL_KEY, scan_code: u16) -> (Option, bool) { - let mut keyboard_state = [0u8; 256]; - unsafe { - if GetKeyboardState(&mut keyboard_state).is_err() { - return (None, false); - } - } - - let mut buffer_c = [0u16; 8]; - let result_c = unsafe { - ToUnicode( - vkey.0 as u32, - scan_code as u32, - Some(&keyboard_state), - &mut buffer_c, - 0x4, - ) - }; - - if result_c == 0 { - return (None, false); - } - - let c = &buffer_c[..result_c.unsigned_abs() as usize]; - let key_char = String::from_utf16(c) - .ok() - .filter(|s| !s.is_empty() && !s.chars().next().unwrap().is_control()); - - if result_c < 0 { - return (key_char, true); - } - - if key_char.is_none() { - return (None, false); - } - - // Workaround for some bug that makes the compiler think keyboard_state is still zeroed out - let keyboard_state = std::hint::black_box(keyboard_state); - let ctrl_down = (keyboard_state[VK_CONTROL.0 as usize] & 0x80) != 0; - let alt_down = (keyboard_state[VK_MENU.0 as usize] & 0x80) != 0; - let win_down = (keyboard_state[VK_LWIN.0 as usize] & 0x80) != 0 - || (keyboard_state[VK_RWIN.0 as usize] & 0x80) != 0; - - let has_modifiers = ctrl_down || alt_down || win_down; - if !has_modifiers { - return (key_char, false); - } - - let mut state_no_modifiers = keyboard_state; - state_no_modifiers[VK_CONTROL.0 as usize] = 0; - state_no_modifiers[VK_LCONTROL.0 as usize] = 0; - state_no_modifiers[VK_RCONTROL.0 as usize] = 0; - state_no_modifiers[VK_MENU.0 as usize] = 0; - state_no_modifiers[VK_LMENU.0 as usize] = 0; - state_no_modifiers[VK_RMENU.0 as usize] = 0; - state_no_modifiers[VK_LWIN.0 as usize] = 0; - state_no_modifiers[VK_RWIN.0 as usize] = 0; - - let mut buffer_c_no_modifiers = [0u16; 8]; - let result_c_no_modifiers = unsafe { - ToUnicode( - vkey.0 as u32, - scan_code as u32, - Some(&state_no_modifiers), - &mut buffer_c_no_modifiers, - 0x4, - ) - }; - - let c_no_modifiers = &buffer_c_no_modifiers[..result_c_no_modifiers.unsigned_abs() as usize]; - ( - key_char, - result_c != result_c_no_modifiers || c != c_no_modifiers, - ) -} - -fn parse_ime_composition_string(ctx: HIMC, comp_type: IME_COMPOSITION_STRING) -> Option> { - unsafe { - let string_len = ImmGetCompositionStringW(ctx, comp_type, None, 0); - if string_len >= 0 { - let mut buffer = vec![0u8; string_len as usize + 2]; - ImmGetCompositionStringW( - ctx, - comp_type, - Some(buffer.as_mut_ptr() as _), - string_len as _, - ); - let wstring = std::slice::from_raw_parts::( - buffer.as_mut_ptr().cast::(), - string_len as usize / 2, - ); - Some(wstring.to_vec()) - } else { - None - } - } -} - -#[inline] -fn retrieve_composition_cursor_position(ctx: HIMC) -> usize { - unsafe { ImmGetCompositionStringW(ctx, GCS_CURSORPOS, None, 0) as usize } -} - -fn should_use_ime_cursor_position(ctx: HIMC, cursor_pos: usize) -> bool { - let attrs_size = unsafe { ImmGetCompositionStringW(ctx, GCS_COMPATTR, None, 0) } as usize; - if attrs_size == 0 { - return false; - } - - let mut attrs = vec![0u8; attrs_size]; - let result = unsafe { - ImmGetCompositionStringW( - ctx, - GCS_COMPATTR, - Some(attrs.as_mut_ptr() as *mut _), - attrs_size as u32, - ) - }; - if result <= 0 { - return false; - } - - // Keep the cursor adjacent to the inserted text by only using the suggested position - // if it's adjacent to unconverted text. - let at_cursor_is_input = cursor_pos < attrs.len() && attrs[cursor_pos] == (ATTR_INPUT as u8); - let before_cursor_is_input = cursor_pos > 0 - && (cursor_pos - 1) < attrs.len() - && attrs[cursor_pos - 1] == (ATTR_INPUT as u8); - - at_cursor_is_input || before_cursor_is_input -} - -#[inline] -fn is_virtual_key_pressed(vkey: VIRTUAL_KEY) -> bool { - unsafe { GetKeyState(vkey.0 as i32) < 0 } -} - -#[inline] -pub(crate) fn current_modifiers() -> Modifiers { - Modifiers { - control: is_virtual_key_pressed(VK_CONTROL), - alt: is_virtual_key_pressed(VK_MENU), - shift: is_virtual_key_pressed(VK_SHIFT), - platform: is_virtual_key_pressed(VK_LWIN) || is_virtual_key_pressed(VK_RWIN), - function: false, - } -} - -#[inline] -pub(crate) fn current_capslock() -> Capslock { - let on = unsafe { GetKeyState(VK_CAPITAL.0 as i32) & 1 } > 0; - Capslock { on } -} - -// there is some additional non-visible space when talking about window -// borders on Windows: -// - SM_CXSIZEFRAME: The resize handle. -// - SM_CXPADDEDBORDER: Additional border space that isn't part of the resize handle. -fn get_frame_thicknessx(dpi: u32) -> i32 { - let resize_frame_thickness = unsafe { GetSystemMetricsForDpi(SM_CXSIZEFRAME, dpi) }; - let padding_thickness = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) }; - resize_frame_thickness + padding_thickness -} - -fn get_frame_thicknessy(dpi: u32) -> i32 { - let resize_frame_thickness = unsafe { GetSystemMetricsForDpi(SM_CYSIZEFRAME, dpi) }; - let padding_thickness = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) }; - resize_frame_thickness + padding_thickness -} - -fn notify_frame_changed(handle: HWND) { - unsafe { - SetWindowPos( - handle, - None, - 0, - 0, - 0, - 0, - SWP_FRAMECHANGED - | SWP_NOACTIVATE - | SWP_NOCOPYBITS - | SWP_NOMOVE - | SWP_NOOWNERZORDER - | SWP_NOREPOSITION - | SWP_NOSENDCHANGING - | SWP_NOSIZE - | SWP_NOZORDER, - ) - .log_err(); - } -} diff --git a/crates/gpui_pre_windows/src/gpui_windows.rs b/crates/gpui_pre_windows/src/gpui_windows.rs deleted file mode 100644 index 692d0db..0000000 --- a/crates/gpui_pre_windows/src/gpui_windows.rs +++ /dev/null @@ -1,42 +0,0 @@ -#![cfg(target_os = "windows")] - -mod clipboard; -mod destination_list; -mod direct_manipulation; -mod direct_write; -mod directx_atlas; -mod directx_devices; -mod directx_renderer; -mod dispatcher; -mod display; -mod events; -mod keyboard; -mod platform; -mod system_notifications; -mod system_settings; -mod util; -mod vsync; -mod window; -mod wrapper; - -pub(crate) use clipboard::*; -pub(crate) use destination_list::*; -pub(crate) use direct_write::*; -pub(crate) use directx_atlas::*; -pub(crate) use directx_devices::*; -pub(crate) use directx_renderer::*; -pub(crate) use dispatcher::*; -pub(crate) use display::*; -pub(crate) use events::*; -pub(crate) use keyboard::*; -pub(crate) use platform::*; -pub(crate) use system_notifications::*; -pub(crate) use system_settings::*; -pub(crate) use util::*; -pub(crate) use vsync::*; -pub(crate) use window::*; -pub(crate) use wrapper::*; - -pub use platform::WindowsPlatform; - -pub(crate) use windows::Win32::Foundation::HWND; diff --git a/crates/gpui_pre_windows/src/keyboard.rs b/crates/gpui_pre_windows/src/keyboard.rs deleted file mode 100644 index 8164bc1..0000000 --- a/crates/gpui_pre_windows/src/keyboard.rs +++ /dev/null @@ -1,371 +0,0 @@ -use anyhow::Result; -use collections::HashMap; -use windows::Win32::UI::{ - Input::KeyboardAndMouse::{ - GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MapVirtualKeyW, ToUnicode, - VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, - VK_CONTROL, VK_MENU, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, - VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT, - }, - WindowsAndMessaging::KL_NAMELENGTH, -}; - -use gpui::{ - KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper, -}; - -pub(crate) struct WindowsKeyboardLayout { - id: String, - name: String, -} - -pub(crate) struct WindowsKeyboardMapper { - key_to_vkey: HashMap, - vkey_to_key: HashMap, - vkey_to_shifted: HashMap, -} - -impl PlatformKeyboardLayout for WindowsKeyboardLayout { - fn id(&self) -> &str { - &self.id - } - - fn name(&self) -> &str { - &self.name - } -} - -impl PlatformKeyboardMapper for WindowsKeyboardMapper { - fn map_key_equivalent( - &self, - mut keystroke: Keystroke, - use_key_equivalents: bool, - ) -> KeybindingKeystroke { - let Some((vkey, shifted_key)) = self.get_vkey_from_key(&keystroke.key, use_key_equivalents) - else { - return KeybindingKeystroke::from_keystroke(keystroke); - }; - if shifted_key && keystroke.modifiers.shift { - log::warn!( - "Keystroke '{}' has both shift and a shifted key, this is likely a bug", - keystroke.key - ); - } - - let shift = shifted_key || keystroke.modifiers.shift; - keystroke.modifiers.shift = false; - - let Some(key) = self.vkey_to_key.get(&vkey).cloned() else { - log::error!( - "Failed to map key equivalent '{:?}' to a valid key", - keystroke - ); - return KeybindingKeystroke::from_keystroke(keystroke); - }; - - keystroke.key = if shift { - let Some(shifted_key) = self.vkey_to_shifted.get(&vkey).cloned() else { - log::error!( - "Failed to map keystroke {:?} with virtual key '{:?}' to a shifted key", - keystroke, - vkey - ); - return KeybindingKeystroke::from_keystroke(keystroke); - }; - shifted_key - } else { - key.clone() - }; - - let modifiers = Modifiers { - shift, - ..keystroke.modifiers - }; - - KeybindingKeystroke::new(keystroke, modifiers, key) - } - - fn get_key_equivalents(&self) -> Option<&HashMap> { - None - } -} - -impl WindowsKeyboardLayout { - pub(crate) fn new() -> Result { - let mut buffer = [0u16; KL_NAMELENGTH as usize]; // KL_NAMELENGTH includes the null terminator - unsafe { GetKeyboardLayoutNameW(&mut buffer)? }; - let id = String::from_utf16_lossy(&buffer[..buffer.len() - 1]); // Remove the null terminator - let entry = windows_registry::LOCAL_MACHINE.open(format!( - "System\\CurrentControlSet\\Control\\Keyboard Layouts\\{id}" - ))?; - let name = entry.get_string("Layout Text")?; - Ok(Self { id, name }) - } - - pub(crate) fn unknown() -> Self { - Self { - id: "unknown".to_string(), - name: "unknown".to_string(), - } - } -} - -impl WindowsKeyboardMapper { - pub(crate) fn new() -> Self { - let mut key_to_vkey = HashMap::default(); - let mut vkey_to_key = HashMap::default(); - let mut vkey_to_shifted = HashMap::default(); - for vkey in CANDIDATE_VKEYS { - if let Some(key) = get_key_from_vkey(*vkey) { - key_to_vkey.insert(key.clone(), (vkey.0, false)); - vkey_to_key.insert(vkey.0, key); - } - let scan_code = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_VSC) }; - if scan_code == 0 { - continue; - } - if let Some(shifted_key) = get_shifted_key(*vkey, scan_code) { - key_to_vkey.insert(shifted_key.clone(), (vkey.0, true)); - vkey_to_shifted.insert(vkey.0, shifted_key); - } - } - Self { - key_to_vkey, - vkey_to_key, - vkey_to_shifted, - } - } - - fn get_vkey_from_key(&self, key: &str, use_key_equivalents: bool) -> Option<(u16, bool)> { - if use_key_equivalents { - get_vkey_from_key_with_us_layout(key) - } else { - self.key_to_vkey.get(key).cloned() - } - } -} - -pub(crate) fn get_keystroke_key( - vkey: VIRTUAL_KEY, - scan_code: u32, - modifiers: &mut Modifiers, -) -> Option { - if modifiers.shift && need_to_convert_to_shifted_key(vkey) { - get_shifted_key(vkey, scan_code).inspect(|_| { - modifiers.shift = false; - }) - } else { - get_key_from_vkey(vkey) - } -} - -fn get_key_from_vkey(vkey: VIRTUAL_KEY) -> Option { - let key_data = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_CHAR) }; - if key_data == 0 { - return None; - } - - // The high word contains dead key flag, the low word contains the character - let key = char::from_u32(key_data & 0xFFFF)?; - - Some(key.to_ascii_lowercase().to_string()) -} - -#[inline] -fn need_to_convert_to_shifted_key(vkey: VIRTUAL_KEY) -> bool { - matches!( - vkey, - VK_OEM_3 - | VK_OEM_MINUS - | VK_OEM_PLUS - | VK_OEM_4 - | VK_OEM_5 - | VK_OEM_6 - | VK_OEM_1 - | VK_OEM_7 - | VK_OEM_COMMA - | VK_OEM_PERIOD - | VK_OEM_2 - | VK_OEM_102 - | VK_OEM_8 - | VK_ABNT_C1 - | VK_0 - | VK_1 - | VK_2 - | VK_3 - | VK_4 - | VK_5 - | VK_6 - | VK_7 - | VK_8 - | VK_9 - ) -} - -fn get_shifted_key(vkey: VIRTUAL_KEY, scan_code: u32) -> Option { - generate_key_char(vkey, scan_code, false, true, false) -} - -pub(crate) fn generate_key_char( - vkey: VIRTUAL_KEY, - scan_code: u32, - control: bool, - shift: bool, - alt: bool, -) -> Option { - let mut state = [0; 256]; - if control { - state[VK_CONTROL.0 as usize] = 0x80; - } - if shift { - state[VK_SHIFT.0 as usize] = 0x80; - } - if alt { - state[VK_MENU.0 as usize] = 0x80; - } - - let mut buffer = [0; 8]; - let len = unsafe { ToUnicode(vkey.0 as u32, scan_code, Some(&state), &mut buffer, 0x5) }; - - match len { - len if len > 0 => String::from_utf16(&buffer[..len as usize]) - .ok() - .filter(|candidate| { - !candidate.is_empty() && !candidate.chars().next().unwrap().is_control() - }), - len if len < 0 => String::from_utf16(&buffer[..(-len as usize)]).ok(), - _ => None, - } -} - -fn get_vkey_from_key_with_us_layout(key: &str) -> Option<(u16, bool)> { - match key { - // ` => VK_OEM_3 - "`" => Some((VK_OEM_3.0, false)), - "~" => Some((VK_OEM_3.0, true)), - "1" => Some((VK_1.0, false)), - "!" => Some((VK_1.0, true)), - "2" => Some((VK_2.0, false)), - "@" => Some((VK_2.0, true)), - "3" => Some((VK_3.0, false)), - "#" => Some((VK_3.0, true)), - "4" => Some((VK_4.0, false)), - "$" => Some((VK_4.0, true)), - "5" => Some((VK_5.0, false)), - "%" => Some((VK_5.0, true)), - "6" => Some((VK_6.0, false)), - "^" => Some((VK_6.0, true)), - "7" => Some((VK_7.0, false)), - "&" => Some((VK_7.0, true)), - "8" => Some((VK_8.0, false)), - "*" => Some((VK_8.0, true)), - "9" => Some((VK_9.0, false)), - "(" => Some((VK_9.0, true)), - "0" => Some((VK_0.0, false)), - ")" => Some((VK_0.0, true)), - "-" => Some((VK_OEM_MINUS.0, false)), - "_" => Some((VK_OEM_MINUS.0, true)), - "=" => Some((VK_OEM_PLUS.0, false)), - "+" => Some((VK_OEM_PLUS.0, true)), - "[" => Some((VK_OEM_4.0, false)), - "{" => Some((VK_OEM_4.0, true)), - "]" => Some((VK_OEM_6.0, false)), - "}" => Some((VK_OEM_6.0, true)), - "\\" => Some((VK_OEM_5.0, false)), - "|" => Some((VK_OEM_5.0, true)), - ";" => Some((VK_OEM_1.0, false)), - ":" => Some((VK_OEM_1.0, true)), - "'" => Some((VK_OEM_7.0, false)), - "\"" => Some((VK_OEM_7.0, true)), - "," => Some((VK_OEM_COMMA.0, false)), - "<" => Some((VK_OEM_COMMA.0, true)), - "." => Some((VK_OEM_PERIOD.0, false)), - ">" => Some((VK_OEM_PERIOD.0, true)), - "/" => Some((VK_OEM_2.0, false)), - "?" => Some((VK_OEM_2.0, true)), - _ => None, - } -} - -const CANDIDATE_VKEYS: &[VIRTUAL_KEY] = &[ - VK_OEM_3, - VK_OEM_MINUS, - VK_OEM_PLUS, - VK_OEM_4, - VK_OEM_5, - VK_OEM_6, - VK_OEM_1, - VK_OEM_7, - VK_OEM_COMMA, - VK_OEM_PERIOD, - VK_OEM_2, - VK_OEM_102, - VK_OEM_8, - VK_ABNT_C1, - VK_0, - VK_1, - VK_2, - VK_3, - VK_4, - VK_5, - VK_6, - VK_7, - VK_8, - VK_9, -]; - -#[cfg(test)] -mod tests { - use crate::WindowsKeyboardMapper; - use gpui::{Keystroke, Modifiers, PlatformKeyboardMapper}; - - #[test] - fn test_keyboard_mapper() { - let mapper = WindowsKeyboardMapper::new(); - - // Normal case - let keystroke = Keystroke { - modifiers: Modifiers::control(), - key: "a".to_string(), - key_char: None, - }; - let mapped = mapper.map_key_equivalent(keystroke.clone(), true); - assert_eq!(*mapped.inner(), keystroke); - assert_eq!(mapped.key(), "a"); - assert_eq!(*mapped.modifiers(), Modifiers::control()); - - // Shifted case, ctrl-$ - let keystroke = Keystroke { - modifiers: Modifiers::control(), - key: "$".to_string(), - key_char: None, - }; - let mapped = mapper.map_key_equivalent(keystroke.clone(), true); - assert_eq!(*mapped.inner(), keystroke); - assert_eq!(mapped.key(), "4"); - assert_eq!(*mapped.modifiers(), Modifiers::control_shift()); - - // Shifted case, but shift is true - let keystroke = Keystroke { - modifiers: Modifiers::control_shift(), - key: "$".to_string(), - key_char: None, - }; - let mapped = mapper.map_key_equivalent(keystroke, true); - assert_eq!(mapped.inner().modifiers, Modifiers::control()); - assert_eq!(mapped.key(), "4"); - assert_eq!(*mapped.modifiers(), Modifiers::control_shift()); - - // Windows style - let keystroke = Keystroke { - modifiers: Modifiers::control_shift(), - key: "4".to_string(), - key_char: None, - }; - let mapped = mapper.map_key_equivalent(keystroke, true); - assert_eq!(mapped.inner().modifiers, Modifiers::control()); - assert_eq!(mapped.inner().key, "$"); - assert_eq!(mapped.key(), "4"); - assert_eq!(*mapped.modifiers(), Modifiers::control_shift()); - } -} diff --git a/crates/gpui_pre_windows/src/platform.rs b/crates/gpui_pre_windows/src/platform.rs deleted file mode 100644 index d0ae3c7..0000000 --- a/crates/gpui_pre_windows/src/platform.rs +++ /dev/null @@ -1,1605 +0,0 @@ -use std::{ - cell::{Cell, RefCell}, - ffi::{OsStr, OsString}, - os::windows::ffi::{OsStrExt as _, OsStringExt as _}, - path::{Path, PathBuf}, - rc::{Rc, Weak}, - sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - }, -}; - -use anyhow::{Context as _, Result, anyhow}; -use futures::channel::oneshot::{self, Receiver}; -use gpui_util::{ResultExt, get_powershell, new_std_command}; -use itertools::Itertools; -use parking_lot::RwLock; -use smallvec::SmallVec; -use windows::{ - UI::ViewManagement::UISettings, - Win32::{ - Foundation::*, - Graphics::{Direct3D11::ID3D11Device, Gdi::*}, - Security::Credentials::*, - System::{Com::*, LibraryLoader::*, Ole::*, Power::*, SystemInformation::*}, - UI::{Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*}, - }, - core::*, -}; - -use crate::*; -use gpui::*; - -pub struct WindowsPlatform { - inner: Rc, - raw_window_handles: Arc>>, - // The below members will never change throughout the entire lifecycle of the app. - headless: bool, - icon: HICON, - background_executor: BackgroundExecutor, - foreground_executor: ForegroundExecutor, - text_system: Arc, - direct_write_text_system: Option>, - drop_target_helper: Option, - /// Flag to instruct the `VSyncProvider` thread to invalidate the directx devices - /// as resizing them has failed, causing us to have lost at least the render target. - invalidate_devices: Arc, - handle: HWND, - suspend_resume_notification: RefCell>, - disable_direct_composition: bool, - has_package_identity: bool, - app_identity: RefCell>, - system_notifications: RefCell, -} - -struct WindowsPlatformInner { - state: WindowsPlatformState, - raw_window_handles: std::sync::Weak>>, - // The below members will never change throughout the entire lifecycle of the app. - validation_number: usize, - main_receiver: PriorityQueueReceiver, - dispatcher: Arc, -} - -pub(crate) struct WindowsPlatformState { - callbacks: PlatformCallbacks, - menus: RefCell>, - jump_list: RefCell, - // NOTE: standard cursor handles don't need to close. - pub(crate) current_cursor: Cell>, - /// Shared with each window so `WM_SETCURSOR` can read it directly. - pub(crate) cursor_visible: Arc, - /// Shared with each window to coordinate draws across windows on the UI - /// thread; see [`DrawCoordinator`]. - pub(crate) draw_coordinator: Rc, - directx_devices: RefCell>, -} - -#[derive(Default)] -struct PlatformCallbacks { - open_urls: Cell)>>>, - quit: Cell bool>>>, - reopen: Cell>>, - app_menu_action: Cell>>, - will_open_app_menu: Cell>>, - validate_app_menu_command: Cell bool>>>, - keyboard_layout_change: Cell>>, - system_wake: Cell>>, -} - -impl WindowsPlatformState { - fn new(directx_devices: Option) -> Self { - let callbacks = PlatformCallbacks::default(); - let jump_list = JumpList::new(); - let current_cursor = load_cursor(CursorStyle::Arrow); - - Self { - callbacks, - jump_list: RefCell::new(jump_list), - current_cursor: Cell::new(current_cursor), - cursor_visible: Arc::new(AtomicBool::new(true)), - draw_coordinator: Rc::new(DrawCoordinator::new()), - directx_devices: RefCell::new(directx_devices), - menus: RefCell::new(Vec::new()), - } - } -} - -impl WindowsPlatform { - pub fn new(headless: bool) -> Result { - unsafe { - OleInitialize(None).context("unable to initialize Windows OLE")?; - } - let (directx_devices, text_system, direct_write_text_system) = if !headless { - let devices = DirectXDevices::new().context("Creating DirectX devices")?; - let dw_text_system = Arc::new( - DirectWriteTextSystem::new(&devices) - .context("Error creating DirectWriteTextSystem")?, - ); - ( - Some(devices), - dw_text_system.clone() as Arc, - Some(dw_text_system), - ) - } else { - ( - None, - Arc::new(gpui::NoopTextSystem::new()) as Arc, - None, - ) - }; - - let (main_sender, main_receiver) = PriorityQueueReceiver::new(); - let validation_number = if usize::BITS == 64 { - rand::random::() as usize - } else { - rand::random::() as usize - }; - let raw_window_handles = Arc::new(RwLock::new(SmallVec::new())); - - register_platform_window_class(); - let mut context = PlatformWindowCreateContext { - inner: None, - raw_window_handles: Arc::downgrade(&raw_window_handles), - validation_number, - main_sender: Some(main_sender), - main_receiver: Some(main_receiver), - directx_devices, - dispatcher: None, - }; - let result = unsafe { - CreateWindowExW( - WINDOW_EX_STYLE(0), - PLATFORM_WINDOW_CLASS_NAME, - None, - WINDOW_STYLE(0), - 0, - 0, - 0, - 0, - Some(HWND_MESSAGE), - None, - None, - Some(&raw const context as *const _), - ) - }; - let inner = context - .inner - .take() - .context("CreateWindowExW did not run correctly")??; - let dispatcher = context - .dispatcher - .take() - .context("CreateWindowExW did not run correctly")?; - let handle = result?; - - let disable_direct_composition = std::env::var(DISABLE_DIRECT_COMPOSITION) - .is_ok_and(|value| value == "true" || value == "1"); - let background_executor = BackgroundExecutor::new(dispatcher.clone()); - let foreground_executor = ForegroundExecutor::new(dispatcher); - - let drop_target_helper: Option = if !headless { - Some(unsafe { - CoCreateInstance(&CLSID_DragDropHelper, None, CLSCTX_INPROC_SERVER) - .context("Error creating drop target helper.")? - }) - } else { - None - }; - let icon = if !headless { - load_icon().unwrap_or_default() - } else { - HICON::default() - }; - - Ok(Self { - inner, - handle, - raw_window_handles, - headless, - icon, - background_executor, - foreground_executor, - text_system, - direct_write_text_system, - suspend_resume_notification: RefCell::new(None), - disable_direct_composition, - has_package_identity: has_package_identity(), - drop_target_helper, - invalidate_devices: Arc::new(AtomicBool::new(false)), - app_identity: RefCell::new(None), - system_notifications: RefCell::new(SystemNotificationState::new()), - }) - } - - pub(crate) fn window_from_hwnd(&self, hwnd: HWND) -> Option> { - self.raw_window_handles - .read() - .iter() - .find(|entry| entry.as_raw() == hwnd) - .and_then(|hwnd| window_from_hwnd(hwnd.as_raw())) - } - - #[inline] - fn post_message(&self, message: u32, wparam: WPARAM, lparam: LPARAM) { - self.raw_window_handles - .read() - .iter() - .for_each(|handle| unsafe { - PostMessageW(Some(handle.as_raw()), message, wparam, lparam).log_err(); - }); - } - - fn generate_creation_info(&self) -> WindowCreationInfo { - WindowCreationInfo { - icon: self.icon, - executor: self.foreground_executor.clone(), - current_cursor: self.inner.state.current_cursor.get(), - cursor_visible: self.inner.state.cursor_visible.clone(), - drop_target_helper: self.drop_target_helper.clone().unwrap(), - validation_number: self.inner.validation_number, - main_receiver: self.inner.main_receiver.clone(), - platform_window_handle: self.handle, - disable_direct_composition: self.disable_direct_composition, - directx_devices: self.inner.state.directx_devices.borrow().clone().unwrap(), - invalidate_devices: self.invalidate_devices.clone(), - draw_coordinator: self.inner.state.draw_coordinator.clone(), - } - } - - fn set_dock_menus(&self, menus: Vec) { - let mut actions = Vec::new(); - menus.into_iter().for_each(|menu| { - if let Some(dock_menu) = DockMenuItem::new(menu).log_err() { - actions.push(dock_menu); - } - }); - self.inner.state.jump_list.borrow_mut().dock_menus = actions; - let borrow = self.inner.state.jump_list.borrow(); - let dock_menus = borrow - .dock_menus - .iter() - .map(|menu| (menu.name.clone(), menu.description.clone())) - .collect::>(); - let recent_workspaces = borrow.recent_workspaces.clone(); - self.background_executor - .spawn(async move { - update_jump_list(&recent_workspaces, &dock_menus).log_err(); - }) - .detach(); - } - - fn update_jump_list( - &self, - menus: Vec, - entries: Vec>, - ) -> Task>> { - let mut actions = Vec::new(); - menus.into_iter().for_each(|menu| { - if let Some(dock_menu) = DockMenuItem::new(menu).log_err() { - actions.push(dock_menu); - } - }); - let mut jump_list = self.inner.state.jump_list.borrow_mut(); - jump_list.dock_menus = actions; - jump_list.recent_workspaces = entries.into(); - let dock_menus = jump_list - .dock_menus - .iter() - .map(|menu| (menu.name.clone(), menu.description.clone())) - .collect::>(); - let recent_workspaces = jump_list.recent_workspaces.clone(); - self.background_executor.spawn(async move { - update_jump_list(&recent_workspaces, &dock_menus) - .log_err() - .unwrap_or_default() - }) - } - - fn find_current_active_window(&self) -> Option { - let active_window_hwnd = unsafe { GetActiveWindow() }; - if active_window_hwnd.is_invalid() { - return None; - } - self.raw_window_handles - .read() - .iter() - .find(|hwnd| hwnd.as_raw() == active_window_hwnd) - .map(|hwnd| hwnd.as_raw()) - } - - fn begin_vsync_thread(&self) { - let Some(directx_devices) = self.inner.state.directx_devices.borrow().clone() else { - return; - }; - let Some(direct_write_text_system) = &self.direct_write_text_system else { - return; - }; - let mut directx_device = directx_devices; - let platform_window: SafeHwnd = self.handle.into(); - let validation_number = self.inner.validation_number; - let all_windows = Arc::downgrade(&self.raw_window_handles); - let text_system = Arc::downgrade(direct_write_text_system); - let invalidate_devices = self.invalidate_devices.clone(); - - std::thread::Builder::new() - .name("VSyncProvider".to_owned()) - .spawn(move || { - let vsync_provider = VSyncProvider::new(); - loop { - vsync_provider.wait_for_vsync(); - if check_device_lost(&directx_device.device) - || invalidate_devices.fetch_and(false, Ordering::Acquire) - { - if let Err(err) = handle_gpu_device_lost( - &mut directx_device, - platform_window.as_raw(), - validation_number, - &all_windows, - &text_system, - ) { - panic!("Device lost: {err}"); - } - } - let Some(all_windows) = all_windows.upgrade() else { - break; - }; - for hwnd in all_windows.read().iter() { - unsafe { - let _ = RedrawWindow(Some(hwnd.as_raw()), None, None, RDW_INVALIDATE); - } - } - } - }) - .unwrap(); - } -} - -fn translate_accelerator(msg: &MSG) -> Option<()> { - if msg.message != WM_KEYDOWN && msg.message != WM_SYSKEYDOWN { - return None; - } - - let result = unsafe { - SendMessageW( - msg.hwnd, - WM_GPUI_KEYDOWN, - Some(msg.wParam), - Some(msg.lParam), - ) - }; - (result.0 == 0).then_some(()) -} - -fn encode_restart_arguments(arguments: &[OsString]) -> OsString { - // `Start-Process` accepts a single native command line, so quote each argument according to - // the Windows argv parsing rules before passing the complete string through the environment. - let mut encoded = Vec::new(); - - for (index, argument) in arguments.iter().enumerate() { - if index > 0 { - encoded.push(b' ' as u16); - } - encoded.push(b'"' as u16); - - let mut backslash_count = 0; - for code_unit in argument.encode_wide() { - if code_unit == b'\\' as u16 { - backslash_count += 1; - } else { - if code_unit == b'"' as u16 { - encoded.extend(std::iter::repeat_n(b'\\' as u16, backslash_count * 2 + 1)); - } else { - encoded.extend(std::iter::repeat_n(b'\\' as u16, backslash_count)); - } - backslash_count = 0; - encoded.push(code_unit); - } - } - - encoded.extend(std::iter::repeat_n(b'\\' as u16, backslash_count * 2)); - encoded.push(b'"' as u16); - } - - OsString::from_wide(&encoded) -} - -impl Platform for WindowsPlatform { - fn background_executor(&self) -> BackgroundExecutor { - self.background_executor.clone() - } - - fn foreground_executor(&self) -> ForegroundExecutor { - self.foreground_executor.clone() - } - - fn text_system(&self) -> Arc { - self.text_system.clone() - } - - fn keyboard_layout(&self) -> Box { - Box::new( - WindowsKeyboardLayout::new() - .log_err() - .unwrap_or(WindowsKeyboardLayout::unknown()), - ) - } - - fn keyboard_mapper(&self) -> Rc { - Rc::new(WindowsKeyboardMapper::new()) - } - - fn on_keyboard_layout_change(&self, callback: Box) { - self.inner - .state - .callbacks - .keyboard_layout_change - .set(Some(callback)); - } - - fn on_thermal_state_change(&self, _callback: Box) {} - - fn thermal_state(&self) -> ThermalState { - ThermalState::Nominal - } - - fn run(&self, on_finish_launching: Box) { - on_finish_launching(); - if !self.headless { - self.begin_vsync_thread(); - } - - let mut msg = MSG::default(); - unsafe { - while GetMessageW(&mut msg, None, 0, 0).as_bool() { - if translate_accelerator(&msg).is_none() { - _ = TranslateMessage(&msg); - DispatchMessageW(&msg); - } - } - } - - self.inner.with_callback( - |callbacks| &callbacks.quit, - |callback| { - callback(); - }, - ); - } - - fn quit(&self) { - self.foreground_executor() - .spawn(async { unsafe { PostQuitMessage(0) } }) - .detach(); - } - - fn restart(&self, binary_path: Option, arguments: Vec) { - let pid = std::process::id(); - let Some(app_path) = binary_path.or(self.app_path().log_err()) else { - return; - }; - let script = r#" - $pidToWaitFor = $env:ZED_RESTART_PID - $exePath = $env:ZED_RESTART_EXECUTABLE - $argumentList = $env:ZED_RESTART_ARGUMENTS - - [Environment]::SetEnvironmentVariable("ZED_RESTART_PID", $null) - [Environment]::SetEnvironmentVariable("ZED_RESTART_EXECUTABLE", $null) - [Environment]::SetEnvironmentVariable("ZED_RESTART_ARGUMENTS", $null) - - while ($true) { - $process = Get-Process -Id $pidToWaitFor -ErrorAction SilentlyContinue - if (-not $process) { - if ([string]::IsNullOrEmpty($argumentList)) { - Start-Process -FilePath $exePath - } else { - Start-Process -FilePath $exePath -ArgumentList $argumentList - } - break - } - Start-Sleep -Seconds 0.1 - } - "#; - - // Defer spawning to the foreground executor so it runs after the - // current `AppCell` borrow is released. On Windows, `Command::spawn()` - // can pump the Win32 message loop (via `CreateProcessW`), which - // re-enters message handling possibly resulting in another mutable - // borrow of the `AppCell` ending up with a double borrow panic - let Some(powershell) = get_powershell() else { - log::error!("failed to restart: PowerShell is unavailable"); - return; - }; - self.foreground_executor - .spawn(async move { - let mut command = new_std_command(powershell); - let arguments = encode_restart_arguments(&arguments); - command - .arg("-command") - .arg(script) - .env("ZED_RESTART_PID", pid.to_string()) - .env("ZED_RESTART_EXECUTABLE", app_path) - .env("ZED_RESTART_ARGUMENTS", arguments); - #[allow( - clippy::disallowed_methods, - reason = "We are restarting ourselves, using std command thus is fine" - )] - let restart_process = command.spawn(); - - match restart_process { - Ok(_) => unsafe { PostQuitMessage(0) }, - Err(e) => log::error!("failed to spawn restart script: {:?}", e), - } - }) - .detach(); - } - - fn activate(&self, _ignoring_other_apps: bool) {} - - fn hide(&self) {} - - // todo(windows) - fn hide_other_apps(&self) { - unimplemented!() - } - - // todo(windows) - fn unhide_other_apps(&self) { - unimplemented!() - } - - fn displays(&self) -> Vec> { - WindowsDisplay::displays() - } - - fn primary_display(&self) -> Option> { - WindowsDisplay::primary_monitor().map(|display| Rc::new(display) as Rc) - } - - #[cfg(feature = "screen-capture")] - fn is_screen_capture_supported(&self) -> bool { - true - } - - #[cfg(feature = "screen-capture")] - fn screen_capture_sources( - &self, - ) -> oneshot::Receiver>>> { - gpui::scap_screen_capture::scap_screen_sources(&self.foreground_executor) - } - - fn active_window(&self) -> Option { - let active_window_hwnd = unsafe { GetActiveWindow() }; - self.window_from_hwnd(active_window_hwnd) - .map(|inner| inner.handle) - } - - fn open_window( - &self, - handle: AnyWindowHandle, - options: WindowParams, - ) -> Result> { - let window = WindowsWindow::new(handle, options, self.generate_creation_info())?; - let handle = window.get_raw_handle(); - self.raw_window_handles.write().push(handle.into()); - - Ok(Box::new(window)) - } - - fn window_appearance(&self) -> WindowAppearance { - system_appearance().log_err().unwrap_or_default() - } - - fn open_url(&self, url: &str) { - if url.is_empty() { - return; - } - let url_string = url.to_string(); - self.background_executor() - .spawn(async move { - open_target(&url_string) - .with_context(|| format!("Opening url: {}", url_string)) - .log_err(); - }) - .detach(); - } - - fn on_open_urls(&self, callback: Box)>) { - self.inner.state.callbacks.open_urls.set(Some(callback)); - } - - fn prompt_for_paths( - &self, - options: PathPromptOptions, - ) -> Receiver>>> { - let (tx, rx) = oneshot::channel(); - let window = self.find_current_active_window(); - self.foreground_executor() - .spawn(async move { - let _ = tx.send(file_open_dialog(options, window)); - }) - .detach(); - - rx - } - - fn prompt_for_new_path( - &self, - directory: &Path, - suggested_name: Option<&str>, - ) -> Receiver>> { - let directory = directory.to_owned(); - let suggested_name = suggested_name.map(|s| s.to_owned()); - let (tx, rx) = oneshot::channel(); - let window = self.find_current_active_window(); - self.foreground_executor() - .spawn(async move { - let _ = tx.send(file_save_dialog(directory, suggested_name, window)); - }) - .detach(); - - rx - } - - fn can_select_mixed_files_and_dirs(&self) -> bool { - // The FOS_PICKFOLDERS flag toggles between "only files" and "only folders". - false - } - - fn reveal_path(&self, path: &Path) { - if path.as_os_str().is_empty() { - return; - } - let path = path.to_path_buf(); - self.background_executor() - .spawn(async move { - open_target_in_explorer(&path) - .with_context(|| format!("Revealing path {} in explorer", path.display())) - .log_err(); - }) - .detach(); - } - - fn open_with_system(&self, path: &Path) { - if path.as_os_str().is_empty() { - return; - } - let path = path.to_path_buf(); - self.background_executor() - .spawn(async move { - open_target(&path) - .with_context(|| format!("Opening {} with system", path.display())) - .log_err(); - }) - .detach(); - } - - fn on_quit(&self, callback: Box bool>) { - self.inner.state.callbacks.quit.set(Some(callback)); - } - - fn on_reopen(&self, callback: Box) { - self.inner.state.callbacks.reopen.set(Some(callback)); - } - - fn on_system_wake(&self, callback: Box) { - self.inner.state.callbacks.system_wake.set(Some(callback)); - let mut notification = self.suspend_resume_notification.borrow_mut(); - if notification.is_none() { - *notification = unsafe { - // SAFETY: self.handle is the platform window receiving WM_POWERBROADCAST. - RegisterSuspendResumeNotification( - HANDLE(self.handle.0), - DEVICE_NOTIFY_WINDOW_HANDLE, - ) - .log_err() - }; - } - } - - fn set_app_identity(&self, identifier: &str, name: &str) { - // If the process has package identity, it's automatally granted an AUMID by the system. - if self.has_package_identity { - return; - } - - let identifier_utf16 = windows::core::HSTRING::from(identifier); - // SAFETY: `identifier_utf16` outlives the call and is null-terminated. - if let Err(error) = unsafe { - windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID( - windows::core::PCWSTR(identifier_utf16.as_ptr()), - ) - } { - log::warn!("failed to set the process AppUserModelID: {error}"); - } - *self.app_identity.borrow_mut() = Some((identifier.to_string(), name.to_string())); - } - - fn show_system_notification(&self, notification: gpui::SystemNotification) { - let app_identity = self.app_identity.borrow().clone(); - self.system_notifications - .borrow_mut() - .show( - self.has_package_identity, - app_identity - .as_ref() - .map(|(identifier, name)| (identifier.as_str(), name.as_str())), - notification, - ) - .log_err(); - } - - fn dismiss_system_notification(&self, tag: &str) { - self.system_notifications.borrow_mut().dismiss(tag); - } - - fn on_system_notification_response( - &self, - callback: Box, - ) { - self.system_notifications - .borrow_mut() - .on_response(&self.foreground_executor, callback); - } - - fn set_menus(&self, menus: Vec

, - /// One-based scene clip-chain index. Zero uses the inline bounds and radii. - pub clip_index: u32, - /// Explicit GPU record padding. - pub clip_padding: u32, -} - -impl ContentMask { - /// Scale the content mask's pixel units by the given scaling factor. - pub fn scale(&self, factor: f32) -> ContentMask { - ContentMask { - bounds: self.bounds.scale(factor), - corner_radii: self.corner_radii.scale(factor), - clip_index: self.clip_index, - clip_padding: 0, - } - } - - /// Intersect without changing either mask's rounded geometry. - pub fn intersect(&self, other: &Self) -> crate::ClipRegion { - crate::ClipRegion::from(*self).intersect(&crate::ClipRegion::from(*other)) - } -} - -impl Window { - fn mark_view_dirty(&mut self, view_id: EntityId) { - // Mark ancestor views as dirty. If already in the `dirty_views` set, then all its ancestors - // should already be dirty. - for view_id in self - .rendered_frame - .dispatch_tree - .view_path_reversed(view_id) - { - if !self.dirty_views.insert(view_id) { - break; - } - } - } - - /// Registers a callback to be invoked when the window appearance changes. - pub fn observe_window_appearance( - &self, - mut callback: impl FnMut(&mut Window, &mut App) + 'static, - ) -> Subscription { - let (subscription, activate) = self.appearance_observers.insert( - (), - Box::new(move |window, cx| { - callback(window, cx); - true - }), - ); - activate(); - subscription - } - - /// Registers a callback to be invoked when the window button layout changes. - pub fn observe_button_layout_changed( - &self, - mut callback: impl FnMut(&mut Window, &mut App) + 'static, - ) -> Subscription { - let (subscription, activate) = self.button_layout_observers.insert( - (), - Box::new(move |window, cx| { - callback(window, cx); - true - }), - ); - activate(); - subscription - } - - /// Replaces the root entity of the window with a new one. - pub fn replace_root( - &mut self, - cx: &mut App, - build_view: impl FnOnce(&mut Window, &mut Context) -> E, - ) -> Entity - where - E: 'static + Render, - { - let view = cx.new(|cx| build_view(self, cx)); - self.root = Some(view.clone().into()); - self.refresh(); - view - } - - /// Returns the root entity of the window, if it has one. - pub fn root(&self) -> Option>> - where - E: 'static + Render, - { - self.root - .as_ref() - .map(|view| view.clone().downcast::().ok()) - } - - /// Obtain a handle to the window that belongs to this context. - pub fn window_handle(&self) -> AnyWindowHandle { - self.handle - } - - /// Mark the window as dirty, scheduling it to be redrawn on the next frame. - pub fn refresh(&mut self) { - if self.invalidator.not_drawing() { - self.refreshing = true; - self.invalidator.set_dirty(true); - } - } - - /// Close this window. - pub fn remove_window(&mut self) { - self.removed = true; - } - - /// Obtain the currently focused [`FocusHandle`]. If no elements are focused, returns `None`. - pub fn focused(&self, cx: &App) -> Option { - self.focus - .and_then(|id| FocusHandle::for_id(id, &cx.focus_handles)) - } - - /// While focus-lost listeners are being dispatched, returns the closest ancestor of the - /// previously focused element that can still receive focus, making it a suitable target - /// for focus restoration. Returns `None` at all other times, or when no such ancestor exists. - pub fn focus_lost_restore_target(&self, cx: &App) -> Option { - let (_leaf, ancestors) = self.focus_lost_path.split_last()?; - ancestors.iter().rev().find_map(|id| { - self.rendered_frame.dispatch_tree.focusable_node_id(*id)?; - FocusHandle::for_id(*id, &cx.focus_handles) - }) - } - - /// Move focus to the element associated with the given [`FocusHandle`]. - pub fn focus(&mut self, handle: &FocusHandle, cx: &mut App) { - if !self.focus_enabled || self.focus == Some(handle.id) { - return; - } - - self.focus = Some(handle.id); - self.focus_generation = self.focus_generation.wrapping_add(1); - self.clear_pending_keystrokes(cx); - - self.refresh(); - } - - /// Remove focus from all elements within this context's window. - pub fn blur(&mut self, cx: &mut App) { - self.clear_pending_keystrokes(cx); - - if !self.focus_enabled { - return; - } - - if self.focus.is_some() { - self.focus_generation = self.focus_generation.wrapping_add(1); - } - self.focus = None; - self.refresh(); - } - - /// Blur the window and don't allow anything in it to be focused again. - pub fn disable_focus(&mut self, cx: &mut App) { - self.blur(cx); - self.focus_enabled = false; - } - - /// Move focus to next tab stop. - pub fn focus_next(&mut self, cx: &mut App) { - if !self.focus_enabled { - return; - } - - if let Some(handle) = self.rendered_frame.tab_stops.next(self.focus.as_ref()) { - self.focus(&handle, cx) - } - } - - /// Move focus to previous tab stop. - pub fn focus_prev(&mut self, cx: &mut App) { - if !self.focus_enabled { - return; - } - - if let Some(handle) = self.rendered_frame.tab_stops.prev(self.focus.as_ref()) { - self.focus(&handle, cx) - } - } - - /// Accessor for the text system. - pub fn text_system(&self) -> &Arc { - &self.text_system - } - - /// The current text style. Which is composed of all the style refinements provided to `with_text_style`. - pub fn text_style(&self) -> TextStyle { - let mut style = TextStyle::default(); - for refinement in &self.text_style_stack { - style.refine(refinement); - } - style - } - - /// Check if the platform window is maximized. - /// - /// On some platforms (namely Windows) this is different than the bounds being the size of the display - pub fn is_maximized(&self) -> bool { - self.platform_window.is_maximized() - } - - /// request a certain window decoration (Wayland) - pub fn request_decorations(&self, decorations: WindowDecorations) { - self.platform_window.request_decorations(decorations); - } - - /// Set the exclusive zone for a layer-shell surface: how much screen space it - /// reserves so other surfaces avoid occluding it (e.g. a panel reserving space). - /// Positive values reserve that distance from the anchored edge, 0 lets the - /// surface be moved out of others' exclusive zones, and -1 ignores reserved - /// space and may extend under other surfaces. (Wayland layer-shell windows only) - pub fn set_exclusive_zone(&self, zone: Pixels) { - self.platform_window.set_exclusive_zone(zone); - } - - /// Set which anchored edge a layer-shell surface's exclusive zone applies to. - /// This is only needed to disambiguate a corner-anchored surface; otherwise the - /// edge is deduced from the anchor. The edge must be a single edge the surface - /// is anchored to, or it is ignored. (Wayland layer-shell windows only) - #[cfg(all(target_os = "linux", feature = "wayland"))] - pub fn set_exclusive_edge(&self, edge: crate::layer_shell::Anchor) { - self.platform_window.set_exclusive_edge(edge); - } - - /// Start an interactive window resize operation if this window is resizable. - pub fn start_window_resize(&self, edge: ResizeEdge) { - if self.is_resizable { - self.platform_window.start_window_resize(edge); - } - } - - /// Linux (wayland) only: Set the window's input region, the area that receives pointer - /// and touch input. Events outside it pass through to whatever is below the window. - /// - /// - `Some(rects)` restricts input to the union of `rects`, in window coordinates. - /// - `Some(&[])` is an empty region, so the window receives no pointer or touch input. - /// - `None` resets the region to the default, so the whole window receives input again. - pub fn set_input_region(&self, region: Option<&[Bounds]>) { - self.platform_window.set_input_region(region); - } - - /// Return the `WindowBounds` to indicate that how a window should be opened - /// after it has been closed - pub fn window_bounds(&self) -> WindowBounds { - self.platform_window.window_bounds() - } - - /// Return the `WindowBounds` excluding insets (Wayland and X11) - pub fn inner_window_bounds(&self) -> WindowBounds { - self.platform_window.inner_window_bounds() - } - - /// Dispatch the given action on the currently focused element. - pub fn dispatch_action(&mut self, action: Box, cx: &mut App) { - let focus_id = self.focused(cx).map(|handle| handle.id); - - let window = self.handle; - cx.defer(move |cx| { - window - .update(cx, |_, window, cx| { - let node_id = window.focus_node_id_in_rendered_frame(focus_id); - window.dispatch_action_on_node(node_id, action.as_ref(), cx); - }) - .log_err(); - }) - } - - pub(crate) fn dispatch_keystroke_observers( - &mut self, - event: &dyn Any, - action: Option>, - context_stack: Vec, - cx: &mut App, - ) { - let Some(key_down_event) = event.downcast_ref::() else { - return; - }; - - cx.keystroke_observers.clone().retain(&(), move |callback| { - (callback)( - &KeystrokeEvent { - keystroke: key_down_event.keystroke.clone(), - action: action.as_ref().map(|action| action.boxed_clone()), - context_stack: context_stack.clone(), - }, - self, - cx, - ) - }); - } - - pub(crate) fn dispatch_keystroke_interceptors( - &mut self, - event: &dyn Any, - context_stack: Vec, - cx: &mut App, - ) { - let Some(key_down_event) = event.downcast_ref::() else { - return; - }; - - cx.keystroke_interceptors - .clone() - .retain(&(), move |callback| { - (callback)( - &KeystrokeEvent { - keystroke: key_down_event.keystroke.clone(), - action: None, - context_stack: context_stack.clone(), - }, - self, - cx, - ) - }); - } - - /// Schedules the given function to be run at the end of the current effect cycle, allowing entities - /// that are currently on the stack to be returned to the app. - pub fn defer(&self, cx: &mut App, f: impl FnOnce(&mut Window, &mut App) + 'static) { - let handle = self.handle; - cx.defer(move |cx| { - handle.update(cx, |_, window, cx| f(window, cx)).ok(); - }); - } - - /// Subscribe to events emitted by a entity. - /// The entity to which you're subscribing must implement the [`EventEmitter`] trait. - /// The callback will be invoked a handle to the emitting entity, the event, and a window context for the current window. - pub fn observe( - &mut self, - observed: &Entity, - cx: &mut App, - mut on_notify: impl FnMut(Entity, &mut Window, &mut App) + 'static, - ) -> Subscription { - let entity_id = observed.entity_id(); - let observed = observed.downgrade(); - let window_handle = self.handle; - cx.new_observer( - entity_id, - Box::new(move |cx| { - window_handle - .update(cx, |_, window, cx| { - if let Some(handle) = observed.upgrade() { - on_notify(handle, window, cx); - true - } else { - false - } - }) - .unwrap_or(false) - }), - ) - } - - /// Subscribe to events emitted by a entity. - /// The entity to which you're subscribing must implement the [`EventEmitter`] trait. - /// The callback will be invoked a handle to the emitting entity, the event, and a window context for the current window. - pub fn subscribe( - &mut self, - entity: &Entity, - cx: &mut App, - mut on_event: impl FnMut(Entity, &Evt, &mut Window, &mut App) + 'static, - ) -> Subscription - where - Emitter: EventEmitter, - Evt: 'static, - { - let entity_id = entity.entity_id(); - let handle = entity.downgrade(); - let window_handle = self.handle; - cx.new_subscription( - entity_id, - ( - TypeId::of::(), - Box::new(move |event, cx| { - window_handle - .update(cx, |_, window, cx| { - if let Some(entity) = handle.upgrade() { - let event = event.downcast_ref().expect("invalid event type"); - on_event(entity, event, window, cx); - true - } else { - false - } - }) - .unwrap_or(false) - }), - ), - ) - } - - /// Register a callback to be invoked when the given `Entity` is released. - pub fn observe_release( - &self, - entity: &Entity, - cx: &mut App, - mut on_release: impl FnOnce(&mut T, &mut Window, &mut App) + 'static, - ) -> Subscription - where - T: 'static, - { - let entity_id = entity.entity_id(); - let window_handle = self.handle; - let (subscription, activate) = cx.release_listeners.insert( - entity_id, - Box::new(move |entity, cx| { - let entity = entity.downcast_mut().expect("invalid entity type"); - let _ = window_handle.update(cx, |_, window, cx| on_release(entity, window, cx)); - }), - ); - activate(); - subscription - } - - /// Creates an [`AsyncWindowContext`], which has a static lifetime and can be held across - /// await points in async code. - pub fn to_async(&self, cx: &App) -> AsyncWindowContext { - AsyncWindowContext::new_context(cx.to_async(), self.handle) - } - - /// Schedule the given closure to be run directly after the current frame is rendered. - pub fn on_next_frame(&self, callback: impl FnOnce(&mut Window, &mut App) + 'static) { - RefCell::borrow_mut(&self.next_frame_callbacks).push(Box::new(callback)); - self.platform_window.schedule_frame(); - // Next-frame callbacks create frame demand without dirtying the - // window, so the platform's frame source must be woken explicitly. - self.invalidator.wake_platform(); - } - - /// Schedule a frame to be drawn on the next animation frame. - /// - /// This is useful for elements that need to animate continuously, such as a video player or an animated GIF. - /// It will cause the window to redraw on the next frame, even if no other changes have occurred. - /// - /// If called from within a view, it will notify that view on the next frame. Otherwise, it will refresh the entire window. - /// - /// Callers driving purely decorative animations (spinners, pulses, and the - /// like) should prefer [`AnimationExt::with_animation`](crate::AnimationExt::with_animation), - /// which automatically respects [`App::reduce_motion`]. When using this - /// method directly for decorative motion, check [`App::reduce_motion`] - /// and skip the frame request when it is set. - pub fn request_animation_frame(&self) { - let entity = self.current_view(); - self.on_next_frame(move |_, cx| cx.notify(entity)); - } - - /// Runs all callbacks scheduled via [`Self::on_next_frame`], returning how many ran. - /// - /// Tests have no platform frame loop, so this simulates the delivery of the - /// next frame. - #[cfg(any(test, feature = "test-support"))] - pub fn simulate_next_frame(&mut self, cx: &mut App) -> usize { - let callbacks = self.next_frame_callbacks.take(); - let count = callbacks.len(); - for callback in callbacks { - callback(self, cx); - } - count - } - - /// Spawn the future returned by the given closure on the application thread pool. - /// The closure is provided a handle to the current window and an `AsyncWindowContext` for - /// use within your future. - #[track_caller] - pub fn spawn(&self, cx: &App, f: AsyncFn) -> Task - where - R: 'static, - AsyncFn: AsyncFnOnce(&mut AsyncWindowContext) -> R + 'static, - { - let handle = self.handle; - cx.spawn(async move |app| { - let mut async_window_cx = AsyncWindowContext::new_context(app.clone(), handle); - f(&mut async_window_cx).await - }) - } - - /// Spawn the future returned by the given closure on the application thread - /// pool, with the given priority. The closure is provided a handle to the - /// current window and an `AsyncWindowContext` for use within your future. - #[track_caller] - pub fn spawn_with_priority( - &self, - priority: Priority, - cx: &App, - f: AsyncFn, - ) -> Task - where - R: 'static, - AsyncFn: AsyncFnOnce(&mut AsyncWindowContext) -> R + 'static, - { - let handle = self.handle; - cx.spawn_with_priority(priority, async move |app| { - let mut async_window_cx = AsyncWindowContext::new_context(app.clone(), handle); - f(&mut async_window_cx).await - }) - } - - /// Notify the window that its bounds have changed. - /// - /// This updates internal state like `viewport_size` and `scale_factor` from - /// the platform window, then notifies observers. Normally called automatically - /// by the platform's resize callback, but exposed publicly for test infrastructure. - pub fn bounds_changed(&mut self, cx: &mut App) { - self.scale_factor = self.platform_window.scale_factor(); - self.viewport_size = self.platform_window.content_size(); - self.display_id = self.platform_window.display().map(|display| display.id()); - self.mouse_position = self.platform_window.mouse_position(); - - self.refresh(); - - self.bounds_observers - .clone() - .retain(&(), |callback| callback(self, cx)); - } - - /// Returns the bounds of the current window in the global coordinate space, which could span across multiple displays. - pub fn bounds(&self) -> Bounds { - self.platform_window.bounds() - } - - /// Renders the current frame's scene to a texture and returns the pixel data as an RGBA image. - /// This does not present the frame to screen - useful for visual testing where we want - /// to capture what would be rendered without displaying it or requiring the window to be visible. - #[cfg(any(test, feature = "test-support"))] - pub fn render_to_image(&self) -> anyhow::Result { - self.platform_window - .render_to_image(&self.rendered_frame.scene) - } - - /// Returns the quads in the most recently rendered frame's scene, so tests can assert on - /// painted output without rasterizing the frame. Quad bounds are in scaled pixels and are - /// not clipped; each quad carries the content mask it will be clipped to when drawn. Quads - /// whose bounds don't intersect their content mask are culled at paint time and won't appear. - #[cfg(any(test, feature = "test-support"))] - pub fn painted_quads(&self) -> Vec { - self.rendered_frame.scene.quads.clone() - } - - /// Returns the immutable rounded clip nodes referenced by painted primitives. - #[cfg(any(test, feature = "test-support"))] - pub fn painted_clips(&self) -> Vec> { - self.rendered_frame.scene.rounded_clips.clone() - } - - /// Returns painted shadows for renderer regression tests. - #[cfg(any(test, feature = "test-support"))] - pub fn painted_shadows(&self) -> Vec { - self.rendered_frame.scene.shadows.clone() - } - - /// Set the content size of the window. - pub fn resize(&mut self, size: Size) { - self.platform_window.resize(size); - } - - /// Returns whether or not the window is currently fullscreen - pub fn is_fullscreen(&self) -> bool { - self.platform_window.is_fullscreen() - } - - /// Returns whether the window is currently in simple (borderless) fullscreen, - /// where it covers the entire screen including the menu bar and notch area. - /// Always `false` on platforms other than macOS. - pub fn is_simple_fullscreen(&self) -> bool { - self.platform_window.is_simple_fullscreen() - } - - pub(crate) fn appearance_changed(&mut self, cx: &mut App) { - self.appearance = self.platform_window.appearance(); - - self.appearance_observers - .clone() - .retain(&(), |callback| callback(self, cx)); - } - - pub(crate) fn button_layout_changed(&mut self, cx: &mut App) { - self.button_layout_observers - .clone() - .retain(&(), |callback| callback(self, cx)); - } - - /// Returns the appearance of the current window. - pub fn appearance(&self) -> WindowAppearance { - self.appearance - } - - /// Returns the size of the drawable area within the window. - pub fn viewport_size(&self) -> Size { - self.viewport_size - } - - /// Returns whether this window is focused by the operating system (receiving key events). - pub fn is_window_active(&self) -> bool { - self.active.get() - } - - /// Returns whether this window is considered to be the window - /// that currently owns the mouse cursor. - /// On mac, this is equivalent to `is_window_active`. - pub fn is_window_hovered(&self) -> bool { - if cfg!(any( - target_os = "windows", - target_os = "linux", - target_os = "freebsd" - )) { - self.hovered.get() - } else { - self.is_window_active() - } - } - - /// Toggle zoom on the window. - pub fn zoom_window(&self) { - self.platform_window.zoom(); - } - - /// Opens the native title bar context menu, useful when implementing client side decorations (Wayland and X11) - pub fn show_window_menu(&self, position: Point) { - self.platform_window.show_window_menu(position) - } - - /// Handle window movement for Linux and macOS. - /// Tells the compositor to take control of window movement (Wayland and X11) - /// - /// Events may not be received during a move operation. - pub fn start_window_move(&self) { - self.platform_window.start_window_move() - } - - /// When using client side decorations, set this to the width of the invisible decorations (Wayland and X11) - pub fn set_client_inset(&mut self, inset: Pixels) { - self.client_inset = Some(inset); - self.platform_window.set_client_inset(inset); - } - - /// Returns the client_inset value by [`Self::set_client_inset`]. - pub fn client_inset(&self) -> Option { - self.client_inset - } - - /// Returns whether the title bar window controls need to be rendered by the application (Wayland and X11) - pub fn window_decorations(&self) -> Decorations { - self.platform_window.window_decorations() - } - - /// Returns whether this window is resizable. - pub fn is_resizable(&self) -> bool { - self.is_resizable - } - - /// Returns whether this window is minimizable. - pub fn is_minimizable(&self) -> bool { - self.is_minimizable - } - - /// Returns the controls supported by the platform. - pub fn window_controls(&self) -> WindowControls { - self.platform_window.window_controls() - } - - /// Updates the window's title at the platform level. - pub fn set_window_title(&mut self, title: &str) { - self.platform_window.set_title(title); - self.a11y.set_window_title(title.to_string()); - } - - /// Sets the position of the macOS traffic light buttons. - #[cfg(target_os = "macos")] - pub fn set_traffic_light_position(&self, position: Point) { - self.platform_window.set_traffic_light_position(position); - } - - /// Sets the application identifier. - pub fn set_app_id(&mut self, app_id: &str) { - self.platform_window.set_app_id(app_id); - } - - /// Sets the window background appearance. - pub fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { - self.platform_window - .set_background_appearance(background_appearance); - } - - /// Mark the window as dirty at the platform level. - pub fn set_window_edited(&mut self, edited: bool) { - self.platform_window.set_edited(edited); - } - - /// Set the path of the file this window represents. - /// On macOS, this sets the window's accessibility document property (AXDocument). - pub fn set_document_path(&self, path: Option<&std::path::Path>) { - self.platform_window.set_document_path(path); - } - - /// Determine the display on which the window is visible. - pub fn display(&self, cx: &App) -> Option> { - cx.platform - .displays() - .into_iter() - .find(|display| Some(display.id()) == self.display_id) - } - - /// Show the platform character palette. - pub fn show_character_palette(&self) { - self.platform_window.show_character_palette(); - } - - /// The scale factor of the display associated with the window. For example, it could - /// return 2.0 for a "retina" display, indicating that each logical pixel should actually - /// be rendered as two pixels on screen. - pub fn scale_factor(&self) -> f32 { - self.scale_factor - } - - /// Overrides the display scale factor for tests. - #[cfg(any(test, feature = "test-support"))] - pub fn set_scale_factor(&mut self, scale_factor: f32) { - self.scale_factor = scale_factor; - self.refresh(); - } - - /// The size of an em for the base font of the application. Adjusting this value allows the - /// UI to scale, just like zooming a web page. - pub fn rem_size(&self) -> Pixels { - self.rem_size_override_stack - .last() - .copied() - .unwrap_or(self.rem_size) - } - - /// Sets the size of an em for the base font of the application. Adjusting this value allows the - /// UI to scale, just like zooming a web page. - pub fn set_rem_size(&mut self, rem_size: impl Into) { - self.rem_size = rem_size.into(); - } - - /// Acquire a globally unique identifier for the given ElementId. - /// Only valid for the duration of the provided closure. - pub fn with_global_id( - &mut self, - element_id: ElementId, - f: impl FnOnce(&GlobalElementId, &mut Self) -> R, - ) -> R { - self.with_id(element_id, |this| { - let global_id = GlobalElementId(Arc::from(&*this.element_id_stack)); - - f(&global_id, this) - }) - } - - /// Calls the provided closure with the element ID pushed on the stack. - #[inline] - pub fn with_id( - &mut self, - element_id: impl Into, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - self.element_id_stack.push(element_id.into()); - let result = f(self); - self.element_id_stack.pop(); - result - } - - /// Executes the provided function with the specified rem size. - /// - /// This method must only be called as part of element drawing. - // This function is called in a highly recursive manner in editor - // prepainting, make sure its inlined to reduce the stack burden - #[inline] - pub fn with_rem_size(&mut self, rem_size: Option>, f: F) -> R - where - F: FnOnce(&mut Self) -> R, - { - self.invalidator.debug_assert_paint_or_prepaint(); - - if let Some(rem_size) = rem_size { - self.rem_size_override_stack.push(rem_size.into()); - let result = f(self); - self.rem_size_override_stack.pop(); - result - } else { - f(self) - } - } - - /// The line height associated with the current text style. - pub fn line_height(&self) -> Pixels { - self.text_style().line_height_in_pixels(self.rem_size()) - } - - /// Rounds a logical value to the nearest device pixel. - #[inline] - pub fn pixel_snap(&self, value: Pixels) -> Pixels { - px(round_to_device_pixel(value.0, self.scale_factor()) / self.scale_factor()) - } - - /// f64 variant of [`Self::pixel_snap`]. - #[inline] - pub fn pixel_snap_f64(&self, value: f64) -> f64 { - let scale_factor = f64::from(self.scale_factor()); - round_half_toward_zero_f64(value * scale_factor) / scale_factor - } - - /// Snaps a bounds' origin and size to the nearest device pixel. - #[inline] - pub fn pixel_snap_bounds(&self, bounds: Bounds) -> Bounds { - bounds.map(|c| self.pixel_snap(c)) - } - - /// Snaps a point's coordinates to the nearest device pixel. - #[inline] - pub fn pixel_snap_point(&self, position: Point) -> Point { - position.map(|c| self.pixel_snap(c)) - } - - #[inline] - fn snap_bounds(&self, bounds: Bounds) -> Bounds { - let scale_factor = self.scale_factor(); - let left = round_to_device_pixel(bounds.left().0, scale_factor); - let top = round_to_device_pixel(bounds.top().0, scale_factor); - let right = round_to_device_pixel(bounds.right().0, scale_factor).max(left); - let bottom = round_to_device_pixel(bounds.bottom().0, scale_factor).max(top); - Bounds::from_corners( - point(ScaledPixels(left), ScaledPixels(top)), - point(ScaledPixels(right), ScaledPixels(bottom)), - ) - } - - /// Rounds half-to-zero but clamps any non-zero input up to 1 dp so thin strokes do not disappear. - #[inline] - fn snap_stroke(&self, value: Pixels) -> ScaledPixels { - ScaledPixels(round_stroke_to_device_pixel(value.0, self.scale_factor())) - } - - #[inline] - fn snap_border_widths(&self, edges: Edges) -> Edges { - edges.map(|e| self.snap_stroke(*e)) - } - - /// Floors the near edge and ceils the far edge, producing a strict superset of the raw region. - #[inline] - fn cover_bounds(&self, bounds: Bounds) -> Bounds { - let scale_factor = self.scale_factor(); - let left = floor_to_device_pixel(bounds.left().0, scale_factor); - let top = floor_to_device_pixel(bounds.top().0, scale_factor); - let right = ceil_to_device_pixel(bounds.right().0, scale_factor).max(left); - let bottom = ceil_to_device_pixel(bounds.bottom().0, scale_factor).max(top); - Bounds::from_corners( - point(ScaledPixels(left), ScaledPixels(top)), - point(ScaledPixels(right), ScaledPixels(bottom)), - ) - } - - #[inline] - fn snapped_content_mask(&mut self) -> ContentMask { - let region = self.content_mask(); - let mut clip_index = 0; - for shape in ®ion.rounded_clips { - let mut clip = shape.scale(self.scale_factor()); - // Use the original element's snapped shape, as paint_quad does. - // A descendant's culling bounds must never relocate these curves. - clip.bounds = self.snap_bounds(shape.bounds); - clip.parent = clip_index; - clip_index = self.next_frame.scene.insert_clip(clip); - } - ContentMask { - bounds: self.cover_bounds(region.bounds), - corner_radii: Corners::default(), - clip_index, - clip_padding: 0, - } - } - - /// Call to prevent the default action of an event. Currently only used to prevent - /// parent elements from becoming focused on mouse down. - pub fn prevent_default(&mut self) { - self.default_prevented = true; - } - - /// Obtain whether default has been prevented for the event currently being dispatched. - pub fn default_prevented(&self) -> bool { - self.default_prevented - } - - /// Determine whether the given action is available along the dispatch path to the currently focused element. - pub fn is_action_available(&self, action: &dyn Action, cx: &App) -> bool { - let node_id = - self.focus_node_id_in_rendered_frame(self.focused(cx).map(|handle| handle.id)); - self.rendered_frame - .dispatch_tree - .is_action_available(action, node_id) - } - - /// Determine whether the given action is available along the dispatch path to the given focus_handle. - pub fn is_action_available_in(&self, action: &dyn Action, focus_handle: &FocusHandle) -> bool { - let node_id = self.focus_node_id_in_rendered_frame(Some(focus_handle.id)); - self.rendered_frame - .dispatch_tree - .is_action_available(action, node_id) - } - - /// The position of the mouse relative to the window. - pub fn mouse_position(&self) -> Point { - self.mouse_position - } - - /// Captures the pointer for the given hitbox. While captured, all mouse move and mouse up - /// events will be routed to listeners that check this hitbox's `is_hovered` status, - /// regardless of actual hit testing. This enables drag operations that continue - /// even when the pointer moves outside the element's bounds. - /// - /// The capture is automatically released on mouse up. - pub fn capture_pointer(&mut self, hitbox_id: HitboxId) { - self.captured_hitbox = Some(hitbox_id); - } - - /// Releases any active pointer capture. - pub fn release_pointer(&mut self) { - self.captured_hitbox = None; - } - - /// Returns the hitbox that has captured the pointer, if any. - pub fn captured_hitbox(&self) -> Option { - self.captured_hitbox - } - - /// Captures the current long press for the given entity. - /// - /// The capture is released when the gesture ends or is cancelled, or when - /// a replacement touch begins. A listener must also call - /// [`Self::prevent_default`] on the started event to claim the gesture. - pub fn capture_long_press(&mut self, entity: &Entity) { - self.long_press_capture = Some(entity.entity_id()); - } - - /// Returns whether the given entity has captured the current long press. - pub fn has_long_press_capture(&self, entity: &Entity) -> bool { - self.long_press_capture == Some(entity.entity_id()) - } - - /// The current state of the keyboard's modifiers - pub fn modifiers(&self) -> Modifiers { - self.modifiers - } - - /// Returns true if the last input event was keyboard-based (key press, tab navigation, etc.) - /// This is used for focus-visible styling to show focus indicators only for keyboard navigation. - pub fn last_input_was_keyboard(&self) -> bool { - self.last_input_modality == InputModality::Keyboard - } - - pub(crate) fn last_input_was_touch(&self) -> bool { - self.last_input_modality == InputModality::Touch - } - - /// The current state of the keyboard's capslock - pub fn capslock(&self) -> Capslock { - self.capslock - } - - /// Produces a new frame and assigns it to `rendered_frame`. To actually show - /// the contents of the new [`Scene`], use [`Self::present`]. - #[profiling::function] - pub fn draw(&mut self, cx: &mut App) -> ArenaClearNeeded { - // Drain every draw in profiler builds so a previous frame's - // first-invalidation timestamp can't be attributed to this one. - #[cfg(feature = "profiler")] - let frame_dirty = self.invalidator.take_frame_dirty(); - #[cfg(feature = "profiler")] - self.window_profiler.begin_draw(); - - // Set up the per-App arena for element allocation during this draw. - // This ensures that multiple test Apps have isolated arenas. - let arena_scope = ElementArenaScope::enter(&cx.element_arena); - - self.invalidate_entities(); - cx.entities.clear_accessed(); - debug_assert!(self.rendered_entity_stack.is_empty()); - self.invalidator.set_dirty(false); - self.requested_autoscroll = None; - - // Restore the previously-used input handler. - // Place it back into a None slot (left by a previous .take()) so that - // cached paint_range indices in reuse_paint find the handler at the - // expected position. - if let Some(input_handler) = self.platform_window.take_input_handler() { - if let Some(slot) = self - .rendered_frame - .input_handlers - .iter_mut() - .rev() - .find(|h| h.is_none()) - { - *slot = Some(input_handler); - } else { - self.rendered_frame.input_handlers.push(Some(input_handler)); - } - } - if !cx.mode.skip_drawing() { - self.draw_roots(cx); - #[cfg(feature = "profiler")] - { - let viewport_size = self.viewport_size; - let scale_factor = self.scale_factor(); - self.debug_frame_overlay.paint( - &mut self.next_frame.scene, - viewport_size, - scale_factor, - ); - } - } - self.dirty_views.clear(); - self.next_frame.window_active = self.active.get(); - - // Register requested input handler with the platform window. - // Use .take() instead of .pop() to preserve Vec length, so that cached - // paint_range indices remain valid for reuse_paint on the next frame. - // Search backwards to find the last Some entry, since reuse_paint may - // have copied None slots from the previous frame. (Fixes #50456) - let focused_text_input_active = if let Some(mut input_handler) = self - .next_frame - .input_handlers - .iter_mut() - .rev() - .find_map(|h| h.take()) - { - let accepts_text_input = input_handler.accepts_text_input(self, cx); - self.platform_window.set_input_handler(input_handler); - accepts_text_input - } else { - false - }; - self.apply_text_input_configuration(cx); - if focused_text_input_active != self.focused_text_input_active { - self.focused_text_input_active = focused_text_input_active; - self.platform_window - .text_input_state_changed(if focused_text_input_active { - TextInputStateChange::FocusGained - } else { - TextInputStateChange::FocusLost - }); - } - - self.layout_engine.as_mut().unwrap().clear(); - self.text_system().finish_frame(); - self.next_frame.finish(&mut self.rendered_frame); - - self.invalidator.set_phase(DrawPhase::Focus); - let previous_focus_path = self.rendered_frame.focus_path(); - let previous_window_active = self.rendered_frame.window_active; - mem::swap(&mut self.rendered_frame, &mut self.next_frame); - self.next_frame.clear(); - let current_focus_path = self.rendered_frame.focus_path(); - let current_window_active = self.rendered_frame.window_active; - let mut focus_before_listeners = self.focus; - - if previous_focus_path != current_focus_path - || previous_window_active != current_window_active - { - if !previous_focus_path.is_empty() && current_focus_path.is_empty() { - self.focus_lost_path = previous_focus_path.clone(); - self.focus_lost_listeners - .clone() - .retain(&(), |listener| listener(self, cx)); - self.focus_lost_path = SmallVec::new(); - // The focus-lost fallback (e.g. a workspace refocusing itself) may target - // an element that isn't part of the element tree, in which case scheduling - // a redraw below would dispatch focus-lost again, looping forever. Only - // track focus movement caused by the focus listeners. - focus_before_listeners = self.focus; - } - - let event = WindowFocusEvent { - previous_focus_path: if previous_window_active { - previous_focus_path - } else { - Default::default() - }, - current_focus_path: if current_window_active { - current_focus_path - } else { - Default::default() - }, - }; - self.focus_listeners - .clone() - .retain(&(), |listener| listener(&event, self, cx)); - } - - debug_assert!(self.rendered_entity_stack.is_empty()); - self.record_entities_accessed(cx); - self.reset_cursor_style(cx); - self.refreshing = false; - self.invalidator.set_phase(DrawPhase::None); - // Focus listeners may move focus (e.g. a dock forwarding focus to its active - // panel). `Window::focus` suppresses `refresh` while a draw is in progress, so - // schedule another frame here to render the new focus state and dispatch the - // resulting focus events. - if self.focus != focus_before_listeners { - self.refresh(); - } - self.needs_present.set(true); - - #[cfg(feature = "profiler")] - { - let draw_duration = self - .window_profiler - .end_draw(frame_dirty.dirty_at, frame_dirty.invalidations); - self.debug_frame_overlay.record_frame(draw_duration); - } - - // Exit the scope to obtain the arena-clear token this draw owes; the - // scope's teardown itself happens in `ElementArenaScope::drop`. - arena_scope.exit(&cx.element_arena) - } - - fn record_entities_accessed(&mut self, cx: &mut App) { - let mut entities_ref = cx.entities.accessed_entities.get_mut(); - let mut entities = mem::take(entities_ref.deref_mut()); - let handle = self.handle; - cx.record_entities_accessed( - handle, - // Try moving window invalidator into the Window - self.invalidator.clone(), - &entities, - ); - let mut entities_ref = cx.entities.accessed_entities.get_mut(); - mem::swap(&mut entities, entities_ref.deref_mut()); - } - - fn invalidate_entities(&mut self) { - let mut views = self.invalidator.take_views(); - for entity in views.drain() { - self.mark_view_dirty(entity); - } - self.invalidator.replace_views(views); - } - - #[profiling::function] - fn present(&mut self) { - #[cfg(feature = "profiler")] - let _foreground_turn = profiler::journal::foreground_turn(); - #[cfg(feature = "profiler")] - let present_start = Instant::now(); - self.platform_window.draw(&self.rendered_frame.scene); - #[cfg(feature = "profiler")] - self.window_profiler.record_present( - present_start, - Instant::now(), - self.active.get(), - !self.next_frame_callbacks.borrow().is_empty(), - ); - self.needs_present.set(false); - profiling::finish_frame!(); - } - - /// Presents the most recently drawn frame if it hasn't been presented yet. - /// - /// Benchmarks drive drawing synchronously rather than through a platform - /// frame-request loop, so they call this after each measured update to - /// submit the frame like production presentation would. - #[cfg(any(feature = "bench-support", all(test, feature = "profiler")))] - pub fn present_if_needed(&mut self) { - if self.needs_present.get() { - self.present(); - } - } - - /// Returns a snapshot of the current input-latency histograms. - #[cfg(feature = "profiler")] - pub fn input_latency_snapshot(&self) -> profiler::InputLatencySnapshot { - self.window_profiler.input_latency_snapshot() - } - - /// Returns a snapshot of the current frame-duration histograms. - #[cfg(feature = "profiler")] - pub fn frame_duration_snapshot(&self) -> profiler::FrameDurationSnapshot { - self.window_profiler.frame_duration_snapshot() - } - - /// Returns the current mode of the debug frame overlay. - #[cfg(feature = "profiler")] - pub fn debug_frame_overlay_mode(&self) -> DebugFrameOverlayMode { - self.debug_frame_overlay.mode() - } - - /// Sets the mode of the debug frame overlay and schedules a redraw. - #[cfg(feature = "profiler")] - pub fn set_debug_frame_overlay_mode(&mut self, mode: DebugFrameOverlayMode) { - self.debug_frame_overlay.set_mode(mode); - self.refresh(); - } - - /// Advances the debug frame overlay through its hidden, frame-time-only, - /// and detailed modes. - #[cfg(feature = "profiler")] - pub fn cycle_debug_frame_overlay_mode(&mut self) { - self.set_debug_frame_overlay_mode(self.debug_frame_overlay.mode().next()); - } - - /// Clears the debug frame overlay's frame-time statistics, except for the - /// total frame count, and schedules a redraw. - #[cfg(feature = "profiler")] - pub fn reset_debug_frame_overlay_stats(&mut self) { - self.debug_frame_overlay.reset_stats(); - self.refresh(); - } - - fn draw_roots(&mut self, cx: &mut App) { - self.invalidator.set_phase(DrawPhase::Prepaint); - self.tooltip_bounds.take(); - - self.a11y.sync_active_flag(); - if self.a11y.is_active() { - self.a11y.begin_frame(); - } - - let _inspector_width: Pixels = rems(30.0).to_pixels(self.rem_size()); - let root_size = { - #[cfg(any(feature = "inspector", debug_assertions))] - { - if self.inspector.is_some() { - let mut size = self.viewport_size; - size.width = (size.width - _inspector_width).max(px(0.0)); - size - } else { - self.viewport_size - } - } - #[cfg(not(any(feature = "inspector", debug_assertions)))] - { - self.viewport_size - } - }; - - // Layout all root elements. Like the root element on the web, which - // stretches to fill the viewport unless explicitly sized, window roots - // fill the window when their size is `auto`. - let scale_factor = self.scale_factor(); - let mut root_element = self.root.as_ref().unwrap().clone().into_any_element(); - let root_layout_id = root_element.request_layout(self, cx); - self.layout_engine - .as_mut() - .unwrap() - .stretch_auto_size_to_fill(root_layout_id, root_size, scale_factor); - root_element.prepaint_as_root(Point::default(), root_size.into(), self, cx); - - #[cfg(any(feature = "inspector", debug_assertions))] - let inspector_element = self.prepaint_inspector(_inspector_width, cx); - - self.prepaint_deferred_draws(cx); - - let mut prompt_element = None; - let mut active_drag_element = None; - let mut tooltip_element = None; - if let Some(prompt) = self.prompt.take() { - let mut element = prompt.view.any_view().into_any_element(); - let prompt_layout_id = element.request_layout(self, cx); - self.layout_engine - .as_mut() - .unwrap() - .stretch_auto_size_to_fill(prompt_layout_id, root_size, scale_factor); - element.prepaint_as_root(Point::default(), root_size.into(), self, cx); - prompt_element = Some(element); - self.prompt = Some(prompt); - } else if let Some(active_drag) = cx.active_drag.take() { - let mut element = active_drag.view.clone().into_any_element(); - let offset = self.mouse_position() - active_drag.cursor_offset; - element.prepaint_as_root(offset, AvailableSpace::min_size(), self, cx); - active_drag_element = Some(element); - cx.active_drag = Some(active_drag); - } else { - tooltip_element = self.prepaint_tooltip(cx); - } - - self.mouse_hit_test = self.next_frame.hit_test(self.mouse_position); - - // Now actually paint the elements. - self.invalidator.set_phase(DrawPhase::Paint); - root_element.paint(self, cx); - - #[cfg(any(feature = "inspector", debug_assertions))] - self.paint_inspector(inspector_element, cx); - - self.paint_deferred_draws(cx); - - if let Some(mut prompt_element) = prompt_element { - prompt_element.paint(self, cx); - } else if let Some(mut drag_element) = active_drag_element { - drag_element.paint(self, cx); - } else if let Some(mut tooltip_element) = tooltip_element { - tooltip_element.paint(self, cx); - } - - #[cfg(any(feature = "inspector", debug_assertions))] - self.paint_inspector_hitbox(cx); - - // a11y may have been activated/deactivated halfway through the frame - let a11y_active_start_of_frame = self.a11y.is_active(); - self.a11y.sync_active_flag(); - let a11y_active_end_of_frame = self.a11y.is_active(); - - let should_send_a11y_update = a11y_active_start_of_frame && a11y_active_end_of_frame; - - if a11y_active_start_of_frame { - // Harvest frame metadata for the debug dump while the live window - // and frame are still in scope. - let frame_info = crate::window::a11y::debug::FrameDebugInfo { - viewport_size: self.viewport_size, - scale_factor: self.scale_factor, - tab_stop_count: self.next_frame.tab_stops.tab_stop_count(), - }; - // clear the builder state regardless - let tree_update = self.a11y.end_frame(frame_info); - - if should_send_a11y_update { - log::debug!( - "Sending a11y tree update: {} nodes", - tree_update.nodes.len() - ); - self.platform_window.a11y_tree_update(tree_update); - } - } - } - - fn prepaint_tooltip(&mut self, cx: &mut App) -> Option { - // Use indexing instead of iteration to avoid borrowing self for the duration of the loop. - for tooltip_request_index in (0..self.next_frame.tooltip_requests.len()).rev() { - let Some(Some(tooltip_request)) = self - .next_frame - .tooltip_requests - .get(tooltip_request_index) - .cloned() - else { - log::error!("Unexpectedly absent TooltipRequest"); - continue; - }; - let mut element = tooltip_request.tooltip.view.clone().into_any_element(); - let mouse_position = tooltip_request.tooltip.mouse_position; - let tooltip_size = element.layout_as_root(AvailableSpace::min_size(), self, cx); - - let mut tooltip_bounds = - Bounds::new(mouse_position + point(px(1.), px(1.)), tooltip_size); - let window_bounds = Bounds { - origin: Point::default(), - size: self.viewport_size(), - }; - - if tooltip_bounds.right() > window_bounds.right() { - let new_x = mouse_position.x - tooltip_bounds.size.width - px(1.); - if new_x >= Pixels::ZERO { - tooltip_bounds.origin.x = new_x; - } else { - tooltip_bounds.origin.x = cmp::max( - Pixels::ZERO, - tooltip_bounds.origin.x - tooltip_bounds.right() - window_bounds.right(), - ); - } - } - - if tooltip_bounds.bottom() > window_bounds.bottom() { - let new_y = mouse_position.y - tooltip_bounds.size.height - px(1.); - if new_y >= Pixels::ZERO { - tooltip_bounds.origin.y = new_y; - } else { - tooltip_bounds.origin.y = cmp::max( - Pixels::ZERO, - tooltip_bounds.origin.y - tooltip_bounds.bottom() - window_bounds.bottom(), - ); - } - } - - // It's possible for an element to have an active tooltip while not being painted (e.g. - // via the `visible_on_hover` method). Since mouse listeners are not active in this - // case, instead update the tooltip's visibility here. - let is_visible = - (tooltip_request.tooltip.check_visible_and_update)(tooltip_bounds, self, cx); - if !is_visible { - continue; - } - - self.with_absolute_element_offset(tooltip_bounds.origin, |window| { - element.prepaint(window, cx) - }); - - self.tooltip_bounds = Some(TooltipBounds { - id: tooltip_request.id, - bounds: tooltip_bounds, - }); - return Some(element); - } - None - } - - fn prepaint_deferred_draws(&mut self, cx: &mut App) { - assert_eq!(self.element_id_stack.len(), 0); - - // Process deferred draws in multiple rounds to support nesting. - // Each round processes all current deferred draws, which may push new ones. - // - // The draws are processed in place rather than being moved out of - // `next_frame.deferred_draws`: `prepaint_index` snapshots that vector's - // length, so any prepaint range recorded during a round (view caches, - // nested deferred draws) must index the same vector `reuse_prepaint` - // slices on the next frame. Moving the draws out and re-appending them - // shifts the indices of nested draws, causing reused subtrees to graft - // the wrong deferred draws and panic in the dispatch tree. - let mut round_start = 0; - let mut depth = 0; - loop { - let round_end = self.next_frame.deferred_draws.len(); - if round_start == round_end { - break; - } - // Limit maximum nesting depth to prevent infinite loops. - assert!(depth < 10, "Exceeded maximum (10) deferred depth"); - depth += 1; - - // Sort this round by priority. - let mut traversal_order = (round_start..round_end).collect::>(); - traversal_order.sort_by_key(|ix| self.next_frame.deferred_draws[*ix].priority); - - for deferred_draw_ix in traversal_order { - let (element, parent_node, current_view, rem_size, absolute_offset, prepaint_range) = { - let deferred_draw = &mut self.next_frame.deferred_draws[deferred_draw_ix]; - self.element_id_stack - .clone_from(&deferred_draw.element_id_stack); - self.text_style_stack - .clone_from(&deferred_draw.text_style_stack); - ( - deferred_draw.element.take(), - deferred_draw.parent_node, - deferred_draw.current_view, - deferred_draw.rem_size, - deferred_draw.absolute_offset, - deferred_draw.prepaint_range.clone(), - ) - }; - self.next_frame.dispatch_tree.set_active_node(parent_node); - - let prepaint_start = self.prepaint_index(); - if let Some(mut element) = element { - self.with_rendered_view(current_view, |window| { - window.with_rem_size(Some(rem_size), |window| { - window.with_absolute_element_offset(absolute_offset, |window| { - element.prepaint(window, cx); - }); - }); - }); - self.next_frame.deferred_draws[deferred_draw_ix].element = Some(element); - } else { - self.reuse_prepaint(prepaint_range); - } - let prepaint_end = self.prepaint_index(); - self.next_frame.deferred_draws[deferred_draw_ix].prepaint_range = - prepaint_start..prepaint_end; - } - - self.element_id_stack.clear(); - self.text_style_stack.clear(); - round_start = round_end; - } - } - - fn paint_deferred_draws(&mut self, cx: &mut App) { - assert_eq!(self.element_id_stack.len(), 0); - - // Paint all deferred draws in priority order. - // Since prepaint has already processed nested deferreds, we just paint them all. - if self.next_frame.deferred_draws.len() == 0 { - return; - } - - let traversal_order = self.deferred_draw_traversal_order(); - let mut deferred_draws = mem::take(&mut self.next_frame.deferred_draws); - for deferred_draw_ix in traversal_order { - let mut deferred_draw = &mut deferred_draws[deferred_draw_ix]; - self.element_id_stack - .clone_from(&deferred_draw.element_id_stack); - self.next_frame - .dispatch_tree - .set_active_node(deferred_draw.parent_node); - - let paint_start = self.paint_index(); - let content_mask = deferred_draw.content_mask.clone(); - if let Some(element) = deferred_draw.element.as_mut() { - self.with_rendered_view(deferred_draw.current_view, |window| { - window.with_content_mask(content_mask, |window| { - window.with_rem_size(Some(deferred_draw.rem_size), |window| { - element.paint(window, cx); - }); - }) - }) - } else { - self.reuse_paint(deferred_draw.paint_range.clone()); - } - let paint_end = self.paint_index(); - deferred_draw.paint_range = paint_start..paint_end; - } - self.next_frame.deferred_draws = deferred_draws; - self.element_id_stack.clear(); - } - - fn deferred_draw_traversal_order(&mut self) -> SmallVec<[usize; 8]> { - let deferred_count = self.next_frame.deferred_draws.len(); - let mut sorted_indices = (0..deferred_count).collect::>(); - sorted_indices.sort_by_key(|ix| self.next_frame.deferred_draws[*ix].priority); - sorted_indices - } - - pub(crate) fn prepaint_index(&self) -> PrepaintStateIndex { - PrepaintStateIndex { - hitboxes_index: self.next_frame.hitboxes.len(), - tooltips_index: self.next_frame.tooltip_requests.len(), - deferred_draws_index: self.next_frame.deferred_draws.len(), - dispatch_tree_index: self.next_frame.dispatch_tree.len(), - accessed_element_states_index: self.next_frame.accessed_element_states.len(), - line_layout_index: self.text_system.layout_index(), - } - } - - pub(crate) fn reuse_prepaint(&mut self, range: Range) { - self.next_frame.hitboxes.extend( - self.rendered_frame.hitboxes[range.start.hitboxes_index..range.end.hitboxes_index] - .iter() - .cloned(), - ); - self.next_frame.tooltip_requests.extend( - self.rendered_frame.tooltip_requests - [range.start.tooltips_index..range.end.tooltips_index] - .iter_mut() - .map(|request| request.take()), - ); - self.next_frame.accessed_element_states.extend( - self.rendered_frame.accessed_element_states[range.start.accessed_element_states_index - ..range.end.accessed_element_states_index] - .iter() - .map(|(id, type_id)| (id.clone(), *type_id)), - ); - self.text_system - .reuse_layouts(range.start.line_layout_index..range.end.line_layout_index); - - let reused_subtree = self.next_frame.dispatch_tree.reuse_subtree( - range.start.dispatch_tree_index..range.end.dispatch_tree_index, - &mut self.rendered_frame.dispatch_tree, - self.focus, - ); - - if reused_subtree.contains_focus() { - self.next_frame.focus = self.focus; - } - - self.next_frame.deferred_draws.extend( - self.rendered_frame.deferred_draws - [range.start.deferred_draws_index..range.end.deferred_draws_index] - .iter() - .map(|deferred_draw| DeferredDraw { - current_view: deferred_draw.current_view, - parent_node: reused_subtree.refresh_node_id(deferred_draw.parent_node), - element_id_stack: deferred_draw.element_id_stack.clone(), - text_style_stack: deferred_draw.text_style_stack.clone(), - content_mask: deferred_draw.content_mask.clone(), - rem_size: deferred_draw.rem_size, - priority: deferred_draw.priority, - element: None, - absolute_offset: deferred_draw.absolute_offset, - prepaint_range: deferred_draw.prepaint_range.clone(), - paint_range: deferred_draw.paint_range.clone(), - }), - ); - } - - pub(crate) fn paint_index(&self) -> PaintIndex { - PaintIndex { - scene_index: self.next_frame.scene.len(), - mouse_listeners_index: self.next_frame.mouse_listeners.len(), - input_handlers_index: self.next_frame.input_handlers.len(), - cursor_styles_index: self.next_frame.cursor_styles.len(), - accessed_element_states_index: self.next_frame.accessed_element_states.len(), - tab_handle_index: self.next_frame.tab_stops.paint_index(), - line_layout_index: self.text_system.layout_index(), - } - } - - pub(crate) fn reuse_paint(&mut self, range: Range) { - self.next_frame.cursor_styles.extend( - self.rendered_frame.cursor_styles - [range.start.cursor_styles_index..range.end.cursor_styles_index] - .iter() - .cloned(), - ); - self.next_frame.input_handlers.extend( - self.rendered_frame.input_handlers - [range.start.input_handlers_index..range.end.input_handlers_index] - .iter_mut() - .map(|handler| handler.take()), - ); - self.next_frame.mouse_listeners.extend( - self.rendered_frame.mouse_listeners - [range.start.mouse_listeners_index..range.end.mouse_listeners_index] - .iter_mut() - .map(|listener| listener.take()), - ); - self.next_frame.accessed_element_states.extend( - self.rendered_frame.accessed_element_states[range.start.accessed_element_states_index - ..range.end.accessed_element_states_index] - .iter() - .map(|(id, type_id)| (id.clone(), *type_id)), - ); - self.next_frame.tab_stops.replay( - &self.rendered_frame.tab_stops.insertion_history - [range.start.tab_handle_index..range.end.tab_handle_index], - ); - - self.text_system - .reuse_layouts(range.start.line_layout_index..range.end.line_layout_index); - self.next_frame.scene.replay( - range.start.scene_index..range.end.scene_index, - &self.rendered_frame.scene, - ); - } - - /// Push a text style onto the stack, and call a function with that style active. - /// Use [`Window::text_style`] to get the current, combined text style. This method - /// should only be called as part of element drawing. - pub fn with_text_style(&mut self, style: Option, f: F) -> R - where - F: FnOnce(&mut Self) -> R, - { - self.invalidator.debug_assert_paint_or_prepaint(); - if let Some(style) = style { - self.text_style_stack.push(style); - let result = f(self); - self.text_style_stack.pop(); - result - } else { - f(self) - } - } - - /// Updates the cursor style at the platform level. This method should only be called - /// during the paint phase of element drawing. - pub fn set_cursor_style(&mut self, style: CursorStyle, hitbox: &Hitbox) { - self.invalidator.debug_assert_paint(); - self.next_frame.cursor_styles.push(CursorStyleRequest { - hitbox_id: Some(hitbox.id), - style, - }); - } - - /// Updates the cursor style for the entire window at the platform level. A cursor - /// style using this method will have precedence over any cursor style set using - /// `set_cursor_style`. This method should only be called during the paint - /// phase of element drawing. - pub fn set_window_cursor_style(&mut self, style: CursorStyle) { - self.invalidator.debug_assert_paint(); - self.next_frame.cursor_styles.push(CursorStyleRequest { - hitbox_id: None, - style, - }) - } - - /// Sets a tooltip to be rendered for the upcoming frame. This method should only be called - /// during the paint phase of element drawing. - pub fn set_tooltip(&mut self, tooltip: AnyTooltip) -> TooltipId { - self.invalidator.debug_assert_prepaint(); - let id = TooltipId(post_inc(&mut self.next_tooltip_id.0)); - self.next_frame - .tooltip_requests - .push(Some(TooltipRequest { id, tooltip })); - id - } - - /// Invoke the given function with the given content mask after intersecting it - /// with the current mask. This method should only be called during element drawing. - // This function is called in a highly recursive manner in editor - // prepainting, make sure its inlined to reduce the stack burden - #[inline] - pub fn with_content_mask( - &mut self, - mask: Option>, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - self.invalidator.debug_assert_paint_or_prepaint(); - if let Some(mask) = mask { - let mask = mask.into().intersect(&self.content_mask()); - self.content_mask_stack.push(mask); - let result = f(self); - self.content_mask_stack.pop(); - result - } else { - f(self) - } - } - - /// Updates the global element offset relative to the current offset. This is used to implement - /// scrolling. This method should only be called during the prepaint phase of element drawing. - pub fn with_element_offset( - &mut self, - offset: Point, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - self.invalidator.debug_assert_prepaint(); - - if offset.is_zero() { - return f(self); - }; - - let abs_offset = self.element_offset() + offset; - self.with_absolute_element_offset(abs_offset, f) - } - - /// Updates the global element offset based on the given offset. This is used to implement - /// drag handles and other manual painting of elements. This method should only be called during - /// the prepaint phase of element drawing. - pub fn with_absolute_element_offset( - &mut self, - offset: Point, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - self.invalidator.debug_assert_prepaint(); - self.element_offset_stack.push(offset); - let result = f(self); - self.element_offset_stack.pop(); - result - } - - pub(crate) fn with_element_opacity( - &mut self, - opacity: Option, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - self.invalidator.debug_assert_paint_or_prepaint(); - - let Some(opacity) = opacity else { - return f(self); - }; - - let previous_opacity = self.element_opacity; - self.element_opacity = previous_opacity * opacity; - let result = f(self); - self.element_opacity = previous_opacity; - result - } - - /// Perform prepaint on child elements in a "retryable" manner, so that any side effects - /// of prepaints can be discarded before prepainting again. This is used to support autoscroll - /// where we need to prepaint children to detect the autoscroll bounds, then adjust the - /// element offset and prepaint again. See [`crate::List`] for an example. This method should only be - /// called during the prepaint phase of element drawing. - pub fn transact(&mut self, f: impl FnOnce(&mut Self) -> Result) -> Result { - self.invalidator.debug_assert_prepaint(); - let index = self.prepaint_index(); - let result = f(self); - if result.is_err() { - self.next_frame.hitboxes.truncate(index.hitboxes_index); - self.next_frame - .tooltip_requests - .truncate(index.tooltips_index); - self.next_frame - .deferred_draws - .truncate(index.deferred_draws_index); - self.next_frame - .dispatch_tree - .truncate(index.dispatch_tree_index); - self.next_frame - .accessed_element_states - .truncate(index.accessed_element_states_index); - self.text_system.truncate_layouts(index.line_layout_index); - } - result - } - - /// When you call this method during [`Element::prepaint`], containing elements will attempt to - /// scroll to cause the specified bounds to become visible. When they decide to autoscroll, they will call - /// [`Element::prepaint`] again with a new set of bounds. See [`crate::List`] for an example of an element - /// that supports this method being called on the elements it contains. This method should only be - /// called during the prepaint phase of element drawing. - pub fn request_autoscroll(&mut self, bounds: Bounds) { - self.invalidator.debug_assert_prepaint(); - self.requested_autoscroll = Some(bounds); - } - - /// This method can be called from a containing element such as [`crate::List`] to support the autoscroll behavior - /// described in [`Self::request_autoscroll`]. - pub fn take_autoscroll(&mut self) -> Option> { - self.invalidator.debug_assert_prepaint(); - self.requested_autoscroll.take() - } - - /// Asynchronously load an asset, if the asset hasn't finished loading this will return None. - /// Your view will be re-drawn once the asset has finished loading. - /// - /// Note that the multiple calls to this method will only result in one `Asset::load` call at a - /// time. - pub fn use_asset(&mut self, source: &A::Source, cx: &mut App) -> Option { - let (task, is_first) = cx.fetch_asset::(source); - task.clone().now_or_never().or_else(|| { - if is_first { - let entity_id = self.current_view(); - self.spawn(cx, { - let task = task.clone(); - async move |cx| { - task.await; - - cx.on_next_frame(move |_, cx| { - cx.notify(entity_id); - }); - } - }) - .detach(); - } - - None - }) - } - - /// Asynchronously load an asset, if the asset hasn't finished loading or doesn't exist this will return None. - /// Your view will not be re-drawn once the asset has finished loading. - /// - /// Note that the multiple calls to this method will only result in one `Asset::load` call at a - /// time. - pub fn get_asset(&mut self, source: &A::Source, cx: &mut App) -> Option { - let (task, _) = cx.fetch_asset::(source); - task.now_or_never() - } - /// Obtain the current element offset. This method should only be called during the - /// prepaint phase of element drawing. - pub fn element_offset(&self) -> Point { - self.invalidator.debug_assert_prepaint(); - self.element_offset_stack - .last() - .copied() - .unwrap_or_default() - } - - /// Obtain the current element opacity. This method should only be called during the - /// prepaint phase of element drawing. - #[inline] - pub(crate) fn element_opacity(&self) -> f32 { - self.invalidator.debug_assert_paint_or_prepaint(); - self.element_opacity - } - - /// Obtain the current content mask. This method should only be called during element drawing. - pub fn content_mask(&self) -> crate::ClipRegion { - self.invalidator.debug_assert_paint_or_prepaint(); - self.content_mask_stack.last().cloned().unwrap_or_else(|| { - ContentMask { - bounds: Bounds { - origin: Point::default(), - size: self.viewport_size, - }, - ..Default::default() - } - .into() - }) - } - - /// Provide elements in the called function with a new namespace in which their identifiers must be unique. - /// This can be used within a custom element to distinguish multiple sets of child elements. - pub fn with_element_namespace( - &mut self, - element_id: impl Into, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - self.element_id_stack.push(element_id.into()); - let result = f(self); - self.element_id_stack.pop(); - result - } - - /// Use a piece of state that exists as long this element is being rendered in consecutive frames. - pub fn use_keyed_state( - &mut self, - key: impl Into, - cx: &mut App, - init: impl FnOnce(&mut Self, &mut Context) -> S, - ) -> Entity { - let current_view = self.current_view(); - self.with_global_id(key.into(), |global_id, window| { - window.with_element_state(global_id, |state: Option>, window| { - if let Some(state) = state { - (state.clone(), state) - } else { - let new_state = cx.new(|cx| init(window, cx)); - cx.observe(&new_state, move |_, cx| { - cx.notify(current_view); - }) - .detach(); - (new_state.clone(), new_state) - } - }) - }) - } - - /// Use a piece of state that exists as long this element is being rendered in consecutive frames, without needing to specify a key - /// - /// NOTE: This method uses the location of the caller to generate an ID for this state. - /// If this is not sufficient to identify your state (e.g. you're rendering a list item), - /// you can provide a custom ElementID using the `use_keyed_state` method. - #[track_caller] - pub fn use_state( - &mut self, - cx: &mut App, - init: impl FnOnce(&mut Self, &mut Context) -> S, - ) -> Entity { - self.use_keyed_state( - ElementId::CodeLocation(*core::panic::Location::caller()), - cx, - init, - ) - } - - /// Updates or initializes state for an element with the given id that lives across multiple - /// frames. If an element with this ID existed in the rendered frame, its state will be passed - /// to the given closure. The state returned by the closure will be stored so it can be referenced - /// when drawing the next frame. This method should only be called as part of element drawing. - pub fn with_element_state( - &mut self, - global_id: &GlobalElementId, - f: impl FnOnce(Option, &mut Self) -> (R, S), - ) -> R - where - S: 'static, - { - self.invalidator.debug_assert_paint_or_prepaint(); - - let key = (global_id.clone(), TypeId::of::()); - self.next_frame.accessed_element_states.push(key.clone()); - - if let Some(any) = self - .next_frame - .element_states - .remove(&key) - .or_else(|| self.rendered_frame.element_states.remove(&key)) - { - let ElementStateBox { - inner, - #[cfg(debug_assertions)] - type_name, - } = any; - // Using the extra inner option to avoid needing to reallocate a new box. - let mut state_box = inner - .downcast::>() - .map_err(|_| { - #[cfg(debug_assertions)] - { - anyhow::anyhow!( - "invalid element state type for id, requested {:?}, actual: {:?}", - std::any::type_name::(), - type_name - ) - } - - #[cfg(not(debug_assertions))] - { - anyhow::anyhow!( - "invalid element state type for id, requested {:?}", - std::any::type_name::(), - ) - } - }) - .unwrap(); - - let state = state_box.take().expect( - "reentrant call to with_element_state for the same state type and element id", - ); - let (result, state) = f(Some(state), self); - state_box.replace(state); - self.next_frame.element_states.insert( - key, - ElementStateBox { - inner: state_box, - #[cfg(debug_assertions)] - type_name, - }, - ); - result - } else { - let (result, state) = f(None, self); - self.next_frame.element_states.insert( - key, - ElementStateBox { - inner: Box::new(Some(state)), - #[cfg(debug_assertions)] - type_name: std::any::type_name::(), - }, - ); - result - } - } - - /// A variant of `with_element_state` that allows the element's id to be optional. This is a convenience - /// method for elements where the element id may or may not be assigned. Prefer using `with_element_state` - /// when the element is guaranteed to have an id. - /// - /// The first option means 'no ID provided' - /// The second option means 'not yet initialized' - pub fn with_optional_element_state( - &mut self, - global_id: Option<&GlobalElementId>, - f: impl FnOnce(Option>, &mut Self) -> (R, Option), - ) -> R - where - S: 'static, - { - self.invalidator.debug_assert_paint_or_prepaint(); - - if let Some(global_id) = global_id { - self.with_element_state(global_id, |state, cx| { - let (result, state) = f(Some(state), cx); - let state = - state.expect("you must return some state when you pass some element id"); - (result, state) - }) - } else { - let (result, state) = f(None, self); - debug_assert!( - state.is_none(), - "you must not return an element state when passing None for the global id" - ); - result - } - } - - /// Executes the given closure within the context of a tab group. - #[inline] - pub fn with_tab_group(&mut self, index: Option, f: impl FnOnce(&mut Self) -> R) -> R { - if let Some(index) = index { - self.next_frame.tab_stops.begin_group(index); - let result = f(self); - self.next_frame.tab_stops.end_group(); - result - } else { - f(self) - } - } - - /// Defers the drawing of the given element, scheduling it to be painted on top of the currently-drawn tree - /// at a later time. The `priority` parameter determines the drawing order relative to other deferred elements, - /// with higher values being drawn on top. - /// - /// When `content_mask` is provided, the deferred element will be clipped to that region during - /// both prepaint and paint. When `None`, no additional clipping is applied. - /// - /// This method should only be called as part of the prepaint phase of element drawing. - pub fn defer_draw( - &mut self, - element: AnyElement, - absolute_offset: Point, - priority: usize, - content_mask: Option, - ) { - self.invalidator.debug_assert_prepaint(); - let parent_node = self.next_frame.dispatch_tree.active_node_id().unwrap(); - self.next_frame.deferred_draws.push(DeferredDraw { - current_view: self.current_view(), - parent_node, - element_id_stack: self.element_id_stack.clone(), - text_style_stack: self.text_style_stack.clone(), - content_mask, - rem_size: self.rem_size(), - priority, - element: Some(element), - absolute_offset, - prepaint_range: PrepaintStateIndex::default()..PrepaintStateIndex::default(), - paint_range: PaintIndex::default()..PaintIndex::default(), - }); - } - - /// Creates a new painting layer for the specified bounds. A "layer" is a batch - /// of geometry that are non-overlapping and have the same draw order. This is typically used - /// for performance reasons. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn paint_layer(&mut self, bounds: Bounds, f: impl FnOnce(&mut Self) -> R) -> R { - self.invalidator.debug_assert_paint(); - - let content_mask = self.content_mask(); - let clipped_bounds = bounds.intersect(&content_mask.bounds); - if !clipped_bounds.is_empty() { - self.next_frame - .scene - .push_layer(self.cover_bounds(clipped_bounds)); - } - - let result = f(self); - - if !clipped_bounds.is_empty() { - self.next_frame.scene.pop_layer(); - } - - result - } - - /// Paint the drop (non-inset) shadows from `shadows` into the scene at the current - /// z-index. Inset shadows are skipped; paint those with [`Self::paint_inset_shadows`] - /// after the element's background so they layer on top of the fill. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn paint_drop_shadows( - &mut self, - bounds: Bounds, - corner_radii: Corners, - shadows: &[BoxShadow], - ) { - self.invalidator.debug_assert_paint(); - - let scale_factor = self.scale_factor(); - let content_mask = self.snapped_content_mask(); - let opacity = self.element_opacity(); - let element_bounds = self.cover_bounds(bounds); - let element_corner_radii = corner_radii.scale(scale_factor); - for shadow in shadows { - if shadow.inset { - continue; - } - let shadow_bounds = (bounds + shadow.offset).dilate(shadow.spread_radius); - // CSS expands a rounded shadow's curve together with its border - // box. Keeping the element radius on a spread shadow makes the - // outside edge visibly squarer than the control (most obvious on - // focus rings, whose two spread layers are the control's outline - // and offset gap). The shadow quad is larger by `spread` on every - // side, so the corresponding corner radius grows by the same - // amount. Negative spreads shrink the curve and clamp at zero. - let shadow_corner_radii = corner_radii.map(|radius| { - (*radius + shadow.spread_radius).max(Pixels::ZERO) - }); - self.next_frame.scene.insert_primitive(Shadow { - order: 0, - blur_radius: shadow.blur_radius.scale(scale_factor), - bounds: self.cover_bounds(shadow_bounds), - content_mask, - corner_radii: shadow_corner_radii.scale(scale_factor), - color: shadow.color.opacity(opacity), - element_bounds, - element_corner_radii, - inset: 0, - pad: 0, - }); - } - } - - /// Paint the inset shadows from `shadows` into the scene at the current z-index. Should - /// be called after the element's background so the shadow layers on top of the fill. - /// Drop shadows are skipped; paint those with [`Self::paint_drop_shadows`] before the background. - pub fn paint_inset_shadows( - &mut self, - bounds: Bounds, - corner_radii: Corners, - shadows: &[BoxShadow], - ) { - self.invalidator.debug_assert_paint(); - - let scale_factor = self.scale_factor(); - let content_mask = self.snapped_content_mask(); - let opacity = self.element_opacity(); - let element_bounds = self.cover_bounds(bounds); - let element_corner_radii = corner_radii.scale(scale_factor); - for shadow in shadows { - if !shadow.inset { - continue; - } - let hole = (bounds + shadow.offset).dilate(-shadow.spread_radius); - // Clamp at zero so a large spread can't produce negative radii, which would - // break the SDF in the shader. - let zero = Pixels::ZERO; - let hole_corner_radii = Corners { - top_left: (corner_radii.top_left - shadow.spread_radius).max(zero), - top_right: (corner_radii.top_right - shadow.spread_radius).max(zero), - bottom_right: (corner_radii.bottom_right - shadow.spread_radius).max(zero), - bottom_left: (corner_radii.bottom_left - shadow.spread_radius).max(zero), - }; - self.next_frame.scene.insert_primitive(Shadow { - order: 0, - blur_radius: shadow.blur_radius.scale(scale_factor), - bounds: self.cover_bounds(hole), - content_mask, - corner_radii: hole_corner_radii.scale(scale_factor), - color: shadow.color.opacity(opacity), - element_bounds, - element_corner_radii, - inset: 1, - pad: 0, - }); - } - } - - fn largest_border_interior(quad: &Quad) -> Bounds { - let radii = &quad.corner_radii; - let widths = &quad.border_widths; - let edge_radii = Edges { - top: radii.top_left.max(radii.top_right), - right: radii.top_right.max(radii.bottom_right), - bottom: radii.bottom_left.max(radii.bottom_right), - left: radii.top_left.max(radii.bottom_left), - }; - - let antialias_inset = point(ScaledPixels(1.0), ScaledPixels(1.0)); - let inset_bounds = |top_left_inset, bottom_right_inset| { - Bounds::from_corners( - quad.bounds.origin + top_left_inset + antialias_inset, - quad.bounds.bottom_right() - bottom_right_inset - antialias_inset, - ) - }; - - // Rounded corners need only be excluded on one axis. Either candidate - // is empty of border pixels, so use the larger interior. - let horizontal_band = inset_bounds( - point(widths.left, widths.top.max(edge_radii.top)), - point(widths.right, widths.bottom.max(edge_radii.bottom)), - ); - let vertical_band = inset_bounds( - point(widths.left.max(edge_radii.left), widths.top), - point(widths.right.max(edge_radii.right), widths.bottom), - ); - - let area = |bounds: &Bounds| { - bounds.size.width.0.max(0.) * bounds.size.height.0.max(0.) - }; - if area(&horizontal_band) >= area(&vertical_band) { - horizontal_band - } else { - vertical_band - } - } - - /// Paint one or more quads into the scene for the next frame at the current stacking context. - /// Quads are colored rectangular regions with an optional background, border, and corner radius. - /// see [`fill`], [`outline`], and [`quad`] to construct this type. - /// - /// This method should only be called as part of the paint phase of element drawing. - /// - /// Note that the `quad.corner_radii` are allowed to exceed the bounds, creating sharp corners - /// where the circular arcs meet. This will not display well when combined with dashed borders. - /// Use `Corners::clamp_radii_for_quad_size` if the radii should fit within the bounds. - pub fn paint_quad(&mut self, quad: PaintQuad) { - self.invalidator.debug_assert_paint(); - - let opacity = self.element_opacity(); - let snapped_bounds = self.snap_bounds(quad.bounds); - let snapped_border_widths = self.snap_border_widths(quad.border_widths); - let quad = Quad { - order: 0, - bounds: snapped_bounds, - content_mask: self.snapped_content_mask(), - background: quad.background.opacity(opacity), - border_color: quad.border_color.opacity(opacity), - corner_radii: quad.corner_radii.scale(self.scale_factor()), - border_widths: snapped_border_widths, - border_style: quad.border_style, - }; - - if !quad.background.is_transparent() { - self.next_frame.scene.insert_primitive(quad); - return; - } - - // Splitting a border-only quad around its empty interior avoids shading - // every transparent pixel inside large outlines. - let outer_bounds = quad.bounds; - let inner_bounds = Self::largest_border_interior(&quad); - - if inner_bounds.is_empty() { - self.next_frame.scene.insert_primitive(quad); - return; - } - - let strips = [ - // Top - Bounds::from_corners( - outer_bounds.origin, - point(outer_bounds.right(), inner_bounds.top()), - ), - // Bottom - Bounds::from_corners( - point(outer_bounds.left(), inner_bounds.bottom()), - outer_bounds.bottom_right(), - ), - // Left - Bounds::from_corners( - point(outer_bounds.left(), inner_bounds.top()), - inner_bounds.bottom_left(), - ), - // Right - Bounds::from_corners( - inner_bounds.top_right(), - point(outer_bounds.right(), inner_bounds.bottom()), - ), - ]; - - for strip in strips { - let content_mask_bounds = quad.content_mask.bounds.intersect(&strip); - if !content_mask_bounds.is_empty() { - self.next_frame.scene.insert_primitive(Quad { - content_mask: ContentMask { - bounds: content_mask_bounds, - ..quad.content_mask - }, - ..quad - }); - } - } - } - - /// Paint the given `Path` into the scene for the next frame at the current z-index. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn paint_path(&mut self, mut path: Path, color: impl Into) { - self.invalidator.debug_assert_paint(); - - let scale_factor = self.scale_factor(); - let content_mask = self.snapped_content_mask(); - let opacity = self.element_opacity(); - let color: Background = color.into(); - path.color = color.opacity(opacity); - let mut path = path.scale(scale_factor); - path.content_mask = content_mask; - self.next_frame.scene.insert_primitive(path); - } - - /// Paint an underline into the scene for the next frame at the current z-index. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn paint_underline( - &mut self, - origin: Point, - width: Pixels, - style: &UnderlineStyle, - ) { - self.invalidator.debug_assert_paint(); - - let scale_factor = self.scale_factor(); - let thickness = self.snap_stroke(style.thickness); - let height = if style.wavy { - ScaledPixels(thickness.0 * 3.) - } else { - thickness - }; - let bounds = Bounds { - origin: origin.map(|c| ScaledPixels(round_to_device_pixel(c.0, scale_factor))), - size: size(self.snap_stroke(width), height), - }; - let element_opacity = self.element_opacity(); - - let content_mask = self.snapped_content_mask(); - self.next_frame.scene.insert_primitive(Underline { - order: 0, - pad: 0, - bounds, - content_mask, - color: style.color.unwrap_or_default().opacity(element_opacity), - thickness, - wavy: style.wavy.into(), - }); - } - - /// Paint a strikethrough into the scene for the next frame at the current z-index. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn paint_strikethrough( - &mut self, - origin: Point, - width: Pixels, - style: &StrikethroughStyle, - ) { - self.invalidator.debug_assert_paint(); - - let scale_factor = self.scale_factor(); - let height = style.thickness; - let bounds = Bounds { - origin: origin.map(|c| ScaledPixels(round_to_device_pixel(c.0, scale_factor))), - size: size(self.snap_stroke(width), self.snap_stroke(height)), - }; - let opacity = self.element_opacity(); - - let content_mask = self.snapped_content_mask(); - self.next_frame.scene.insert_primitive(Underline { - order: 0, - pad: 0, - bounds, - content_mask, - thickness: self.snap_stroke(style.thickness), - color: style.color.unwrap_or_default().opacity(opacity), - wavy: false.into(), - }); - } - - /// Paints a monochrome (non-emoji) glyph into the scene for the next frame at the current z-index. - /// - /// The y component of the origin is the baseline of the glyph. - /// You should generally prefer to use the [`ShapedLine::paint`](crate::ShapedLine::paint) or - /// [`WrappedLine::paint`](crate::WrappedLine::paint) methods in the [`TextSystem`](crate::TextSystem). - /// This method is only useful if you need to paint a single glyph that has already been shaped. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn paint_glyph( - &mut self, - origin: Point, - font_id: FontId, - glyph_id: GlyphId, - font_size: Pixels, - color: Hsla, - ) -> Result<()> { - self.invalidator.debug_assert_paint(); - - let element_opacity = self.element_opacity(); - let scale_factor = self.scale_factor(); - let glyph_origin = origin.scale(scale_factor); - - let quantized_origin = Point::new( - round_half_toward_zero(glyph_origin.x.0 * SUBPIXEL_VARIANTS_X as f32) - / SUBPIXEL_VARIANTS_X as f32, - round_half_toward_zero(glyph_origin.y.0 * SUBPIXEL_VARIANTS_Y as f32) - / SUBPIXEL_VARIANTS_Y as f32, - ); - let subpixel_variant = Point::new( - (quantized_origin.x.fract() * SUBPIXEL_VARIANTS_X as f32) as u8, - (quantized_origin.y.fract() * SUBPIXEL_VARIANTS_Y as f32) as u8, - ); - let integer_origin = quantized_origin.map(|c| ScaledPixels(c.trunc())); - let subpixel_rendering = self.should_use_subpixel_rendering(font_id, font_size); - let dilation = self.text_system().glyph_dilation_for_color(color); - let params = RenderGlyphParams { - font_id, - glyph_id, - font_size, - subpixel_variant, - scale_factor, - is_emoji: false, - subpixel_rendering, - dilation, - }; - - let raster_bounds = self.text_system().raster_bounds(¶ms)?; - if !raster_bounds.is_zero() { - let tile = self - .sprite_atlas - .get_or_insert_with(¶ms.clone().into(), &mut || { - let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?; - Ok(Some((size, Cow::Owned(bytes)))) - })? - .expect("Callback above only errors or returns Some"); - let bounds = Bounds { - origin: integer_origin + raster_bounds.origin.map(Into::into), - size: tile.bounds.size.map(Into::into), - }; - let content_mask = self.snapped_content_mask(); - - if subpixel_rendering { - self.next_frame.scene.insert_primitive(SubpixelSprite { - order: 0, - pad: 0, - bounds, - content_mask, - color: color.opacity(element_opacity), - tile, - transformation: TransformationMatrix::unit(), - }); - } else { - self.next_frame.scene.insert_primitive(MonochromeSprite { - order: 0, - pad: 0, - bounds, - content_mask, - color: color.opacity(element_opacity), - tile, - transformation: TransformationMatrix::unit(), - }); - } - } - Ok(()) - } - - fn should_use_subpixel_rendering(&self, font_id: FontId, font_size: Pixels) -> bool { - if self.platform_window.background_appearance() != WindowBackgroundAppearance::Opaque { - return false; - } - - if !self.platform_window.is_subpixel_rendering_supported() { - return false; - } - - let mode = match self.text_rendering_mode.get() { - TextRenderingMode::PlatformDefault => self - .text_system() - .recommended_rendering_mode(font_id, font_size), - mode => mode, - }; - - mode == TextRenderingMode::Subpixel - } - - /// Paints an emoji glyph into the scene for the next frame at the current z-index. - /// - /// The y component of the origin is the baseline of the glyph. - /// You should generally prefer to use the [`ShapedLine::paint`](crate::ShapedLine::paint) or - /// [`WrappedLine::paint`](crate::WrappedLine::paint) methods in the [`TextSystem`](crate::TextSystem). - /// This method is only useful if you need to paint a single emoji that has already been shaped. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn paint_emoji( - &mut self, - origin: Point, - font_id: FontId, - glyph_id: GlyphId, - font_size: Pixels, - ) -> Result<()> { - self.invalidator.debug_assert_paint(); - - let scale_factor = self.scale_factor(); - let glyph_origin = origin.scale(scale_factor); - let integer_origin = glyph_origin.map(|c| ScaledPixels(round_half_toward_zero(c.0))); - let params = RenderGlyphParams { - font_id, - glyph_id, - font_size, - subpixel_variant: Default::default(), - scale_factor, - is_emoji: true, - subpixel_rendering: false, - dilation: 0, - }; - - let raster_bounds = self.text_system().raster_bounds(¶ms)?; - if !raster_bounds.is_zero() { - let tile = self - .sprite_atlas - .get_or_insert_with(¶ms.clone().into(), &mut || { - let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?; - Ok(Some((size, Cow::Owned(bytes)))) - })? - .expect("Callback above only errors or returns Some"); - - let bounds = Bounds { - origin: integer_origin + raster_bounds.origin.map(Into::into), - size: tile.bounds.size.map(Into::into), - }; - let content_mask = self.snapped_content_mask(); - let opacity = self.element_opacity(); - - self.next_frame.scene.insert_primitive(PolychromeSprite { - order: 0, - pad: 0, - grayscale: false.into(), - bounds, - corner_radii: Default::default(), - content_mask, - tile, - opacity, - }); - } - Ok(()) - } - - /// Paint a monochrome SVG into the scene for the next frame at the current stacking context. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn paint_svg( - &mut self, - bounds: Bounds, - path: SharedString, - mut data: Option<&[u8]>, - transformation: TransformationMatrix, - color: Hsla, - cx: &App, - ) -> Result<()> { - self.invalidator.debug_assert_paint(); - - let element_opacity = self.element_opacity(); - let bounds = self.snap_bounds(bounds); - - let params = RenderSvgParams { - path, - size: bounds.size.map(|pixels| { - DevicePixels::from((pixels.0 * SMOOTH_SVG_SCALE_FACTOR).ceil() as i32) - }), - }; - - let Some(tile) = - self.sprite_atlas - .get_or_insert_with(¶ms.clone().into(), &mut || { - let Some((size, bytes)) = cx.svg_renderer.render_alpha_mask(¶ms, data)? - else { - return Ok(None); - }; - Ok(Some((size, Cow::Owned(bytes)))) - })? - else { - return Ok(()); - }; - let content_mask = self.snapped_content_mask(); - let svg_bounds = Bounds { - origin: bounds.center() - - Point::new( - ScaledPixels(tile.bounds.size.width.0 as f32 / SMOOTH_SVG_SCALE_FACTOR / 2.), - ScaledPixels(tile.bounds.size.height.0 as f32 / SMOOTH_SVG_SCALE_FACTOR / 2.), - ), - size: tile - .bounds - .size - .map(|value| ScaledPixels(value.0 as f32 / SMOOTH_SVG_SCALE_FACTOR)), - }; - let final_bounds = svg_bounds - .map_origin(|value| ScaledPixels(round_half_toward_zero(value.0))) - .map_size(|size| size.ceil()); - - self.next_frame.scene.insert_primitive(MonochromeSprite { - order: 0, - pad: 0, - bounds: final_bounds, - content_mask, - color: color.opacity(element_opacity), - tile, - transformation, - }); - - Ok(()) - } - - /// Paint an image into the scene for the next frame at the current z-index. - /// This method will panic if the frame_index is not valid - /// - /// This method should only be called as part of the paint phase of element drawing. - /// Paint an image into `bounds`, positioning and scaling it according to `image_bounds`. - /// - /// The visible region rendered is `bounds.intersect(&image_bounds)`, with `corner_radii` - /// applied to `bounds`. - pub fn paint_image( - &mut self, - bounds: Bounds, - image_bounds: Bounds, - corner_radii: Corners, - data: Arc, - frame_index: usize, - grayscale: bool, - ) -> Result<()> { - self.invalidator.debug_assert_paint(); - - let visible_bounds = bounds.intersect(&image_bounds); - if visible_bounds.size.width <= Pixels::ZERO || visible_bounds.size.height <= Pixels::ZERO { - return Ok(()); - } - if image_bounds.size.width <= Pixels::ZERO || image_bounds.size.height <= Pixels::ZERO { - return Ok(()); - } - - let params = RenderImageParams { - image_id: data.id, - frame_index, - }; - - let tile = self - .sprite_atlas - .get_or_insert_with(¶ms.into(), &mut || { - Ok(Some(( - data.size(frame_index), - Cow::Borrowed( - data.as_bytes(frame_index) - .expect("It's the caller's job to pass a valid frame index"), - ), - ))) - })? - .expect("Callback above only returns Some"); - - let visible_bounds_snapped = self.snap_bounds(visible_bounds); - - let sub_tile = if visible_bounds == image_bounds { - tile - } else { - let x_offset_ratio = - (visible_bounds.origin.x - image_bounds.origin.x) / image_bounds.size.width; - let y_offset_ratio = - (visible_bounds.origin.y - image_bounds.origin.y) / image_bounds.size.height; - let width_ratio = visible_bounds.size.width / image_bounds.size.width; - let height_ratio = visible_bounds.size.height / image_bounds.size.height; - - let tile_origin_x = tile.bounds.origin.x.0; - let tile_origin_y = tile.bounds.origin.y.0; - let tile_width = tile.bounds.size.width.0; - let tile_height = tile.bounds.size.height.0; - - let sub_origin_x = tile_origin_x + (x_offset_ratio * tile_width as f32).round() as i32; - let sub_origin_y = tile_origin_y + (y_offset_ratio * tile_height as f32).round() as i32; - let sub_width = (width_ratio * tile_width as f32).round() as i32; - let sub_height = (height_ratio * tile_height as f32).round() as i32; - - let max_x = tile_origin_x + tile_width; - let max_y = tile_origin_y + tile_height; - - let clamped_origin_x = sub_origin_x.clamp(tile_origin_x, max_x); - let clamped_origin_y = sub_origin_y.clamp(tile_origin_y, max_y); - let clamped_width = sub_width.min(max_x - clamped_origin_x).max(0); - let clamped_height = sub_height.min(max_y - clamped_origin_y).max(0); - - AtlasTile { - bounds: Bounds { - origin: point( - DevicePixels(clamped_origin_x), - DevicePixels(clamped_origin_y), - ), - size: size(DevicePixels(clamped_width), DevicePixels(clamped_height)), - }, - ..tile - } - }; - - let content_mask = self.snapped_content_mask(); - let corner_radii = corner_radii - .clamp_radii_for_quad_size(visible_bounds.size) - .scale(self.scale_factor()); - let opacity = self.element_opacity(); - - self.next_frame.scene.insert_primitive(PolychromeSprite { - order: 0, - pad: 0, - grayscale: grayscale.into(), - bounds: visible_bounds_snapped, - content_mask, - corner_radii, - tile: sub_tile, - opacity, - }); - Ok(()) - } - - /// Paint a surface into the scene for the next frame at the current z-index. - /// - /// This method should only be called as part of the paint phase of element drawing. - #[cfg(target_os = "macos")] - pub fn paint_surface(&mut self, bounds: Bounds, image_buffer: CVPixelBuffer) { - use crate::PaintSurface; - - self.invalidator.debug_assert_paint(); - - let bounds = self.snap_bounds(bounds); - let content_mask = self.snapped_content_mask(); - self.next_frame.scene.insert_primitive(PaintSurface { - order: 0, - bounds, - content_mask, - image_buffer, - }); - } - - /// Removes an image from the sprite atlas. - pub fn drop_image(&mut self, data: Arc) -> Result<()> { - for frame_index in 0..data.frame_count() { - let params = RenderImageParams { - image_id: data.id, - frame_index, - }; - - self.sprite_atlas.remove(¶ms.clone().into()); - } - - Ok(()) - } - - /// Returns whether every frame of an image is present in the sprite atlas. - #[cfg(any(test, feature = "test-support"))] - pub fn has_image_atlas_entry(&self, data: &RenderImage) -> bool { - data.frame_count() > 0 - && (0..data.frame_count()).all(|frame_index| { - self.sprite_atlas.contains( - &RenderImageParams { - image_id: data.id, - frame_index, - } - .into(), - ) - }) - } - - /// Add a node to the layout tree for the current frame. Takes the `Style` of the element for which - /// layout is being requested, along with the layout ids of any children. This method is called during - /// calls to the [`Element::request_layout`] trait method and enables any element to participate in layout. - /// - /// This method should only be called as part of the request_layout or prepaint phase of element drawing. - #[must_use] - pub fn request_layout( - &mut self, - style: Style, - children: impl IntoIterator, - cx: &mut App, - ) -> LayoutId { - self.invalidator.debug_assert_prepaint(); - - cx.layout_id_buffer.clear(); - cx.layout_id_buffer.extend(children); - let rem_size = self.rem_size(); - let scale_factor = self.scale_factor(); - - self.layout_engine.as_mut().unwrap().request_layout( - style, - rem_size, - scale_factor, - &cx.layout_id_buffer, - ) - } - - /// Add a node to the layout tree for the current frame. Instead of taking a `Style` and children, - /// this variant takes a function that is invoked during layout so you can use arbitrary logic to - /// determine the element's size. One place this is used internally is when measuring text. - /// - /// The given closure is invoked at layout time with the known dimensions and available space and - /// returns a `Size`. - /// - /// This method should only be called as part of the request_layout or prepaint phase of element drawing. - pub fn request_measured_layout(&mut self, style: Style, measure: F) -> LayoutId - where - F: Fn(Size>, Size, &mut Window, &mut App) -> Size - + 'static, - { - self.invalidator.debug_assert_prepaint(); - - let rem_size = self.rem_size(); - let scale_factor = self.scale_factor(); - self.layout_engine - .as_mut() - .unwrap() - .request_measured_layout(style, rem_size, scale_factor, measure) - } - - /// Compute the layout for the given id within the given available space. - /// This method is called for its side effect, typically by the framework prior to painting. - /// After calling it, you can request the bounds of the given layout node id or any descendant. - /// - /// This method should only be called as part of the prepaint phase of element drawing. - pub fn compute_layout( - &mut self, - layout_id: LayoutId, - available_space: Size, - cx: &mut App, - ) { - self.invalidator.debug_assert_prepaint(); - - let mut layout_engine = self.layout_engine.take().unwrap(); - layout_engine.compute_layout(layout_id, available_space, self, cx); - self.layout_engine = Some(layout_engine); - } - - /// Obtain the bounds computed for the given LayoutId relative to the window. This method will usually be invoked by - /// GPUI itself automatically in order to pass your element its `Bounds` automatically. - /// - /// This method should only be called as part of element drawing. - pub fn layout_bounds(&mut self, layout_id: LayoutId) -> Bounds { - self.invalidator.debug_assert_prepaint(); - - let scale_factor = self.scale_factor(); - let mut bounds = self - .layout_engine - .as_mut() - .unwrap() - .layout_bounds(layout_id, scale_factor) - .map(Into::into); - let snapped_offset = self.pixel_snap_point(self.element_offset()); - bounds.origin += snapped_offset; - bounds - } - - /// This method should be called during `prepaint`. You can use - /// the returned [Hitbox] during `paint` or in an event handler - /// to determine whether the inserted hitbox was the topmost. - /// - /// This method should only be called as part of the prepaint phase of element drawing. - pub fn insert_hitbox(&mut self, bounds: Bounds, behavior: HitboxBehavior) -> Hitbox { - self.invalidator.debug_assert_prepaint(); - - let content_mask = self.content_mask(); - let mut id = self.next_hitbox_id; - self.next_hitbox_id = self.next_hitbox_id.next(); - let hitbox = Hitbox { - id, - bounds, - content_mask, - behavior, - }; - self.next_frame.hitboxes.push(hitbox.clone()); - hitbox - } - - /// Set a hitbox which will act as a control area of the platform window. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn insert_window_control_hitbox(&mut self, area: WindowControlArea, hitbox: Hitbox) { - self.invalidator.debug_assert_paint(); - self.next_frame.window_control_hitboxes.push((area, hitbox)); - } - - /// Sets the key context for the current element. This context will be used to translate - /// keybindings into actions. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn set_key_context(&mut self, context: KeyContext) { - self.invalidator.debug_assert_paint(); - self.next_frame.dispatch_tree.set_key_context(context); - } - - /// Sets the focus handle for the current element. This handle will be used to manage focus state - /// and keyboard event dispatch for the element. - /// - /// This method should only be called as part of the prepaint phase of element drawing. - pub fn set_focus_handle(&mut self, focus_handle: &FocusHandle, _: &App) { - self.invalidator.debug_assert_prepaint(); - if focus_handle.is_focused(self) { - self.next_frame.focus = Some(focus_handle.id); - } - self.next_frame.dispatch_tree.set_focus_id(focus_handle.id); - } - - /// Sets the view id for the current element, which will be used to manage view caching. - /// - /// This method should only be called as part of element prepaint. We plan on removing this - /// method eventually when we solve some issues that require us to construct editor elements - /// directly instead of always using editors via views. - pub fn set_view_id(&mut self, view_id: EntityId) { - self.invalidator.debug_assert_prepaint(); - self.next_frame.dispatch_tree.set_view_id(view_id); - } - - /// Get the entity ID for the currently rendering view - pub fn current_view(&self) -> EntityId { - self.invalidator.debug_assert_paint_or_prepaint(); - self.rendered_entity_stack.last().copied().unwrap() - } - - #[inline] - pub(crate) fn with_rendered_view( - &mut self, - id: EntityId, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - self.rendered_entity_stack.push(id); - let result = f(self); - self.rendered_entity_stack.pop(); - result - } - - /// Executes the provided function with the specified image cache. - pub fn with_image_cache(&mut self, image_cache: Option, f: F) -> R - where - F: FnOnce(&mut Self) -> R, - { - if let Some(image_cache) = image_cache { - self.image_cache_stack.push(image_cache); - let result = f(self); - self.image_cache_stack.pop(); - result - } else { - f(self) - } - } - - /// Sets an input handler, such as [`ElementInputHandler`][element_input_handler], which interfaces with the - /// platform to receive textual input with proper integration with concerns such - /// as IME interactions. This handler will be active for the upcoming frame until the following frame is - /// rendered. - /// - /// This method should only be called as part of the paint phase of element drawing. - /// - /// [element_input_handler]: crate::ElementInputHandler - pub fn handle_input( - &mut self, - focus_handle: &FocusHandle, - input_handler: impl InputHandler, - cx: &App, - ) { - self.invalidator.debug_assert_paint(); - - if focus_handle.is_focused(self) { - let cx = self.to_async(cx); - self.next_frame - .input_handlers - .push(Some(PlatformInputHandler::new(cx, Box::new(input_handler)))); - } - } - - /// Forwards the focused input handler's [`TextInputConfiguration`] to the - /// platform window when it differs from the last forwarded value. With no - /// input handler the default configuration applies, so a field's - /// preferences don't outlive its focus. - fn apply_text_input_configuration(&mut self, cx: &mut App) { - let configuration = match self.platform_window.take_input_handler() { - Some(mut input_handler) => { - let configuration = input_handler.text_input_configuration(self, cx); - self.platform_window.set_input_handler(input_handler); - configuration - } - None => TextInputConfiguration::default(), - }; - if self.last_text_input_configuration.as_ref() != Some(&configuration) { - self.platform_window - .set_text_input_configuration(configuration.clone()); - self.last_text_input_configuration = Some(configuration); - } - } - - /// Register a mouse event listener on the window for the next frame. The type of event - /// is determined by the first parameter of the given listener. When the next frame is rendered - /// the listener will be cleared. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn on_mouse_event( - &mut self, - mut listener: impl FnMut(&Event, DispatchPhase, &mut Window, &mut App) + 'static, - ) { - self.invalidator.debug_assert_paint(); - - self.next_frame.mouse_listeners.push(Some(Box::new( - move |event: &dyn Any, phase: DispatchPhase, window: &mut Window, cx: &mut App| { - if let Some(event) = event.downcast_ref() { - listener(event, phase, window, cx) - } - }, - ))); - } - - /// Register a key event listener on this node for the next frame. The type of event - /// is determined by the first parameter of the given listener. When the next frame is rendered - /// the listener will be cleared. - /// - /// This is a fairly low-level method, so prefer using event handlers on elements unless you have - /// a specific need to register a listener yourself. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn on_key_event( - &mut self, - listener: impl Fn(&Event, DispatchPhase, &mut Window, &mut App) + 'static, - ) { - self.invalidator.debug_assert_paint(); - - self.next_frame.dispatch_tree.on_key_event(Rc::new( - move |event: &dyn Any, phase, window: &mut Window, cx: &mut App| { - if let Some(event) = event.downcast_ref::() { - listener(event, phase, window, cx) - } - }, - )); - } - - /// Register a modifiers changed event listener on the window for the next frame. - /// - /// This is a fairly low-level method, so prefer using event handlers on elements unless you have - /// a specific need to register a global listener. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn on_modifiers_changed( - &mut self, - listener: impl Fn(&ModifiersChangedEvent, &mut Window, &mut App) + 'static, - ) { - self.invalidator.debug_assert_paint(); - - self.next_frame.dispatch_tree.on_modifiers_changed(Rc::new( - move |event: &ModifiersChangedEvent, window: &mut Window, cx: &mut App| { - listener(event, window, cx) - }, - )); - } - - /// Register a listener to be called when the given focus handle or one of its descendants receives focus. - /// This does not fire if the given focus handle - or one of its descendants - was previously focused. - /// Returns a subscription and persists until the subscription is dropped. - pub fn on_focus_in( - &mut self, - handle: &FocusHandle, - cx: &mut App, - mut listener: impl FnMut(&mut Window, &mut App) + 'static, - ) -> Subscription { - let focus_id = handle.id; - let (subscription, activate) = - self.new_focus_listener(Box::new(move |event, window, cx| { - if event.is_focus_in(focus_id) { - listener(window, cx); - } - true - })); - cx.defer(move |_| activate()); - subscription - } - - /// Register a listener to be called when the given focus handle or one of its descendants loses focus. - /// Returns a subscription and persists until the subscription is dropped. - pub fn on_focus_out( - &mut self, - handle: &FocusHandle, - cx: &mut App, - mut listener: impl FnMut(FocusOutEvent, &mut Window, &mut App) + 'static, - ) -> Subscription { - let focus_id = handle.id; - let (subscription, activate) = - self.new_focus_listener(Box::new(move |event, window, cx| { - if let Some(blurred_id) = event.previous_focus_path.last().copied() - && event.is_focus_out(focus_id) - { - let event = FocusOutEvent { - blurred: WeakFocusHandle { - id: blurred_id, - handles: Arc::downgrade(&cx.focus_handles), - }, - }; - listener(event, window, cx) - } - true - })); - cx.defer(move |_| activate()); - subscription - } - - fn reset_cursor_style(&self, cx: &mut App) { - // Set the cursor only if we're the active window. - if self.is_window_hovered() { - let style = self - .rendered_frame - .cursor_style(self) - .unwrap_or(CursorStyle::Arrow); - cx.platform.set_cursor_style(style); - } - } - - /// Dispatch a given keystroke as though the user had typed it. - /// You can create a keystroke with Keystroke::parse(""). - pub fn dispatch_keystroke(&mut self, keystroke: Keystroke, cx: &mut App) -> bool { - let keystroke = keystroke.with_simulated_ime(); - let result = self.dispatch_event( - PlatformInput::KeyDown(KeyDownEvent { - keystroke: keystroke.clone(), - is_held: false, - prefer_character_input: false, - }), - cx, - ); - if !result.propagate { - return true; - } - - if let Some(input) = keystroke.key_char - && let Some(mut input_handler) = self.platform_window.take_input_handler() - { - input_handler.dispatch_input(&input, self, cx); - self.platform_window.set_input_handler(input_handler); - return true; - } - - false - } - - /// Return a key binding string for an action, to display in the UI. Uses the highest precedence - /// binding for the action (last binding added to the keymap). - pub fn keystroke_text_for(&self, action: &dyn Action) -> String { - self.highest_precedence_binding_for_action(action) - .map(|binding| { - binding - .keystrokes() - .iter() - .map(ToString::to_string) - .collect::>() - .join(" ") - }) - .unwrap_or_else(|| action.name().to_string()) - } - - /// Dispatch a mouse, keyboard, or touch event on the window. - #[profiling::function] - pub fn dispatch_event(&mut self, event: PlatformInput, cx: &mut App) -> DispatchEventResult { - #[cfg(feature = "profiler")] - self.window_profiler.begin_input(event.kind_name()); - let update_count_before = self.invalidator.update_count(); - // Track input modality for focus-visible styling and hover suppression. - // Hover is suppressed during keyboard modality so that keyboard navigation - // doesn't show hover highlights on the item under the mouse cursor. - let old_modality = self.last_input_modality; - self.last_input_modality = match &event { - PlatformInput::KeyDown(_) => InputModality::Keyboard, - PlatformInput::MouseMove(_) | PlatformInput::MouseDown(_) => InputModality::Mouse, - PlatformInput::Touch(_) => InputModality::Touch, - _ => self.last_input_modality, - }; - if self.last_input_modality != old_modality { - self.refresh(); - } - - // Handlers may set this to false by calling `stop_propagation`. - cx.propagate_event = true; - // Handlers may set this to true by calling `prevent_default`. - self.default_prevented = false; - - let event = match event { - // Track the mouse position with our own state, since accessing the platform - // API for the mouse position can only occur on the main thread. - PlatformInput::MouseMove(mouse_move) => { - self.mouse_position = mouse_move.position; - self.modifiers = mouse_move.modifiers; - PlatformInput::MouseMove(mouse_move) - } - PlatformInput::MouseDown(mouse_down) => { - self.mouse_position = mouse_down.position; - self.modifiers = mouse_down.modifiers; - PlatformInput::MouseDown(mouse_down) - } - PlatformInput::MouseUp(mouse_up) => { - self.mouse_position = mouse_up.position; - self.modifiers = mouse_up.modifiers; - PlatformInput::MouseUp(mouse_up) - } - PlatformInput::MousePressure(mouse_pressure) => { - PlatformInput::MousePressure(mouse_pressure) - } - PlatformInput::MouseExited(mouse_exited) => { - self.modifiers = mouse_exited.modifiers; - PlatformInput::MouseExited(mouse_exited) - } - PlatformInput::ModifiersChanged(modifiers_changed) => { - self.modifiers = modifiers_changed.modifiers; - self.capslock = modifiers_changed.capslock; - PlatformInput::ModifiersChanged(modifiers_changed) - } - PlatformInput::ScrollWheel(scroll_wheel) => { - self.mouse_position = scroll_wheel.position; - self.modifiers = scroll_wheel.modifiers; - PlatformInput::ScrollWheel(scroll_wheel) - } - PlatformInput::Pinch(pinch) => { - self.mouse_position = pinch.position; - self.modifiers = pinch.modifiers; - PlatformInput::Pinch(pinch) - } - // Translate dragging and dropping of external files from the operating system - // to internal drag and drop events. - PlatformInput::FileDrop(file_drop) => match file_drop { - FileDropEvent::Entered { position, paths } => { - self.mouse_position = position; - let source_window = self.handle.window_id(); - if !cx.restore_platform_drag(source_window) && cx.active_drag.is_none() { - cx.active_drag = Some(AnyDrag { - value: Arc::new(paths.clone()), - view: cx.new(|_| paths).into(), - cursor_offset: position, - cursor_style: None, - external_payload_source: None, - }); - } - PlatformInput::MouseMove(MouseMoveEvent { - position, - pressed_button: Some(MouseButton::Left), - modifiers: Modifiers::default(), - }) - } - FileDropEvent::Pending { position } => { - self.mouse_position = position; - PlatformInput::MouseMove(MouseMoveEvent { - position, - pressed_button: Some(MouseButton::Left), - modifiers: Modifiers::default(), - }) - } - FileDropEvent::Submit { position } => { - cx.activate(true); - self.mouse_position = position; - PlatformInput::MouseUp(MouseUpEvent { - button: MouseButton::Left, - position, - modifiers: Modifiers::default(), - click_count: 1, - }) - } - FileDropEvent::Exited => { - if !cx.hand_restored_drag_to_platform(self.handle.window_id()) { - cx.active_drag.take(); - } - self.refresh(); - PlatformInput::FileDrop(FileDropEvent::Exited) - } - FileDropEvent::Ended => { - cx.end_platform_drag(self.handle.window_id()); - self.refresh(); - PlatformInput::FileDrop(FileDropEvent::Ended) - } - }, - PlatformInput::Touch(touch) => PlatformInput::Touch(touch), - PlatformInput::LongPress(long_press) => { - self.mouse_position = if long_press.phase == crate::TouchPhase::Started { - long_press.start_position - } else { - long_press.position - }; - if long_press.phase == crate::TouchPhase::Started { - self.long_press_capture = None; - } - PlatformInput::LongPress(long_press) - } - PlatformInput::TouchDrag(touch_drag) => { - self.mouse_position = touch_drag.start_position; - PlatformInput::TouchDrag(touch_drag) - } - PlatformInput::KeyDown(_) | PlatformInput::KeyUp(_) => event, - }; - - if let Some(any_mouse_event) = event.mouse_event() { - self.dispatch_mouse_event(any_mouse_event, cx); - } else if let Some(any_key_event) = event.keyboard_event() { - self.dispatch_key_event(any_key_event, cx); - } else if let Some(touch_event) = event.touch_event() { - self.dispatch_touch_event(touch_event, cx); - } - if let PlatformInput::LongPress(long_press) = &event { - match long_press.phase { - crate::TouchPhase::Started if !self.default_prevented => { - self.long_press_capture = None; - } - crate::TouchPhase::Ended | crate::TouchPhase::Cancelled => { - self.long_press_capture = None; - } - crate::TouchPhase::Started | crate::TouchPhase::Moved => {} - } - } - - // Must run after the move is dispatched: the platform owns the gesture afterwards, so this - // is the last chance for drag listeners to see the pointer leave and reset their state. - self.promote_external_drag_to_platform(&event, cx); - - let caused_invalidation = self.invalidator.update_count() > update_count_before; - if caused_invalidation { - self.input_rate_tracker.borrow_mut().record_input(); - } - #[cfg(feature = "profiler")] - self.window_profiler.end_input(caused_invalidation); - - DispatchEventResult { - propagate: cx.propagate_event, - default_prevented: self.default_prevented, - } - } - - fn promote_external_drag_to_platform(&mut self, event: &PlatformInput, cx: &mut App) { - let PlatformInput::MouseMove(mouse_move) = event else { - return; - }; - if mouse_move.pressed_button != Some(MouseButton::Left) { - return; - } - if Bounds::new(Point::default(), self.viewport_size).contains(&mouse_move.position) { - return; - } - if !self.platform_window.can_start_external_drag() { - return; - } - let Some(payload_source) = cx - .active_drag - .as_mut() - .and_then(|drag| drag.external_payload_source.take()) - else { - return; - }; - let Some(payload) = payload_source(self, cx) else { - return; - }; - if self.platform_window.start_external_drag(&payload) - && cx.hand_active_drag_to_platform(self.handle.window_id()) - { - self.refresh(); - } - } - - /// Whether recognized touch pans may use the platform's predicted touch - /// positions ([`TouchEvent::predicted_position`]) to compensate for input - /// latency. Defaults to true. - pub fn touch_prediction_enabled(&self) -> bool { - self.touch_prediction_enabled - } - - /// Sets whether recognized touch pans may use the platform's predicted - /// touch positions. Disabling drops [`TouchEvent::predicted_position`] - /// before gesture recognition, so pans track only raw touch positions. - pub fn set_touch_prediction_enabled(&mut self, enabled: bool) { - self.touch_prediction_enabled = enabled; - } - - /// Runs the portable gesture recognizer over a raw touch event and - /// dispatches whatever it resolves (scroll steps, synthesized taps) - /// through the ordinary mouse-event path. - fn dispatch_touch_event(&mut self, event: &TouchEvent, cx: &mut App) { - let mut event = event.clone(); - if !self.touch_prediction_enabled { - event.predicted_position = None; - } - let recognized_gestures = self.touch_gestures.handle_event(&event); - if event.phase == crate::TouchPhase::Started - && let Some(touch_drag) = self.touch_gestures.offer_touch_drag(event.id) - { - self.dispatch_recognized_touch_gesture(touch_drag, cx); - } - if event.phase == crate::TouchPhase::Started - && self.touch_gestures.pending_long_press().is_some() - { - self.long_press_capture = None; - } - let mut tapped = false; - for gesture in recognized_gestures { - tapped |= matches!(gesture, RecognizedTouchGesture::Tap { .. }); - self.dispatch_recognized_touch_gesture(gesture, cx); - } - if event.phase == crate::TouchPhase::Started { - self.schedule_long_press_timer(cx); - } else if self.touch_gestures.pending_long_press().is_none() { - self.long_press_timer.take(); - } - // The platform's touch-release handler may inspect the input handler - // as soon as this dispatch returns (the web platform decides virtual - // keyboard visibility there, inside the user gesture). Input handlers - // are registered during draw, so draw now to make them reflect any - // focus change the tap just caused. - if tapped && self.invalidator.is_dirty() { - self.draw(cx).clear(cx); - } - if self.touch_gestures.has_momentum() { - self.schedule_touch_momentum_tick(); - } - } - - fn dispatch_recognized_touch_gesture(&mut self, gesture: RecognizedTouchGesture, cx: &mut App) { - match gesture { - RecognizedTouchGesture::Scroll(scroll_wheel) => { - self.mouse_position = scroll_wheel.position; - cx.propagate_event = true; - self.dispatch_mouse_event(&scroll_wheel, cx); - } - RecognizedTouchGesture::Tap { down, up } => { - self.mouse_position = up.position; - cx.propagate_event = true; - self.dispatch_mouse_event(&down, cx); - cx.propagate_event = true; - self.dispatch_mouse_event(&up, cx); - } - RecognizedTouchGesture::TouchDrag(touch_drag) => { - self.mouse_position = touch_drag.start_position; - cx.propagate_event = true; - self.default_prevented = false; - let started = touch_drag.phase == crate::TouchPhase::Started; - self.dispatch_mouse_event(&touch_drag, cx); - if started { - self.touch_gestures - .resolve_touch_drag(self.default_prevented); - } - } - RecognizedTouchGesture::LongPress(long_press) => { - self.mouse_position = if long_press.phase == crate::TouchPhase::Started { - long_press.start_position - } else { - long_press.position - }; - cx.propagate_event = true; - self.default_prevented = false; - let started = long_press.phase == crate::TouchPhase::Started; - let ended = matches!( - long_press.phase, - crate::TouchPhase::Ended | crate::TouchPhase::Cancelled - ); - self.dispatch_mouse_event(&long_press, cx); - if started { - let claimed = self.default_prevented; - self.touch_gestures.resolve_long_press(claimed); - if !claimed { - self.long_press_capture = None; - } - } - if ended { - self.long_press_capture = None; - } - } - } - } - - fn schedule_long_press_timer(&mut self, cx: &mut App) { - self.long_press_timer.take(); - let Some((touch_id, duration)) = self.touch_gestures.pending_long_press() else { - return; - }; - self.long_press_timer = Some(self.spawn(cx, async move |cx| { - cx.background_executor.timer(duration).await; - cx.update(move |window, cx| { - window.long_press_timer.take(); - if let Some(gesture) = window.touch_gestures.offer_long_press(touch_id) { - window.dispatch_recognized_touch_gesture(gesture, cx); - } - }) - .log_err(); - })); - } - - fn schedule_touch_momentum_tick(&mut self) { - self.on_next_frame(|window, cx| { - if let Some(gesture) = window.touch_gestures.tick_momentum() { - window.dispatch_recognized_touch_gesture(gesture, cx); - } - if window.touch_gestures.has_momentum() { - window.schedule_touch_momentum_tick(); - } - }); - } - - fn dispatch_mouse_event(&mut self, event: &dyn Any, cx: &mut App) { - let hit_test = self.rendered_frame.hit_test(self.mouse_position()); - if hit_test != self.mouse_hit_test { - self.mouse_hit_test = hit_test; - self.reset_cursor_style(cx); - } - - #[cfg(any(feature = "inspector", debug_assertions))] - if self.is_inspector_picking(cx) { - self.handle_inspector_mouse_event(event, cx); - // When inspector is picking, all other mouse handling is skipped. - return; - } - - let mut mouse_listeners = mem::take(&mut self.rendered_frame.mouse_listeners); - - // Capture phase, events bubble from back to front. Handlers for this phase are used for - // special purposes, such as detecting events outside of a given Bounds. - for listener in &mut mouse_listeners { - let listener = listener.as_mut().unwrap(); - listener(event, DispatchPhase::Capture, self, cx); - if !cx.propagate_event { - break; - } - } - - // Bubble phase, where most normal handlers do their work. - if cx.propagate_event { - for listener in mouse_listeners.iter_mut().rev() { - let listener = listener.as_mut().unwrap(); - listener(event, DispatchPhase::Bubble, self, cx); - if !cx.propagate_event { - break; - } - } - } - - self.rendered_frame.mouse_listeners = mouse_listeners; - - if cx.has_active_drag() { - if event.is::() { - // If this was a mouse move event, redraw the window so that the - // active drag can follow the mouse cursor. - self.refresh(); - } else if event.is::() { - // If this was a mouse up event, cancel the active drag and redraw - // the window. - cx.active_drag = None; - self.refresh(); - } - } - - // Auto-release pointer capture on mouse up - if event.is::() && self.captured_hitbox.is_some() { - self.captured_hitbox = None; - } - } - - fn dispatch_key_event(&mut self, event: &dyn Any, cx: &mut App) { - if self.invalidator.is_dirty() { - self.draw(cx).clear(cx); - } - - let node_id = self.focus_node_id_in_rendered_frame(self.focus); - let dispatch_path = self.rendered_frame.dispatch_tree.dispatch_path(node_id); - - let mut keystroke: Option = None; - - if let Some(event) = event.downcast_ref::() { - if event.modifiers.number_of_modifiers() == 0 - && self.pending_modifier.modifiers.number_of_modifiers() == 1 - && !self.pending_modifier.saw_other_input - { - let key = match self.pending_modifier.modifiers { - modifiers if modifiers.shift => Some("shift"), - modifiers if modifiers.control => Some("control"), - modifiers if modifiers.alt => Some("alt"), - modifiers if modifiers.platform => Some("platform"), - modifiers if modifiers.function => Some("function"), - _ => None, - }; - if let Some(key) = key { - keystroke = Some(Keystroke { - key: key.to_string(), - key_char: None, - modifiers: Modifiers::default(), - }); - } - } - - if self.pending_modifier.modifiers.number_of_modifiers() == 0 - && event.modifiers.number_of_modifiers() == 1 - { - self.pending_modifier.saw_other_input = false - } else if event.modifiers.number_of_modifiers() > 1 { - self.pending_modifier.saw_other_input = true - } - self.pending_modifier.modifiers = event.modifiers - } else if let Some(key_down_event) = event.downcast_ref::() { - self.pending_modifier.saw_other_input = true; - keystroke = Some(key_down_event.keystroke.clone()); - if key_down_event.keystroke.key_char.is_some() - && matches!( - cx.cursor_hide_mode, - CursorHideMode::OnTyping | CursorHideMode::OnTypingAndAction - ) - { - cx.platform.hide_cursor_until_mouse_moves(); - } - } - - let Some(keystroke) = keystroke else { - self.finish_dispatch_key_event(event, dispatch_path, self.context_stack(), cx); - return; - }; - - cx.propagate_event = true; - self.dispatch_keystroke_interceptors(event, self.context_stack(), cx); - if !cx.propagate_event { - self.finish_dispatch_key_event(event, dispatch_path, self.context_stack(), cx); - return; - } - - let mut currently_pending = self.pending_input.take().unwrap_or_default(); - if currently_pending.focus.is_some() && currently_pending.focus != self.focus { - currently_pending = PendingInput::default(); - } - - let match_result = self.rendered_frame.dispatch_tree.dispatch_key( - currently_pending.keystrokes, - keystroke, - &dispatch_path, - ); - - if !match_result.to_replay.is_empty() { - self.replay_pending_input(match_result.to_replay, cx); - cx.propagate_event = true; - } - - if !match_result.pending.is_empty() { - let previous_timeout = currently_pending.timeout.take(); - currently_pending.keystrokes = match_result.pending; - currently_pending.focus = self.focus; - - let text_input_requires_timeout = event - .downcast_ref::() - .filter(|key_down| key_down.keystroke.key_char.is_some()) - .and_then(|_| self.platform_window.take_input_handler()) - .map_or(false, |mut input_handler| { - let accepts = input_handler.accepts_text_input(self, cx); - self.platform_window.set_input_handler(input_handler); - accepts - }); - - let needs_timeout = previous_timeout.is_some() - || match_result.pending_has_binding - || text_input_requires_timeout; - currently_pending.timeout = if needs_timeout { - match previous_timeout { - Some(mut timeout) if timeout.is_paused() => { - timeout.reset_duration(PENDING_INPUT_TIMEOUT); - Some(timeout) - } - previous_timeout => { - drop(previous_timeout); - Some(self.new_pending_input_timeout(PENDING_INPUT_TIMEOUT, cx)) - } - } - } else { - None - }; - self.pending_input = Some(currently_pending); - self.pending_input_changed(cx); - cx.propagate_event = false; - return; - } - - let skip_bindings = event - .downcast_ref::() - .filter(|key_down_event| key_down_event.prefer_character_input) - .map(|_| { - self.platform_window - .take_input_handler() - .map_or(false, |mut input_handler| { - let accepts = input_handler.accepts_text_input(self, cx); - self.platform_window.set_input_handler(input_handler); - // If modifiers are not excessive (e.g. AltGr), and the input handler is accepting text input, - // we prefer the text input over bindings. - accepts - }) - }) - .unwrap_or(false); - - if !skip_bindings { - for binding in match_result.bindings { - self.dispatch_action_on_node(node_id, binding.action.as_ref(), cx); - if !cx.propagate_event { - self.dispatch_keystroke_observers( - event, - Some(binding.action), - match_result.context_stack, - cx, - ); - self.pending_input_changed(cx); - return; - } - } - } - - self.finish_dispatch_key_event(event, dispatch_path, match_result.context_stack, cx); - self.pending_input_changed(cx); - } - - fn new_pending_input_timeout(&self, duration: Duration, cx: &App) -> PendingInputTimeout { - let (started_at, task) = self.start_pending_input_timeout(duration, cx); - PendingInputTimeout { - duration, - remaining: duration, - state: PendingInputTimeoutState::Running { started_at, task }, - } - } - - fn start_pending_input_timeout(&self, remaining: Duration, cx: &App) -> (Instant, Task<()>) { - let started_at = cx.background_executor().now(); - let task = self.spawn(cx, async move |cx| { - cx.background_executor.timer(remaining).await; - cx.update(move |window, cx| { - let Some(currently_pending) = window - .pending_input - .take() - .filter(|pending| pending.focus == window.focus) - else { - return; - }; - - let node_id = window.focus_node_id_in_rendered_frame(window.focus); - let dispatch_path = window.rendered_frame.dispatch_tree.dispatch_path(node_id); - - let to_replay = window - .rendered_frame - .dispatch_tree - .flush_dispatch(currently_pending.keystrokes, &dispatch_path); - - window.pending_input_changed(cx); - window.replay_pending_input(to_replay, cx) - }) - .log_err(); - }); - (started_at, task) - } - - fn finish_dispatch_key_event( - &mut self, - event: &dyn Any, - dispatch_path: SmallVec<[DispatchNodeId; 32]>, - context_stack: Vec, - cx: &mut App, - ) { - self.dispatch_key_down_up_event(event, &dispatch_path, cx); - if !cx.propagate_event { - return; - } - - self.dispatch_modifiers_changed_event(event, &dispatch_path, cx); - if !cx.propagate_event { - return; - } - - self.dispatch_keystroke_observers(event, None, context_stack, cx); - } - - pub(crate) fn pending_input_changed(&mut self, cx: &mut App) { - self.pending_input_observers - .clone() - .retain(&(), |callback| callback(self, cx)); - } - - fn defer_pending_input_changed(&self, cx: &mut App) { - // Avoid re-entrant entity updates by deferring observer notifications to the end of the - // current effect cycle, and only for this window. - let window_handle = self.handle; - cx.defer(move |cx| { - window_handle - .update(cx, |_, window, cx| { - window.pending_input_changed(cx); - }) - .ok(); - }); - } - - fn dispatch_key_down_up_event( - &mut self, - event: &dyn Any, - dispatch_path: &SmallVec<[DispatchNodeId; 32]>, - cx: &mut App, - ) { - // Capture phase - for node_id in dispatch_path { - let node = self.rendered_frame.dispatch_tree.node(*node_id); - - for key_listener in node.key_listeners.clone() { - key_listener(event, DispatchPhase::Capture, self, cx); - if !cx.propagate_event { - return; - } - } - } - - // Bubble phase - for node_id in dispatch_path.iter().rev() { - // Handle low level key events - let node = self.rendered_frame.dispatch_tree.node(*node_id); - for key_listener in node.key_listeners.clone() { - key_listener(event, DispatchPhase::Bubble, self, cx); - if !cx.propagate_event { - return; - } - } - } - } - - fn dispatch_modifiers_changed_event( - &mut self, - event: &dyn Any, - dispatch_path: &SmallVec<[DispatchNodeId; 32]>, - cx: &mut App, - ) { - let Some(event) = event.downcast_ref::() else { - return; - }; - for node_id in dispatch_path.iter().rev() { - let node = self.rendered_frame.dispatch_tree.node(*node_id); - for listener in node.modifiers_changed_listeners.clone() { - listener(event, self, cx); - if !cx.propagate_event { - return; - } - } - } - } - - /// Determine whether a potential multi-stroke key binding is in progress on this window. - pub fn has_pending_keystrokes(&self) -> bool { - self.pending_input().is_some() - } - - #[cfg(test)] - pub(crate) fn pending_input_is_none(&self) -> bool { - self.pending_input.is_none() - } - - pub(crate) fn clear_pending_keystrokes(&mut self, cx: &mut App) { - if self.pending_input.take().is_some() { - self.defer_pending_input_changed(cx); - } - } - - /// Returns pending input that can still complete a multi-stroke key binding. Input left over - /// from a previous focus can never complete one. - pub fn pending_input(&self) -> Option> { - self.pending_input - .as_ref() - .filter(|pending_input| pending_input.focus == self.focus) - .map(|pending_input| PendingInputStatus { - keystrokes: pending_input.keystrokes.as_slice(), - timeout: pending_input - .timeout - .as_ref() - .map(PendingInputTimeout::status), - }) - } - - /// Pauses or resumes the current pending input timeout on behalf of `owner`. - /// - /// A paused timeout resumes automatically if `owner` is released. Returns whether the timeout - /// state changed. A timeout paused by one owner cannot be resumed by another. - pub fn set_pending_input_timeout_paused( - &mut self, - owner: &Entity, - paused: bool, - cx: &mut App, - ) -> bool { - let owner_id = owner.entity_id(); - if !paused { - return self.resume_pending_input_timeout(owner_id, cx); - } - - let timeout = self - .pending_input - .as_ref() - .filter(|pending_input| pending_input.focus == self.focus) - .and_then(|pending_input| pending_input.timeout.as_ref()); - let Some(timeout) = timeout else { - return false; - }; - if timeout.is_paused() { - return false; - } - - let release_subscription = self.observe_release(owner, cx, move |_, window, cx| { - window.resume_pending_input_timeout(owner_id, cx); - }); - let now = cx.background_executor().now(); - let changed = self - .pending_input - .as_mut() - .filter(|pending_input| pending_input.focus == self.focus) - .and_then(|pending_input| pending_input.timeout.as_mut()) - .is_some_and(|timeout| { - timeout.pause( - PendingInputTimeoutPause { - owner_id, - _release_subscription: release_subscription, - }, - now, - ) - }); - - if changed { - self.defer_pending_input_changed(cx); - } - changed - } - - fn resume_pending_input_timeout(&mut self, owner_id: EntityId, cx: &mut App) -> bool { - let Some(remaining) = self - .pending_input - .as_ref() - .and_then(|pending_input| pending_input.timeout.as_ref()) - .filter(|timeout| timeout.pause_owner_id() == Some(owner_id)) - .map(|timeout| timeout.remaining) - else { - return false; - }; - - let (started_at, task) = self.start_pending_input_timeout(remaining, cx); - let changed = self - .pending_input - .as_mut() - .and_then(|pending_input| pending_input.timeout.as_mut()) - .is_some_and(|timeout| timeout.resume(owner_id, started_at, task)); - - if changed { - self.defer_pending_input_changed(cx); - } - changed - } - - /// Returns the currently pending input keystrokes that might result in a multi-stroke key binding. - pub fn pending_input_keystrokes(&self) -> Option<&[Keystroke]> { - self.pending_input() - .map(|pending_input| pending_input.keystrokes()) - } - - fn replay_pending_input(&mut self, replays: SmallVec<[Replay; 1]>, cx: &mut App) { - let node_id = self.focus_node_id_in_rendered_frame(self.focus); - let dispatch_path = self.rendered_frame.dispatch_tree.dispatch_path(node_id); - - 'replay: for replay in replays { - let event = KeyDownEvent { - keystroke: replay.keystroke.clone(), - is_held: false, - prefer_character_input: true, - }; - - cx.propagate_event = true; - for binding in replay.bindings { - self.dispatch_action_on_node(node_id, binding.action.as_ref(), cx); - if !cx.propagate_event { - self.dispatch_keystroke_observers( - &event, - Some(binding.action), - Vec::default(), - cx, - ); - continue 'replay; - } - } - - self.dispatch_key_down_up_event(&event, &dispatch_path, cx); - if !cx.propagate_event { - continue 'replay; - } - if let Some(input) = replay.keystroke.key_char.as_ref().cloned() - && let Some(mut input_handler) = self.platform_window.take_input_handler() - { - input_handler.dispatch_input(&input, self, cx); - self.platform_window.set_input_handler(input_handler) - } - } - } - - fn focus_node_id_in_rendered_frame(&self, focus_id: Option) -> DispatchNodeId { - focus_id - .and_then(|focus_id| { - self.rendered_frame - .dispatch_tree - .focusable_node_id(focus_id) - }) - .unwrap_or_else(|| self.rendered_frame.dispatch_tree.root_node_id()) - } - - fn dispatch_action_on_node( - &mut self, - node_id: DispatchNodeId, - action: &dyn Action, - cx: &mut App, - ) { - self.dispatch_action_on_node_inner(node_id, action, cx); - - if !cx.propagate_event - && cx.cursor_hide_mode == CursorHideMode::OnTypingAndAction - && self.last_input_was_keyboard() - { - cx.platform.hide_cursor_until_mouse_moves(); - } - } - - fn dispatch_action_on_node_inner( - &mut self, - node_id: DispatchNodeId, - action: &dyn Action, - cx: &mut App, - ) { - let dispatch_path = self.rendered_frame.dispatch_tree.dispatch_path(node_id); - - // Capture phase for global actions. - cx.propagate_event = true; - if let Some(mut global_listeners) = cx - .global_action_listeners - .remove(&action.as_any().type_id()) - { - for listener in &global_listeners { - #[cfg(feature = "profiler")] - self.window_profiler.begin_action_handler(action, cx); - listener(action.as_any(), DispatchPhase::Capture, cx); - #[cfg(feature = "profiler")] - self.window_profiler.end_action_handler(); - if !cx.propagate_event { - break; - } - } - - global_listeners.extend( - cx.global_action_listeners - .remove(&action.as_any().type_id()) - .unwrap_or_default(), - ); - - cx.global_action_listeners - .insert(action.as_any().type_id(), global_listeners); - } - - if !cx.propagate_event { - return; - } - - // Capture phase for window actions. - for node_id in &dispatch_path { - let node = self.rendered_frame.dispatch_tree.node(*node_id); - for DispatchActionListener { - action_type, - listener, - } in node.action_listeners.clone() - { - let any_action = action.as_any(); - if action_type == any_action.type_id() { - #[cfg(feature = "profiler")] - self.window_profiler.begin_action_handler(action, cx); - listener(any_action, DispatchPhase::Capture, self, cx); - #[cfg(feature = "profiler")] - self.window_profiler.end_action_handler(); - - if !cx.propagate_event { - return; - } - } - } - } - - // Bubble phase for window actions. - for node_id in dispatch_path.iter().rev() { - let node = self.rendered_frame.dispatch_tree.node(*node_id); - for DispatchActionListener { - action_type, - listener, - } in node.action_listeners.clone() - { - let any_action = action.as_any(); - if action_type == any_action.type_id() { - cx.propagate_event = false; // Actions stop propagation by default during the bubble phase - #[cfg(feature = "profiler")] - self.window_profiler.begin_action_handler(action, cx); - listener(any_action, DispatchPhase::Bubble, self, cx); - #[cfg(feature = "profiler")] - self.window_profiler.end_action_handler(); - - if !cx.propagate_event { - return; - } - } - } - } - - // Bubble phase for global actions. - if let Some(mut global_listeners) = cx - .global_action_listeners - .remove(&action.as_any().type_id()) - { - for listener in global_listeners.iter().rev() { - cx.propagate_event = false; // Actions stop propagation by default during the bubble phase - - #[cfg(feature = "profiler")] - self.window_profiler.begin_action_handler(action, cx); - listener(action.as_any(), DispatchPhase::Bubble, cx); - #[cfg(feature = "profiler")] - self.window_profiler.end_action_handler(); - if !cx.propagate_event { - break; - } - } - - global_listeners.extend( - cx.global_action_listeners - .remove(&action.as_any().type_id()) - .unwrap_or_default(), - ); - - cx.global_action_listeners - .insert(action.as_any().type_id(), global_listeners); - } - } - - /// Register the given handler to be invoked whenever the global of the given type - /// is updated. - pub fn observe_global( - &mut self, - cx: &mut App, - f: impl Fn(&mut Window, &mut App) + 'static, - ) -> Subscription { - let window_handle = self.handle; - let (subscription, activate) = cx.global_observers.insert( - TypeId::of::(), - Box::new(move |cx| { - window_handle - .update(cx, |_, window, cx| f(window, cx)) - .is_ok() - }), - ); - cx.defer(move |_| activate()); - subscription - } - - /// Focus the current window and bring it to the foreground at the platform level. - pub fn activate_window(&self) { - self.platform_window.activate(); - } - - /// Requests that the operating system draw attention to this window. - pub fn request_attention(&self) { - self.platform_window.request_attention(); - } - - /// Minimize the current window at the platform level. - pub fn minimize_window(&self) { - self.platform_window.minimize(); - } - - /// Toggle full screen status on the current window at the platform level. - pub fn toggle_fullscreen(&self) { - self.platform_window.toggle_fullscreen(); - } - - /// Toggle simple (borderless) fullscreen, where the window covers the entire - /// screen including the menu bar and, on notched displays, the area around the - /// notch. Unlike [`Window::toggle_fullscreen`], this does not move the window - /// into its own Mission Control space. Only has an effect on macOS. - pub fn toggle_simple_fullscreen(&self) { - self.platform_window.toggle_simple_fullscreen(); - } - - /// Updates the IME panel position suggestions for languages like japanese, chinese. - pub fn invalidate_character_coordinates(&self) { - self.on_next_frame(|window, cx| { - if let Some(mut input_handler) = window.platform_window.take_input_handler() { - if let Some(bounds) = input_handler.selected_bounds(window, cx) { - window.platform_window.update_ime_position(bounds); - } - window.platform_window.set_input_handler(input_handler); - } - }); - } - - /// Present a platform dialog. - /// The provided message will be presented, along with buttons for each answer. - /// When a button is clicked, the returned Receiver will receive the index of the clicked button. - pub fn prompt( - &mut self, - level: PromptLevel, - message: &str, - detail: Option<&str>, - answers: &[T], - cx: &mut App, - ) -> oneshot::Receiver - where - T: Clone + Into, - { - let prompt_builder = cx.prompt_builder.take(); - let Some(prompt_builder) = prompt_builder else { - unreachable!("Re-entrant window prompting is not supported by GPUI"); - }; - - let answers = answers - .iter() - .map(|answer| answer.clone().into()) - .collect::>(); - - let receiver = match &prompt_builder { - PromptBuilder::Default => self - .platform_window - .prompt(level, message, detail, &answers) - .unwrap_or_else(|| { - self.build_custom_prompt(&prompt_builder, level, message, detail, &answers, cx) - }), - PromptBuilder::Custom(_) => { - self.build_custom_prompt(&prompt_builder, level, message, detail, &answers, cx) - } - }; - - cx.prompt_builder = Some(prompt_builder); - - receiver - } - - fn build_custom_prompt( - &mut self, - prompt_builder: &PromptBuilder, - level: PromptLevel, - message: &str, - detail: Option<&str>, - answers: &[PromptButton], - cx: &mut App, - ) -> oneshot::Receiver { - let (sender, receiver) = oneshot::channel(); - let handle = PromptHandle::new(sender); - let handle = (prompt_builder)(level, message, detail, answers, handle, self, cx); - self.prompt = Some(handle); - receiver - } - - /// Returns whether a prompt rendered by GPUI is currently active in this window. - /// - /// This is only true for prompts rendered in the window (see - /// [`App::set_prompt_builder`]), not for platform-native prompt dialogs. - pub fn has_active_prompt(&self) -> bool { - self.prompt.is_some() - } - - /// Returns the current context stack. - pub fn context_stack(&self) -> Vec { - let node_id = self.focus_node_id_in_rendered_frame(self.focus); - let dispatch_tree = &self.rendered_frame.dispatch_tree; - dispatch_tree - .dispatch_path(node_id) - .iter() - .filter_map(move |&node_id| dispatch_tree.node(node_id).context.clone()) - .collect() - } - - /// Returns all available actions for the focused element. - pub fn available_actions(&self, cx: &App) -> Vec> { - let node_id = self.focus_node_id_in_rendered_frame(self.focus); - let mut actions = self.rendered_frame.dispatch_tree.available_actions(node_id); - for action_type in cx.global_action_listeners.keys() { - if let Err(ix) = actions.binary_search_by_key(action_type, |a| a.as_any().type_id()) { - let action = cx.actions.build_action_type(action_type).ok(); - if let Some(action) = action { - actions.insert(ix, action); - } - } - } - actions - } - - /// Returns key bindings that invoke an action on the currently focused element. Bindings are - /// returned in the order they were added. For display, the last binding should take precedence. - pub fn bindings_for_action(&self, action: &dyn Action) -> Vec { - self.rendered_frame - .dispatch_tree - .bindings_for_action(action, &self.rendered_frame.dispatch_tree.context_stack) - } - - /// Returns the highest precedence key binding that invokes an action on the currently focused - /// element. This is more efficient than getting the last result of `bindings_for_action`. - pub fn highest_precedence_binding_for_action(&self, action: &dyn Action) -> Option { - self.rendered_frame - .dispatch_tree - .highest_precedence_binding_for_action( - action, - &self.rendered_frame.dispatch_tree.context_stack, - ) - } - - /// Returns the key bindings for an action in a context. - pub fn bindings_for_action_in_context( - &self, - action: &dyn Action, - context: KeyContext, - ) -> Vec { - let dispatch_tree = &self.rendered_frame.dispatch_tree; - dispatch_tree.bindings_for_action(action, &[context]) - } - - /// Returns the highest precedence key binding for an action in a context. This is more - /// efficient than getting the last result of `bindings_for_action_in_context`. - pub fn highest_precedence_binding_for_action_in_context( - &self, - action: &dyn Action, - context: KeyContext, - ) -> Option { - let dispatch_tree = &self.rendered_frame.dispatch_tree; - dispatch_tree.highest_precedence_binding_for_action(action, &[context]) - } - - /// Returns any bindings that would invoke an action on the given focus handle if it were - /// focused. Bindings are returned in the order they were added. For display, the last binding - /// should take precedence. - pub fn bindings_for_action_in( - &self, - action: &dyn Action, - focus_handle: &FocusHandle, - ) -> Vec { - let dispatch_tree = &self.rendered_frame.dispatch_tree; - let Some(context_stack) = self.context_stack_for_focus_handle(focus_handle) else { - return vec![]; - }; - dispatch_tree.bindings_for_action(action, &context_stack) - } - - /// Returns the highest precedence key binding that would invoke an action on the given focus - /// handle if it were focused. This is more efficient than getting the last result of - /// `bindings_for_action_in`. - pub fn highest_precedence_binding_for_action_in( - &self, - action: &dyn Action, - focus_handle: &FocusHandle, - ) -> Option { - let dispatch_tree = &self.rendered_frame.dispatch_tree; - let context_stack = self.context_stack_for_focus_handle(focus_handle)?; - dispatch_tree.highest_precedence_binding_for_action(action, &context_stack) - } - - /// Find the bindings that can follow the current input sequence for the current context stack. - pub fn possible_bindings_for_input(&self, input: &[Keystroke]) -> Vec { - self.rendered_frame - .dispatch_tree - .possible_next_bindings_for_input(input, &self.context_stack()) - } - - fn context_stack_for_focus_handle( - &self, - focus_handle: &FocusHandle, - ) -> Option> { - let dispatch_tree = &self.rendered_frame.dispatch_tree; - let node_id = dispatch_tree.focusable_node_id(focus_handle.id)?; - let context_stack: Vec<_> = dispatch_tree - .dispatch_path(node_id) - .into_iter() - .filter_map(|node_id| dispatch_tree.node(node_id).context.clone()) - .collect(); - Some(context_stack) - } - - /// Returns a generic event listener that invokes the given listener with the view and context associated with the given view handle. - pub fn listener_for( - &self, - view: &Entity, - f: impl Fn(&mut T, &E, &mut Window, &mut Context) + 'static, - ) -> impl Fn(&E, &mut Window, &mut App) + 'static { - let view = view.downgrade(); - move |e: &E, window: &mut Window, cx: &mut App| { - view.update(cx, |view, cx| f(view, e, window, cx)).ok(); - } - } - - /// Returns a generic handler that invokes the given handler with the view and context associated with the given view handle. - pub fn handler_for) + 'static>( - &self, - entity: &Entity, - f: Callback, - ) -> impl Fn(&mut Window, &mut App) + 'static { - let entity = entity.downgrade(); - move |window: &mut Window, cx: &mut App| { - entity.update(cx, |entity, cx| f(entity, window, cx)).ok(); - } - } - - /// Register a callback that can interrupt the closing of the current window based the returned boolean. - /// If the callback returns false, the window won't be closed. - pub fn on_window_should_close( - &self, - cx: &App, - f: impl Fn(&mut Window, &mut App) -> bool + 'static, - ) { - let mut cx = self.to_async(cx); - self.platform_window.on_should_close(Box::new(move || { - cx.update(|window, cx| f(window, cx)).unwrap_or(true) - })) - } - - /// Register an action listener on this node for the next frame. The type of action - /// is determined by the first parameter of the given listener. When the next frame is rendered - /// the listener will be cleared. - /// - /// This is a fairly low-level method, so prefer using action handlers on elements unless you have - /// a specific need to register a listener yourself. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn on_action( - &mut self, - action_type: TypeId, - listener: impl Fn(&dyn Any, DispatchPhase, &mut Window, &mut App) + 'static, - ) { - self.invalidator.debug_assert_paint(); - - self.next_frame - .dispatch_tree - .on_action(action_type, Rc::new(listener)); - } - - /// Register a capturing action listener on this node for the next frame if the condition is true. - /// The type of action is determined by the first parameter of the given listener. When the next - /// frame is rendered the listener will be cleared. - /// - /// This is a fairly low-level method, so prefer using action handlers on elements unless you have - /// a specific need to register a listener yourself. - /// - /// This method should only be called as part of the paint phase of element drawing. - pub fn on_action_when( - &mut self, - condition: bool, - action_type: TypeId, - listener: impl Fn(&dyn Any, DispatchPhase, &mut Window, &mut App) + 'static, - ) { - self.invalidator.debug_assert_paint(); - - if condition { - self.next_frame - .dispatch_tree - .on_action(action_type, Rc::new(listener)); - } - } - - /// Read information about the GPU backing this window. - /// Currently returns None on Mac and Windows. - pub fn gpu_specs(&self) -> Option { - self.platform_window.gpu_specs() - } - - /// Perform titlebar double-click action. - /// This is macOS specific. - pub fn titlebar_double_click(&self) { - self.platform_window - .titlebar_double_click(self.is_resizable, self.is_minimizable); - } - - /// Gets the window's title at the platform level. - /// This is macOS specific. - pub fn window_title(&self) -> String { - self.platform_window.get_title() - } - - /// Returns a list of all tabbed windows and their titles. - /// This is macOS specific. - pub fn tabbed_windows(&self) -> Option> { - self.platform_window.tabbed_windows() - } - - /// Returns the tab bar visibility. - /// This is macOS specific. - pub fn tab_bar_visible(&self) -> bool { - self.platform_window.tab_bar_visible() - } - - /// Merges all open windows into a single tabbed window. - /// This is macOS specific. - pub fn merge_all_windows(&self) { - self.platform_window.merge_all_windows() - } - - /// Moves the tab to a new containing window. - /// This is macOS specific. - pub fn move_tab_to_new_window(&self) { - self.platform_window.move_tab_to_new_window() - } - - /// Shows or hides the window tab overview. - /// This is macOS specific. - pub fn toggle_window_tab_overview(&self) { - self.platform_window.toggle_window_tab_overview() - } - - /// Sets the tabbing identifier for the window. - /// This is macOS specific. - pub fn set_tabbing_identifier(&self, tabbing_identifier: Option) { - self.platform_window - .set_tabbing_identifier(tabbing_identifier) - } - - /// Request the OS to play an alert sound. On some platforms this is associated - /// with the window, for others it's just a simple global function call. - pub fn play_system_bell(&self) { - self.platform_window.play_system_bell() - } - - /// Returns whether accessibility features are active for this frame, - /// i.e. whether assistive technology (such as a screen reader) is - /// connected and an accessibility tree is being built. - /// - /// Use this to skip computing data during rendering that is only - /// observable through the accessibility tree. When accessibility is - /// activated, a redraw is forced, so gated work is recomputed before the - /// next tree update is sent to the platform. - /// - /// See the [accessibility guide](crate::_accessibility) for an overview. - pub fn is_a11y_active(&self) -> bool { - self.a11y.is_active() - } - - /// Debug representation of the last frame's accessibility information. - pub fn debug_a11y_tree_json(&self) -> Option { - self.a11y.debug_tree_json() - } - - /// Register a listener for an accessibility action on a specific node. - /// The listener will be called when a screen reader requests the given - /// action on the node identified by `node_id`. - /// - /// See the [accessibility guide](crate::_accessibility) for an overview. - pub fn on_a11y_action( - &mut self, - node_id: accesskit::NodeId, - action: accesskit::Action, - listener: impl FnMut(Option<&accesskit::ActionData>, &mut Window, &mut App) + 'static, - ) { - self.a11y - .action_listeners - .entry(node_id) - .or_default() - .push((action, Box::new(listener))); - } - - #[cfg(not(target_family = "wasm"))] - pub(crate) fn handle_a11y_action(&mut self, request: accesskit::ActionRequest, cx: &mut App) { - // Take listeners out temporarily so the closures can borrow Window - // mutably, then restore them afterward. - if let Some(mut listeners) = self.a11y.action_listeners.remove(&request.target_node) { - let extra_data = request.data.as_ref(); - let mut matched = false; - for (action, listener) in &mut listeners { - if *action == request.action { - listener(extra_data, self, cx); - matched = true; - } - } - self.a11y - .action_listeners - .insert(request.target_node, listeners); - if matched { - return; - } - } - - // Fall back to built-in action handling. - match request.action { - accesskit::Action::Click => { - if let Some(bounds) = self.a11y.node_bounds.get(&request.target_node).copied() { - let center = bounds.center(); - let mouse_down = PlatformInput::MouseDown(crate::MouseDownEvent { - button: MouseButton::Left, - position: center, - modifiers: Modifiers::default(), - click_count: 1, - first_mouse: false, - }); - let mouse_up = PlatformInput::MouseUp(MouseUpEvent { - button: MouseButton::Left, - position: center, - modifiers: Modifiers::default(), - click_count: 1, - }); - self.dispatch_event(mouse_down, cx); - self.dispatch_event(mouse_up, cx); - } - } - accesskit::Action::Focus => { - if let Some(focus_id) = self.a11y.focus_ids.get(&request.target_node).copied() - && let Some(handle) = FocusHandle::for_id(focus_id, &cx.focus_handles) - { - self.focus(&handle, cx); - } - } - accesskit::Action::Blur => { - self.blur(cx); - } - _ => { - log::debug!( - "Unhandled a11y action: {:?} on {:?}", - request.action, - request.target_node - ); - } - } - } - - /// Toggles the inspector mode on this window. - #[cfg(any(feature = "inspector", debug_assertions))] - pub fn toggle_inspector(&mut self, cx: &mut App) { - self.inspector = match self.inspector { - None => Some(cx.new(|_| Inspector::new())), - Some(_) => None, - }; - self.refresh(); - } - - /// Returns true if the window is in inspector mode. - pub fn is_inspector_picking(&self, _cx: &App) -> bool { - #[cfg(any(feature = "inspector", debug_assertions))] - { - if let Some(inspector) = &self.inspector { - return inspector.read(_cx).is_picking(); - } - } - false - } - - /// Executes the provided function with mutable access to an inspector state. - #[cfg(any(feature = "inspector", debug_assertions))] - pub fn with_inspector_state( - &mut self, - _inspector_id: Option<&crate::InspectorElementId>, - cx: &mut App, - f: impl FnOnce(&mut Option, &mut Self) -> R, - ) -> R { - if let Some(inspector_id) = _inspector_id - && let Some(inspector) = &self.inspector - { - let inspector = inspector.clone(); - let active_element_id = inspector.read(cx).active_element_id(); - if Some(inspector_id) == active_element_id { - return inspector.update(cx, |inspector, _cx| { - inspector.with_active_element_state(self, f) - }); - } - } - f(&mut None, self) - } - - #[cfg(any(feature = "inspector", debug_assertions))] - pub(crate) fn build_inspector_element_id( - &mut self, - path: crate::InspectorElementPath, - ) -> crate::InspectorElementId { - self.invalidator.debug_assert_paint_or_prepaint(); - let path = Rc::new(path); - let next_instance_id = self - .next_frame - .next_inspector_instance_ids - .entry(path.clone()) - .or_insert(0); - let instance_id = *next_instance_id; - *next_instance_id += 1; - crate::InspectorElementId { path, instance_id } - } - - #[cfg(any(feature = "inspector", debug_assertions))] - fn prepaint_inspector(&mut self, inspector_width: Pixels, cx: &mut App) -> Option { - if let Some(inspector) = self.inspector.take() { - let mut inspector_element = AnyView::from(inspector.clone()).into_any_element(); - inspector_element.prepaint_as_root( - point(self.viewport_size.width - inspector_width, px(0.0)), - size(inspector_width, self.viewport_size.height).into(), - self, - cx, - ); - self.inspector = Some(inspector); - Some(inspector_element) - } else { - None - } - } - - #[cfg(any(feature = "inspector", debug_assertions))] - fn paint_inspector(&mut self, mut inspector_element: Option, cx: &mut App) { - if let Some(mut inspector_element) = inspector_element { - inspector_element.paint(self, cx); - }; - } - - /// Registers a hitbox that can be used for inspector picking mode, allowing users to select and - /// inspect UI elements by clicking on them. - #[cfg(any(feature = "inspector", debug_assertions))] - pub fn insert_inspector_hitbox( - &mut self, - hitbox_id: HitboxId, - inspector_id: Option<&crate::InspectorElementId>, - cx: &App, - ) { - self.invalidator.debug_assert_paint_or_prepaint(); - if !self.is_inspector_picking(cx) { - return; - } - if let Some(inspector_id) = inspector_id { - self.next_frame - .inspector_hitboxes - .insert(hitbox_id, inspector_id.clone()); - } - } - - #[cfg(any(feature = "inspector", debug_assertions))] - fn paint_inspector_hitbox(&mut self, cx: &App) { - if let Some(inspector) = self.inspector.as_ref() { - let inspector = inspector.read(cx); - if let Some((hitbox_id, _)) = self.hovered_inspector_hitbox(inspector, &self.next_frame) - && let Some(hitbox) = self - .next_frame - .hitboxes - .iter() - .find(|hitbox| hitbox.id == hitbox_id) - { - self.paint_quad(crate::fill(hitbox.bounds, crate::rgba(0x61afef4d))); - } - } - } - - #[cfg(any(feature = "inspector", debug_assertions))] - fn handle_inspector_mouse_event(&mut self, event: &dyn Any, cx: &mut App) { - let Some(inspector) = self.inspector.clone() else { - return; - }; - if event.downcast_ref::().is_some() { - inspector.update(cx, |inspector, _cx| { - if let Some((_, inspector_id)) = - self.hovered_inspector_hitbox(inspector, &self.rendered_frame) - { - inspector.hover(inspector_id, self); - } - }); - } else if event.downcast_ref::().is_some() { - inspector.update(cx, |inspector, _cx| { - if let Some((_, inspector_id)) = - self.hovered_inspector_hitbox(inspector, &self.rendered_frame) - { - inspector.select(inspector_id, self); - } - }); - } else if let Some(event) = event.downcast_ref::() { - // This should be kept in sync with SCROLL_LINES in x11 platform. - const SCROLL_LINES: f32 = 3.0; - const SCROLL_PIXELS_PER_LAYER: f32 = 36.0; - let delta_y = event - .delta - .pixel_delta(px(SCROLL_PIXELS_PER_LAYER / SCROLL_LINES)) - .y; - if let Some(inspector) = self.inspector.clone() { - inspector.update(cx, |inspector, _cx| { - if let Some(depth) = inspector.pick_depth.as_mut() { - *depth += f32::from(delta_y) / SCROLL_PIXELS_PER_LAYER; - let max_depth = self.mouse_hit_test.ids.len() as f32 - 0.5; - if *depth < 0.0 { - *depth = 0.0; - } else if *depth > max_depth { - *depth = max_depth; - } - if let Some((_, inspector_id)) = - self.hovered_inspector_hitbox(inspector, &self.rendered_frame) - { - inspector.set_active_element_id(inspector_id, self); - } - } - }); - } - } - } - - #[cfg(any(feature = "inspector", debug_assertions))] - fn hovered_inspector_hitbox( - &self, - inspector: &Inspector, - frame: &Frame, - ) -> Option<(HitboxId, crate::InspectorElementId)> { - if let Some(pick_depth) = inspector.pick_depth { - let depth = (pick_depth as i64).try_into().unwrap_or(0); - let max_skipped = self.mouse_hit_test.ids.len().saturating_sub(1); - let skip_count = (depth as usize).min(max_skipped); - for hitbox_id in self.mouse_hit_test.ids.iter().skip(skip_count) { - if let Some(inspector_id) = frame.inspector_hitboxes.get(hitbox_id) { - return Some((*hitbox_id, inspector_id.clone())); - } - } - } - None - } - - /// For testing: set the current modifier keys state. - /// This does not generate any events. - #[cfg(any(test, feature = "test-support"))] - pub fn set_modifiers(&mut self, modifiers: Modifiers) { - self.modifiers = modifiers; - } - - /// For testing: simulate a mouse move event to the given position. - /// This dispatches the event through the normal event handling path, - /// which will trigger hover states and tooltips. - #[cfg(any(test, feature = "test-support"))] - pub fn simulate_mouse_move(&mut self, position: Point, cx: &mut App) { - let event = PlatformInput::MouseMove(MouseMoveEvent { - position, - modifiers: self.modifiers, - pressed_button: None, - }); - let _ = self.dispatch_event(event, cx); - } -} - -// #[derive(Clone, Copy, Eq, PartialEq, Hash)] -slotmap::new_key_type! { - /// A unique identifier for a window. - pub struct WindowId; -} - -impl WindowId { - /// Converts this window ID to a `u64`. - pub fn as_u64(&self) -> u64 { - self.0.as_ffi() - } -} - -impl From for WindowId { - fn from(value: u64) -> Self { - WindowId(slotmap::KeyData::from_ffi(value)) - } -} - -/// A handle to a window with a specific root view type. -/// Note that this does not keep the window alive on its own. -#[derive(Deref, DerefMut)] -pub struct WindowHandle { - #[deref] - #[deref_mut] - pub(crate) any_handle: AnyWindowHandle, - state_type: PhantomData V>, -} - -impl Debug for WindowHandle { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("WindowHandle") - .field("any_handle", &self.any_handle.id.as_u64()) - .finish() - } -} - -impl WindowHandle { - /// Creates a new handle from a window ID. - /// This does not check if the root type of the window is `V`. - pub fn new(id: WindowId) -> Self { - WindowHandle { - any_handle: AnyWindowHandle { - id, - state_type: TypeId::of::(), - root_entity_type_name: std::any::type_name::(), - }, - state_type: PhantomData, - } - } - - /// Get the root view out of this window. - /// - /// This will fail if the window is closed or if the root view's type does not match `V`. - #[cfg(any(test, feature = "test-support"))] - pub fn root(&self, cx: &mut C) -> Result> - where - C: AppContext, - { - cx.update_window(self.any_handle, |root_view, _, _| { - root_view - .downcast::() - .map_err(|_| anyhow!("the type of the window's root view has changed")) - })? - } - - /// Updates the root view of this window. - /// - /// This will fail if the window has been closed or if the root view's type does not match - pub fn update( - &self, - cx: &mut C, - update: impl FnOnce(&mut V, &mut Window, &mut Context) -> R, - ) -> Result - where - C: AppContext, - { - cx.update_window(self.any_handle, |root_view, window, cx| { - let view = root_view - .downcast::() - .map_err(|_| anyhow!("the type of the window's root view has changed"))?; - - Ok(view.update(cx, |view, cx| update(view, window, cx))) - })? - } - - /// Read the root view out of this window. - /// - /// This will fail if the window is closed or if the root view's type does not match `V`. - pub fn read<'a>(&self, cx: &'a App) -> Result<&'a V> { - let x = cx - .windows - .get(self.id) - .and_then(|window| { - window - .as_deref() - .and_then(|window| window.root.clone()) - .map(|root_view| root_view.downcast::()) - }) - .context("window not found")? - .map_err(|_| anyhow!("the type of the window's root view has changed"))?; - - Ok(x.read(cx)) - } - - /// Read the root view out of this window, with a callback - /// - /// This will fail if the window is closed or if the root view's type does not match `V`. - pub fn read_with(&self, cx: &C, read_with: impl FnOnce(&V, &App) -> R) -> Result - where - C: AppContext, - { - cx.read_window(self, |root_view, cx| read_with(root_view.read(cx), cx)) - } - - /// Read the root view pointer off of this window. - /// - /// This will fail if the window is closed or if the root view's type does not match `V`. - pub fn entity(&self, cx: &C) -> Result> - where - C: AppContext, - { - cx.read_window(self, |root_view, _cx| root_view) - } - - /// Check if this window is 'active'. - /// - /// Will return `None` if the window is closed or currently - /// borrowed. - pub fn is_active(&self, cx: &mut App) -> Option { - cx.update_window(self.any_handle, |_, window, _| window.is_window_active()) - .ok() - } -} - -impl Copy for WindowHandle {} - -impl Clone for WindowHandle { - fn clone(&self) -> Self { - *self - } -} - -impl PartialEq for WindowHandle { - fn eq(&self, other: &Self) -> bool { - self.any_handle == other.any_handle - } -} - -impl Eq for WindowHandle {} - -impl Hash for WindowHandle { - fn hash(&self, state: &mut H) { - self.any_handle.hash(state); - } -} - -impl From> for AnyWindowHandle { - fn from(val: WindowHandle) -> Self { - val.any_handle - } -} - -/// A handle to a window with any root view type, which can be downcast to a window with a specific root view type. -#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct AnyWindowHandle { - pub(crate) id: WindowId, - state_type: TypeId, - root_entity_type_name: &'static str, -} - -impl AnyWindowHandle { - /// Get the ID of this window. - pub fn window_id(&self) -> WindowId { - self.id - } - - /// Returns the name of the window's declared root entity type. - pub fn root_entity_type_name(&self) -> &'static str { - self.root_entity_type_name - } - - /// Attempt to convert this handle to a window handle with a specific root view type. - /// If the types do not match, this will return `None`. - pub fn downcast(&self) -> Option> { - if TypeId::of::() == self.state_type { - Some(WindowHandle { - any_handle: *self, - state_type: PhantomData, - }) - } else { - None - } - } - - /// Updates the state of the root view of this window. - /// - /// This will fail if the window has been closed. - pub fn update( - self, - cx: &mut C, - update: impl FnOnce(AnyView, &mut Window, &mut App) -> R, - ) -> Result - where - C: AppContext, - { - cx.update_window(self, update) - } - - /// Read the state of the root view of this window. - /// - /// This will fail if the window has been closed. - pub fn read(self, cx: &C, read: impl FnOnce(Entity, &App) -> R) -> Result - where - C: AppContext, - T: 'static, - { - let view = self - .downcast::() - .context("the type of the window's root view has changed")?; - - cx.read_window(&view, read) - } -} - -impl HasWindowHandle for Window { - fn window_handle(&self) -> Result, HandleError> { - self.platform_window.window_handle() - } -} - -impl HasDisplayHandle for Window { - fn display_handle( - &self, - ) -> std::result::Result, HandleError> { - self.platform_window.display_handle() - } -} - -/// An identifier for an [`Element`]. -/// -/// Can be constructed with a string, a number, or both, as well -/// as other internal representations. -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub enum ElementId { - /// The ID of a View element - View(EntityId), - /// An integer ID. - Integer(u64), - /// A string based ID. - Name(SharedString), - /// A UUID. - Uuid(Uuid), - /// An ID that's equated with a focus handle. - FocusHandle(FocusId), - /// A combination of a name and an integer. - NamedInteger(SharedString, u64), - /// A path. - Path(Arc), - /// A code location. - CodeLocation(core::panic::Location<'static>), - /// A labeled child of an element. - NamedChild(Arc, SharedString), - /// A byte array ID (used for text-anchors) - OpaqueId([u8; 20]), -} - -impl ElementId { - /// Constructs an `ElementId::NamedInteger` from a name and `usize`. - pub fn named_usize(name: impl Into, integer: usize) -> ElementId { - Self::NamedInteger(name.into(), integer as u64) - } -} - -impl Display for ElementId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ElementId::View(entity_id) => write!(f, "view-{}", entity_id)?, - ElementId::Integer(ix) => write!(f, "{}", ix)?, - ElementId::Name(name) => write!(f, "{}", name)?, - ElementId::FocusHandle(_) => write!(f, "FocusHandle")?, - ElementId::NamedInteger(s, i) => write!(f, "{}-{}", s, i)?, - ElementId::Uuid(uuid) => write!(f, "{}", uuid)?, - ElementId::Path(path) => write!(f, "{}", path.display())?, - ElementId::CodeLocation(location) => write!(f, "{}", location)?, - ElementId::NamedChild(id, name) => write!(f, "{}-{}", id, name)?, - ElementId::OpaqueId(opaque_id) => write!(f, "{:x?}", opaque_id)?, - } - - Ok(()) - } -} - -impl TryInto for ElementId { - type Error = anyhow::Error; - - fn try_into(self) -> anyhow::Result { - if let ElementId::Name(name) = self { - Ok(name) - } else { - anyhow::bail!("element id is not string") - } - } -} - -impl From for ElementId { - fn from(id: usize) -> Self { - ElementId::Integer(id as u64) - } -} - -impl From for ElementId { - fn from(id: i32) -> Self { - Self::Integer(id as u64) - } -} - -impl From for ElementId { - fn from(name: SharedString) -> Self { - ElementId::Name(name) - } -} - -impl From for ElementId { - fn from(name: String) -> Self { - ElementId::Name(name.into()) - } -} - -impl From> for ElementId { - fn from(name: Arc) -> Self { - ElementId::Name(name.into()) - } -} - -impl From> for ElementId { - fn from(path: Arc) -> Self { - ElementId::Path(path) - } -} - -impl From<&'static str> for ElementId { - fn from(name: &'static str) -> Self { - ElementId::Name(SharedString::new_static(name)) - } -} - -impl<'a> From<&'a FocusHandle> for ElementId { - fn from(handle: &'a FocusHandle) -> Self { - ElementId::FocusHandle(handle.id) - } -} - -impl From<(&'static str, EntityId)> for ElementId { - fn from((name, id): (&'static str, EntityId)) -> Self { - ElementId::NamedInteger(SharedString::new_static(name), id.as_u64()) - } -} - -impl From<(&'static str, usize)> for ElementId { - fn from((name, id): (&'static str, usize)) -> Self { - ElementId::NamedInteger(SharedString::new_static(name), id as u64) - } -} - -impl From<(SharedString, usize)> for ElementId { - fn from((name, id): (SharedString, usize)) -> Self { - ElementId::NamedInteger(name, id as u64) - } -} - -impl From<(&'static str, u64)> for ElementId { - fn from((name, id): (&'static str, u64)) -> Self { - ElementId::NamedInteger(SharedString::new_static(name), id) - } -} - -impl From for ElementId { - fn from(value: Uuid) -> Self { - Self::Uuid(value) - } -} - -impl From<(&'static str, u32)> for ElementId { - fn from((name, id): (&'static str, u32)) -> Self { - ElementId::NamedInteger(SharedString::new_static(name), u64::from(id)) - } -} - -impl> From<(ElementId, T)> for ElementId { - fn from((id, name): (ElementId, T)) -> Self { - ElementId::NamedChild(Arc::new(id), name.into()) - } -} - -impl From<&'static core::panic::Location<'static>> for ElementId { - fn from(location: &'static core::panic::Location<'static>) -> Self { - ElementId::CodeLocation(*location) - } -} - -impl From<[u8; 20]> for ElementId { - fn from(opaque_id: [u8; 20]) -> Self { - ElementId::OpaqueId(opaque_id) - } -} - -/// A rectangle to be rendered in the window at the given position and size. -/// Passed as an argument [`Window::paint_quad`]. -#[derive(Clone)] -pub struct PaintQuad { - /// The bounds of the quad within the window. - pub bounds: Bounds, - /// The radii of the quad's corners. - pub corner_radii: Corners, - /// The background color of the quad. - pub background: Background, - /// The widths of the quad's borders. - pub border_widths: Edges, - /// The color of the quad's borders. - pub border_color: Hsla, - /// The style of the quad's borders. - pub border_style: BorderStyle, -} - -impl PaintQuad { - /// Sets the corner radii of the quad. - pub fn corner_radii(self, corner_radii: impl Into>) -> Self { - PaintQuad { - corner_radii: corner_radii.into(), - ..self - } - } - - /// Sets the border widths of the quad. - pub fn border_widths(self, border_widths: impl Into>) -> Self { - PaintQuad { - border_widths: border_widths.into(), - ..self - } - } - - /// Sets the border color of the quad. - pub fn border_color(self, border_color: impl Into) -> Self { - PaintQuad { - border_color: border_color.into(), - ..self - } - } - - /// Sets the background color of the quad. - pub fn background(self, background: impl Into) -> Self { - PaintQuad { - background: background.into(), - ..self - } - } -} - -/// Creates a quad with the given parameters. -pub fn quad( - bounds: Bounds, - corner_radii: impl Into>, - background: impl Into, - border_widths: impl Into>, - border_color: impl Into, - border_style: BorderStyle, -) -> PaintQuad { - PaintQuad { - bounds, - corner_radii: corner_radii.into(), - background: background.into(), - border_widths: border_widths.into(), - border_color: border_color.into(), - border_style, - } -} - -/// Creates a filled quad with the given bounds and background color. -pub fn fill(bounds: impl Into>, background: impl Into) -> PaintQuad { - PaintQuad { - bounds: bounds.into(), - corner_radii: (0.).into(), - background: background.into(), - border_widths: (0.).into(), - border_color: transparent_black(), - border_style: BorderStyle::default(), - } -} - -/// Creates a rectangle outline with the given bounds, border color, and a 1px border width -pub fn outline( - bounds: impl Into>, - border_color: impl Into, - border_style: BorderStyle, -) -> PaintQuad { - PaintQuad { - bounds: bounds.into(), - corner_radii: (0.).into(), - background: transparent_black().into(), - border_widths: (1.).into(), - border_color: border_color.into(), - border_style, - } -} - -#[cfg(test)] -mod tests { - use std::{ - cell::{Cell, RefCell}, - path::PathBuf, - rc::Rc, - time::Duration, - }; - - use crate::{ - canvas, div, point, px, size, AnyWindowHandle, AppContext as _, Bounds, ContentMask, - Context, Corners, DispatchPhase, DragMoveEvent, Empty, ExternalDragPayload, ExternalPaths, - FileDragPaths, FileDropEvent, FocusHandle, InputEvent as _, InteractiveElement as _, - IntoElement, LongPressEvent, MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, - Pixels, Point, Render, RequestFrameOptions, StatefulInteractiveElement as _, Styled, - TestAppContext, TouchDragEvent, TouchEvent, TouchId, TouchPhase, Window, WindowAppearance, - WindowOptions, - }; - - struct EmptyView; - - #[test] - fn rounded_content_mask_intersection_keeps_the_original_curve() { - let outer = ContentMask { - bounds: Bounds::from_corners(point(px(0.), px(0.)), point(px(100.), px(100.))), - corner_radii: Corners::all(px(16.)), - ..Default::default() - }; - let inset = ContentMask { - bounds: Bounds::from_corners(point(px(4.), px(4.)), point(px(96.), px(96.))), - ..Default::default() - }; - - let intersection = outer.intersect(&inset); - assert_eq!(intersection.bounds.origin, point(px(4.), px(4.))); - assert_eq!(intersection.rounded_clips[0].bounds, outer.bounds); - assert!(intersection.contains(point(px(4.5), px(8.5)))); - } - - impl Render for EmptyView { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - } - } - - struct OpensWindowOnPaint { - opened: Rc>, - } - - impl Render for OpensWindowOnPaint { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let opened = self.opened.clone(); - div() - .size_full() - .child(canvas( - |_, _, _| {}, - move |_, _, _window, cx| { - if !opened.replace(true) { - cx.open_window(WindowOptions::default(), |_, cx| cx.new(|_| EmptyView)) - .unwrap(); - } - }, - )) - // Siblings painted after the canvas: their elements were - // allocated in the arena before the nested draw, so they detect - // a mid-draw arena clear when painted afterwards. - .child(div().child("after")) - } - } - - /// Opening a window synchronously draws it and requests an element arena - /// clear. When that happens from within another window's draw (here: from - /// an element's paint), the clear must be deferred until the outer draw - /// finishes, or the outer draw's arena-allocated elements would be freed - /// out from under it. - #[test] - fn test_window_opened_during_draw_defers_arena_clear() { - let mut cx = TestAppContext::single(); - - let opened = Rc::new(Cell::new(false)); - // add_window draws once, which runs the nested open_window mid-draw. - let window = cx.add_window({ - let opened = opened.clone(); - move |_, _| OpensWindowOnPaint { opened } - }); - - assert!(opened.get()); - assert_eq!(cx.windows().len(), 2); - - // The deferred clear must actually run once the outer draw unwinds: - // subsequent draws of both windows work against a fresh arena. - cx.update_window(window.into(), |_, window, cx| window.draw(cx).clear(cx)) - .unwrap(); - } - - /// Platforms that stop requesting frames for idle windows (currently web) - /// rely on the frame waker firing whenever frame demand arises; a demand - /// source that skips the waker shows up there as a window that silently - /// stops repainting until unrelated activity wakes it. - #[gpui::test] - fn test_frame_waker_fires_on_frame_demand(cx: &mut TestAppContext) { - let window = cx.add_window(|_, _| EmptyView); - let test_window = cx.test_window(window.into()); - - // Windows start dirty, and that can predate waker installation; - // installing the waker must deliver the pending wake or the first - // frame would never be requested. - assert!( - test_window.frame_wake_count() >= 1, - "opening a window must wake the frame source for the initial frame" - ); - - // Serve outstanding demand (present the frame drawn by `add_window`). - test_window.simulate_frame_request(RequestFrameOptions::default()); - - // An idle window must not wake on clean frames or plain updates, or - // the frame source could never stop. - let baseline = test_window.frame_wake_count(); - test_window.simulate_frame_request(RequestFrameOptions::default()); - window.update(cx, |_, _, _| {}).unwrap(); - assert_eq!( - test_window.frame_wake_count(), - baseline, - "clean frames and non-notifying updates must not wake the frame source" - ); - - // Notifying a view in an idle window is the core demand signal. - window.update(cx, |_, _, cx| cx.notify()).unwrap(); - assert!( - test_window.frame_wake_count() > baseline, - "notifying a view in an idle window must wake the frame source" - ); - - // Serving that demand returns to idle without further wakes. - test_window.simulate_frame_request(RequestFrameOptions::default()); - let baseline = test_window.frame_wake_count(); - test_window.simulate_frame_request(RequestFrameOptions::default()); - assert_eq!( - test_window.frame_wake_count(), - baseline, - "serving demand must return the window to idle" - ); - - // Next-frame callbacks create demand without dirtying the window. - window - .update(cx, |_, window, _| window.on_next_frame(|_, _| {})) - .unwrap(); - assert!( - test_window.frame_wake_count() > baseline, - "scheduling a next-frame callback in an idle window must wake the frame source" - ); - } - - /// A frame request that arrives while next-frame callbacks are pending - /// must never strand them: either the frame runs them, or (when the - /// inactive-window frame-rate throttle defers the frame) the waker fires - /// so another request is delivered. - #[gpui::test] - fn test_pending_next_frame_callbacks_are_not_stranded(cx: &mut TestAppContext) { - let window = cx.add_window(|_, _| EmptyView); - let test_window = cx.test_window(window.into()); - // Establish a recent last-frame time so the inactive-window throttle - // can engage on the next request. - test_window.simulate_frame_request(RequestFrameOptions::default()); - - let callback_ran = Rc::new(Cell::new(false)); - window - .update(cx, { - let callback_ran = callback_ran.clone(); - move |_, window, _| { - window.on_next_frame(move |_, _| callback_ran.set(true)); - } - }) - .unwrap(); - - let baseline = test_window.frame_wake_count(); - test_window.simulate_frame_request(RequestFrameOptions::default()); - // The test window is inactive, so this request throttles to ~30fps - // when it lands within the throttle interval of the previous frame - // (the common case here, but timing-dependent): the callback is - // deferred and the waker must re-arm the frame source. On a slow run - // the request instead lands outside the interval and runs the - // callback directly. - assert!( - test_window.frame_wake_count() > baseline || callback_ran.get(), - "a frame request with pending next-frame callbacks must either run them or re-arm the frame source" - ); - } - - #[gpui::test] - fn test_window_reports_no_raw_handle_instead_of_panicking(cx: &mut TestAppContext) { - use raw_window_handle::{HandleError, HasDisplayHandle as _, HasWindowHandle as _}; - - let window = cx.add_window(|_, _| EmptyView); - window - .update(cx, |_, window, _| { - assert!(matches!( - window.window_handle(), - Err(HandleError::NotSupported) - )); - assert!(matches!( - window.display_handle(), - Err(HandleError::NotSupported) - )); - }) - .unwrap(); - } - - #[gpui::test] - fn test_appearance_change_runs_after_app_update(cx: &mut TestAppContext) { - let window = cx.add_window(|_, _| EmptyView); - let observed_appearance = Rc::new(Cell::new(None)); - let _subscription = window - .update(cx, { - let observed_appearance = observed_appearance.clone(); - move |_, window, _| { - window.observe_window_appearance(move |window, _| { - observed_appearance.set(Some(window.appearance())); - }) - } - }) - .unwrap(); - let test_window = cx.test_window(window.into()); - - cx.update(|_| { - test_window.simulate_appearance_change(WindowAppearance::Dark); - assert_eq!(observed_appearance.get(), None); - }); - cx.run_until_parked(); - - assert_eq!(observed_appearance.get(), Some(WindowAppearance::Dark)); - } - - #[gpui::test] - fn queued_frame_callback_wakes_a_parked_render_loop(cx: &mut TestAppContext) { - let window = cx.add_window(|_, _| Empty); - let test_window = cx.test_window(window.into()); - - assert!(test_window.simulate_scheduled_frame()); - assert!(test_window.simulate_scheduled_frame()); - assert!(!test_window.frame_scheduled()); - - cx.update_window(window.into(), |_, window, _| { - window.active.set(true); - window.on_next_frame(|_, _| {}); - }) - .unwrap(); - assert!( - test_window.frame_scheduled(), - "queuing work on a parked window must wake the render loop" - ); - - assert!(test_window.simulate_scheduled_frame()); - assert!( - test_window.frame_scheduled(), - "presenting the frame must await one compositor callback" - ); - assert!(test_window.simulate_scheduled_frame()); - assert!(!test_window.frame_scheduled()); - } - - #[gpui::test] - fn pending_presentation_wakes_a_parked_render_loop(cx: &mut TestAppContext) { - let window = cx.add_window(|_, _| Empty); - let test_window = cx.test_window(window.into()); - - assert!(test_window.simulate_scheduled_frame()); - assert!(test_window.simulate_scheduled_frame()); - assert!(!test_window.frame_scheduled()); - - cx.update_window(window.into(), |_, window, cx| window.draw(cx).clear(cx)) - .unwrap(); - - assert!( - test_window.frame_scheduled(), - "a rendered scene awaiting presentation must wake the render loop" - ); - } - - #[gpui::test] - fn callback_queued_during_a_frame_requests_a_follow_up(cx: &mut TestAppContext) { - let window = cx.add_window(|_, _| Empty); - let test_window = cx.test_window(window.into()); - - let callback_ran = Rc::new(Cell::new(false)); - cx.update_window(window.into(), |_, window, _| { - // Inactive windows are frame-rate throttled, which would defer the - // ticks this test drives manually. - window.active.set(true); - let callback_ran = callback_ran.clone(); - window.on_next_frame(move |window, _| { - window.on_next_frame(move |_, _| callback_ran.set(true)); - }); - }) - .unwrap(); - - assert!(test_window.simulate_scheduled_frame()); - assert!(!callback_ran.get()); - assert!( - test_window.frame_scheduled(), - "a callback queued mid-frame must schedule a follow-up before the loop parks" - ); - - assert!(test_window.simulate_scheduled_frame()); - assert!(callback_ran.get()); - } - - struct RootView { - explicit_size: bool, - child_bounds: Rc>>, - } - - impl Render for RootView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - let child_bounds = self.child_bounds.clone(); - let root = div().flex().flex_col().child( - canvas( - move |bounds, _, _| child_bounds.set(bounds), - |_, _, _, _| {}, - ) - .size_full(), - ); - if self.explicit_size { - root.w(px(300.)).h(px(200.)) - } else { - root - } - } - } - - #[test] - fn auto_sized_window_root_fills_the_window() { - let mut cx = TestAppContext::single(); - let child_bounds = Rc::new(Cell::new(Bounds::default())); - let window = cx.add_window({ - let child_bounds = child_bounds.clone(); - move |_, _| RootView { - explicit_size: false, - child_bounds, - } - }); - - let viewport_size = cx - .update_window(window.into(), |_, window, cx| { - window.draw(cx).clear(cx); - window.viewport_size() - }) - .unwrap(); - - assert_eq!(child_bounds.get().size, viewport_size); - } - - #[test] - fn explicitly_sized_window_root_keeps_its_size() { - let mut cx = TestAppContext::single(); - let child_bounds = Rc::new(Cell::new(Bounds::default())); - let window = cx.add_window({ - let child_bounds = child_bounds.clone(); - move |_, _| RootView { - explicit_size: true, - child_bounds, - } - }); - - cx.update_window(window.into(), |_, window, cx| { - window.draw(cx).clear(cx); - }) - .unwrap(); - - assert_eq!(child_bounds.get().size, size(px(300.), px(200.))); - } - - struct FileDragView { - path: PathBuf, - observed_drag_moves: Rc>>>, - observed_drops: Rc>>, - } - - impl Render for FileDragView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - div() - .id("file-drag") - .size_full() - .on_drag(self.path.clone(), |_, _, _, cx| cx.new(|_| Empty)) - .external_drag_payload(|path: &PathBuf, _, _| { - Some(ExternalDragPayload::Files(FileDragPaths::new([( - path.clone(), - true, - )]))) - }) - .on_drag_move({ - let observed_drag_moves = self.observed_drag_moves.clone(); - move |event: &DragMoveEvent, _, _| { - observed_drag_moves.borrow_mut().push(event.event.position); - } - }) - .on_drop({ - let observed_drops = self.observed_drops.clone(); - move |path: &PathBuf, _, _| observed_drops.borrow_mut().push(path.clone()) - }) - } - } - - #[gpui::test] - fn file_drag_is_promoted_once_and_restored_in_source_window(cx: &mut TestAppContext) { - struct Drag { - window: AnyWindowHandle, - observed_drag_moves: Rc>>>, - observed_drops: Rc>>, - } - - fn start_drag(cx: &mut TestAppContext, path: PathBuf, platform_result: bool) -> Drag { - let observed_drag_moves = Rc::new(RefCell::new(Vec::new())); - let observed_drops = Rc::new(RefCell::new(Vec::new())); - let window: AnyWindowHandle = cx - .add_window({ - let observed_drag_moves = observed_drag_moves.clone(); - let observed_drops = observed_drops.clone(); - move |_, _| FileDragView { - path, - observed_drag_moves, - observed_drops, - } - }) - .into(); - cx.test_window(window) - .set_start_external_drag_result(platform_result); - - let update_result = cx.update_window(window, |_, window, cx| { - window.draw(cx).clear(cx); - window.dispatch_event( - MouseDownEvent { - position: point(px(10.), px(10.)), - button: MouseButton::Left, - modifiers: Default::default(), - click_count: 1, - first_mouse: false, - } - .to_platform_input(), - cx, - ); - window.dispatch_event( - MouseMoveEvent { - position: point(px(20.), px(20.)), - pressed_button: Some(MouseButton::Left), - modifiers: Default::default(), - } - .to_platform_input(), - cx, - ); - assert!(cx.active_drag.is_some()); - }); - assert!( - update_result.is_ok(), - "failed to start drag: {update_result:?}" - ); - - assert!(cx.test_window(window).external_drag_files().is_empty()); - Drag { - window, - observed_drag_moves, - observed_drops, - } - } - - let successful_path = PathBuf::from("/tmp/successful-drag"); - let successful = start_drag(cx, successful_path.clone(), true); - let outside_position = point(px(-1.), px(20.)); - let update_result = cx.update_window(successful.window, |_, window, cx| { - window.dispatch_event( - MouseMoveEvent { - position: outside_position, - pressed_button: Some(MouseButton::Left), - modifiers: Default::default(), - } - .to_platform_input(), - cx, - ); - assert!(cx.active_drag.is_none()); - }); - assert!( - update_result.is_ok(), - "failed to promote drag: {update_result:?}" - ); - assert_eq!( - cx.test_window(successful.window).external_drag_files(), - [(successful_path.clone(), true)] - ); - // Views must still see the move that leaves the window, otherwise they never learn to tear - // down the drag state they built up while the pointer was inside. - assert_eq!( - successful.observed_drag_moves.borrow().last(), - Some(&outside_position) - ); - - let destination: AnyWindowHandle = cx.add_window(|_, _| EmptyView).into(); - let reentry_position = point(px(30.), px(30.)); - let external_paths = || ExternalPaths([successful_path.clone()].into_iter().collect()); - let update_result = cx.update_window(destination, |_, window, cx| { - window.dispatch_event( - FileDropEvent::Entered { - position: reentry_position, - paths: external_paths(), - } - .to_platform_input(), - cx, - ); - assert!(cx - .active_drag - .as_ref() - .is_some_and(|drag| drag.value.downcast_ref::().is_some())); - window.dispatch_event(FileDropEvent::Exited.to_platform_input(), cx); - assert!(cx.active_drag.is_none()); - }); - assert!( - update_result.is_ok(), - "failed to handle drag in destination window: {update_result:?}" - ); - - let update_result = cx.update_window(successful.window, |_, window, cx| { - window.dispatch_event( - FileDropEvent::Entered { - position: reentry_position, - paths: external_paths(), - } - .to_platform_input(), - cx, - ); - assert!(cx - .active_drag - .as_ref() - .is_some_and(|drag| drag.value.downcast_ref::().is_some())); - assert_eq!( - successful.observed_drag_moves.borrow().last(), - Some(&reentry_position) - ); - - window.dispatch_event(FileDropEvent::Exited.to_platform_input(), cx); - assert!(cx.active_drag.is_none()); - - window.dispatch_event( - FileDropEvent::Entered { - position: reentry_position, - paths: external_paths(), - } - .to_platform_input(), - cx, - ); - assert!(cx - .active_drag - .as_ref() - .is_some_and(|drag| drag.value.downcast_ref::().is_some())); - - window.dispatch_event( - FileDropEvent::Submit { - position: reentry_position, - } - .to_platform_input(), - cx, - ); - assert_eq!( - successful.observed_drops.borrow().as_slice(), - std::slice::from_ref(&successful_path) - ); - assert!(cx.active_drag.is_none()); - - window.dispatch_event(FileDropEvent::Exited.to_platform_input(), cx); - assert!(cx.active_drag.is_none()); - window.dispatch_event(FileDropEvent::Ended.to_platform_input(), cx); - assert!(cx.active_drag.is_none()); - - window.dispatch_event( - FileDropEvent::Entered { - position: reentry_position, - paths: external_paths(), - } - .to_platform_input(), - cx, - ); - assert!(cx - .active_drag - .as_ref() - .is_some_and(|drag| drag.value.downcast_ref::().is_some())); - window.dispatch_event(FileDropEvent::Exited.to_platform_input(), cx); - }); - assert!( - update_result.is_ok(), - "failed to restore drag in source window: {update_result:?}" - ); - - let cancelled_path = PathBuf::from("/tmp/cancelled-drag"); - let cancelled = start_drag(cx, cancelled_path.clone(), true); - let update_result = cx.update_window(cancelled.window, |_, window, cx| { - window.dispatch_event( - MouseMoveEvent { - position: outside_position, - pressed_button: Some(MouseButton::Left), - modifiers: Default::default(), - } - .to_platform_input(), - cx, - ); - assert!(cx.active_drag.is_none()); - - window.dispatch_event( - FileDropEvent::Entered { - position: reentry_position, - paths: ExternalPaths([cancelled_path].into_iter().collect()), - } - .to_platform_input(), - cx, - ); - assert!(cx - .active_drag - .as_ref() - .is_some_and(|drag| drag.value.downcast_ref::().is_some())); - assert!(cx.stop_active_drag(window)); - assert!(cx.active_drag.is_none()); - }); - assert!( - update_result.is_ok(), - "failed to cancel restored drag: {update_result:?}" - ); - assert!(!cx.update(|cx| cx.end_platform_drag(cancelled.window.window_id()))); - - let removed_path = PathBuf::from("/tmp/removed-window-drag"); - let removed = start_drag(cx, removed_path, true); - let removed_window_id = removed.window.window_id(); - let update_result = cx.update_window(removed.window, |_, window, cx| { - window.dispatch_event( - MouseMoveEvent { - position: outside_position, - pressed_button: Some(MouseButton::Left), - modifiers: Default::default(), - } - .to_platform_input(), - cx, - ); - assert!(cx.active_drag.is_none()); - window.remove_window(); - }); - assert!( - update_result.is_ok(), - "failed to remove drag source window: {update_result:?}" - ); - assert!(!cx.update(|cx| cx.end_platform_drag(removed_window_id))); - - let failed_path = PathBuf::from("/tmp/failed-drag"); - let failed = start_drag(cx, failed_path.clone(), false); - let update_result = cx.update_window(failed.window, |_, window, cx| { - for x_position in [-1., -2.] { - window.dispatch_event( - MouseMoveEvent { - position: point(px(x_position), px(20.)), - pressed_button: Some(MouseButton::Left), - modifiers: Default::default(), - } - .to_platform_input(), - cx, - ); - } - assert!(cx.active_drag.is_some()); - }); - assert!( - update_result.is_ok(), - "failed to retain drag after platform failure: {update_result:?}" - ); - assert_eq!( - cx.test_window(failed.window).external_drag_files(), - [(failed_path, true)] - ); - } - - struct FocusForwarder { - a: FocusHandle, - b: FocusHandle, - } - - impl Render for FocusForwarder { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - div() - .size_full() - .child(div().w(px(50.)).h(px(50.)).track_focus(&self.a)) - .child(div().w(px(50.)).h(px(50.)).track_focus(&self.b)) - } - } - - /// When a focus listener moves focus again (e.g. a dock forwarding focus to its - /// active panel), the resulting focus events must be dispatched without waiting - /// for an unrelated redraw of the window. - #[gpui::test] - fn test_focus_moved_by_focus_listener_is_dispatched(cx: &mut TestAppContext) { - let b_focus_count = Rc::new(Cell::new(0)); - let window = cx.add_window({ - let b_focus_count = b_focus_count.clone(); - move |window, cx| { - let a = cx.focus_handle(); - let b = cx.focus_handle(); - cx.on_focus(&a, window, |this: &mut FocusForwarder, window, cx| { - let b = this.b.clone(); - window.focus(&b, cx); - }) - .detach(); - cx.on_focus(&b, window, move |_, _, _| { - b_focus_count.set(b_focus_count.get() + 1); - }) - .detach(); - FocusForwarder { a, b } - } - }); - - window - .update(cx, |_, window, _| window.activate_window()) - .unwrap(); - cx.executor().run_until_parked(); - - window - .update(cx, |this, window, cx| { - let a = this.a.clone(); - window.focus(&a, cx); - }) - .unwrap(); - cx.executor().run_until_parked(); - - window - .update(cx, |this, window, _| { - assert!(this.b.is_focused(window)); - }) - .unwrap(); - assert_eq!(b_focus_count.get(), 1); - } - - #[gpui::test] - fn claimed_touch_drag_receives_movement_and_release(cx: &mut TestAppContext) { - let events = Rc::new(RefCell::new(Vec::new())); - let window = cx.add_window({ - let events = events.clone(); - move |_, _| TouchDragListener { events } - }); - let touch = TouchId(1); - - dispatch_touch(window, cx, touch, TouchPhase::Started, 10.); - dispatch_touch(window, cx, touch, TouchPhase::Moved, 30.); - dispatch_touch(window, cx, touch, TouchPhase::Ended, 40.); - - assert_eq!( - events.borrow().as_slice(), - [ - (TouchPhase::Started, px(10.)), - (TouchPhase::Moved, px(30.)), - (TouchPhase::Ended, px(40.)), - ] - ); - } - - struct TouchDragListener { - events: Rc>>, - } - - impl Render for TouchDragListener { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let events = self.events.clone(); - canvas( - |_, _, _| {}, - move |_, _, window, _| { - window.on_mouse_event(move |event: &TouchDragEvent, phase, window, _cx| { - if phase != DispatchPhase::Bubble { - return; - } - events.borrow_mut().push((event.phase, event.position.x)); - if event.phase == TouchPhase::Started { - window.prevent_default(); - } - }); - }, - ) - } - } - - #[gpui::test] - fn long_press_is_claimed_only_when_started_prevents_default(cx: &mut TestAppContext) { - for response in [ - LongPressResponse::PreventDefault, - LongPressResponse::StopPropagation, - LongPressResponse::None, - ] { - let phases = Rc::new(RefCell::new(Vec::new())); - let window = cx.add_window({ - let phases = phases.clone(); - move |_, _| LongPressListener { phases, response } - }); - dispatch_touch(window, cx, TouchId(1), TouchPhase::Started, 0.); - cx.executor().advance_clock(Duration::from_millis(501)); - cx.executor().run_until_parked(); - window - .update(cx, |_, window, _| { - assert_eq!( - window.long_press_capture.is_some(), - response == LongPressResponse::PreventDefault - ); - }) - .unwrap(); - dispatch_touch(window, cx, TouchId(1), TouchPhase::Moved, 2.); - dispatch_touch(window, cx, TouchId(1), TouchPhase::Ended, 2.); - window - .update(cx, |_, window, _| { - assert!(window.long_press_capture.is_none()); - }) - .unwrap(); - - let phases = phases.borrow(); - if response == LongPressResponse::PreventDefault { - assert_eq!( - phases.as_slice(), - [TouchPhase::Started, TouchPhase::Moved, TouchPhase::Ended] - ); - } else { - assert_eq!(phases.as_slice(), [TouchPhase::Started]); - } - } - } - - #[gpui::test] - fn stale_default_prevention_does_not_claim_long_press(cx: &mut TestAppContext) { - let phases = Rc::new(RefCell::new(Vec::new())); - let window = cx.add_window({ - let phases = phases.clone(); - move |_, _| LongPressListener { - phases, - response: LongPressResponse::None, - } - }); - window - .update(cx, |_, window, _| { - window.prevent_default(); - }) - .unwrap(); - - dispatch_touch(window, cx, TouchId(1), TouchPhase::Started, 0.); - cx.executor().advance_clock(Duration::from_millis(501)); - cx.executor().run_until_parked(); - dispatch_touch(window, cx, TouchId(1), TouchPhase::Moved, 2.); - - assert_eq!(phases.borrow().as_slice(), [TouchPhase::Started]); - } - - #[gpui::test] - fn resolved_touch_cancels_scheduled_long_press(cx: &mut TestAppContext) { - for (phase, position) in [ - (TouchPhase::Ended, 0.), - (TouchPhase::Cancelled, 0.), - (TouchPhase::Moved, 20.), - ] { - let phases = Rc::new(RefCell::new(Vec::new())); - let window = cx.add_window({ - let phases = phases.clone(); - move |_, _| LongPressListener { - phases, - response: LongPressResponse::PreventDefault, - } - }); - dispatch_touch(window, cx, TouchId(1), TouchPhase::Started, 0.); - dispatch_touch(window, cx, TouchId(1), phase, position); - cx.executor().advance_clock(Duration::from_millis(501)); - cx.executor().run_until_parked(); - - assert!(phases.borrow().is_empty(), "{phase:?} allowed long press"); - } - } - - #[gpui::test] - fn stale_long_press_timer_cannot_affect_replacement_touch(cx: &mut TestAppContext) { - let phases = Rc::new(RefCell::new(Vec::new())); - let window = cx.add_window({ - let phases = phases.clone(); - move |_, _| LongPressListener { - phases, - response: LongPressResponse::PreventDefault, - } - }); - let first_touch = TouchId(1); - dispatch_touch(window, cx, first_touch, TouchPhase::Started, 0.); - cx.executor().advance_clock(Duration::from_millis(250)); - dispatch_touch(window, cx, first_touch, TouchPhase::Cancelled, 0.); - dispatch_touch(window, cx, TouchId(2), TouchPhase::Started, 10.); - - cx.executor().advance_clock(Duration::from_millis(251)); - cx.executor().run_until_parked(); - assert!(phases.borrow().is_empty()); - - cx.executor().advance_clock(Duration::from_millis(250)); - cx.executor().run_until_parked(); - assert_eq!(phases.borrow().as_slice(), [TouchPhase::Started]); - } - - #[derive(Clone, Copy, PartialEq)] - enum LongPressResponse { - PreventDefault, - StopPropagation, - None, - } - - struct LongPressListener { - phases: Rc>>, - response: LongPressResponse, - } - - impl Render for LongPressListener { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let entity = cx.entity(); - let phases = self.phases.clone(); - let response = self.response; - canvas( - |_, _, _| {}, - move |_, _, window, _| { - window.on_mouse_event(move |event: &LongPressEvent, phase, window, cx| { - if phase != DispatchPhase::Bubble { - return; - } - phases.borrow_mut().push(event.phase); - match response { - LongPressResponse::PreventDefault => { - window.capture_long_press(&entity); - window.prevent_default(); - } - LongPressResponse::StopPropagation => cx.stop_propagation(), - LongPressResponse::None => {} - } - }); - }, - ) - } - } - - fn dispatch_touch( - window: crate::WindowHandle, - cx: &mut TestAppContext, - id: TouchId, - phase: TouchPhase, - x: f32, - ) { - window - .update(cx, |_, window, cx| { - window.dispatch_event( - TouchEvent { - id, - phase, - position: point(px(x), px(0.)), - predicted_position: None, - force: None, - } - .to_platform_input(), - cx, - ); - }) - .unwrap(); - } -} diff --git a/crates/gpui_pre/src/window/a11y.rs b/crates/gpui_pre/src/window/a11y.rs deleted file mode 100644 index 234e7f0..0000000 --- a/crates/gpui_pre/src/window/a11y.rs +++ /dev/null @@ -1,888 +0,0 @@ -//! Accessibility support, provided by [AccessKit][accesskit]. -//! -//! There are user-facing guide-level docs [here](crate::_accessibility). -//! -//! ## Architecture -//! -//! ```text -//! ┌────────────────────────────────┐ ┌─────────────────────┐ -//! ┌─▶│ AccessKit Adapter (MacOS) │◀─▶│ MacOS System APIs │ -//! │ └────────────────────────────────┘ └─────────────────────┘ -//! │ -//! ┌──────┐ ┌───────────┐ │ ┌────────────────────────────────┐ ┌─────────────────────┐ -//! │ GPUI │◀─▶│ AccessKit │◀─┼─▶│ AccessKit Adapter (Windows) │◀─▶│ Windows System APIs │ -//! └──────┘ └───────────┘ │ └────────────────────────────────┘ └─────────────────────┘ -//! │ -//! │ ┌────────────────────────────────┐ ┌─────────────────────┐ -//! └─▶│ AccessKit Adapter (Linux) │◀─▶│ dbus │ -//! └────────────────────────────────┘ └─────────────────────┘ -//! ``` -//! -//! In order for GPUI apps to be usable for people using assistive technology, -//! we must do a few things: -//! - Inform the system when the UI changes meaningfully. This includes: -//! - Reporting new/removed/changed UI elements -//! - *Not* reporting irrelevant UI changes, e.g. an invisible `div()` being -//! added. -//! - Reporting the appearance and capabilities of each UI element. For example: -//! - What does this piece of text say? -//! - How far along is this progress bar? -//! - Can this node be focused? -//! - Can this node have a value directly assigned? (e.g. a slider) -//! - Allowing the system to interact with the UI by dispatching actions to -//! nodes. Note that AccessKit has its own [`Action`] type, which is not the -//! [`crate::Action`] trait. -//! - Activate and deactivate accessibility features when requested by the -//! system. -//! -//! Activating and deactivating at the right time is trivial, so I won't go into -//! detail here. The other two are almost orthogonal in implementation. -//! -//! The state for both lives in the [`A11y`] struct in this module. -//! -//! ### Reporting UI changes -//! -//! Every frame, we build a [`TreeUpdate`] and send it to the platform-specific -//! adapter. A [`TreeUpdate`] is a representation of a subset of the UI tree. -//! When the adapter receives the update, it diffs it against the previous -//! update, and calls platform-specific APIs to inform screen readers about the -//! changes. Nodes may have been created, destroyed, or updated. -//! -//! Each node has an ID, and this ID *should* be stable across frames. If a -//! node's ID changes, then, from AccessKit's point of view, it is a different -//! node. -//! -//! We derive the node ID from the [`GlobalElementId`] in -//! [`GlobalElementId::accesskit_node_id`]. Nodes without [`GlobalElementId`]s -//! cannot produce an AccessKit [`NodeId`], and so are not included in the -//! accessibility tree. We try to warn when using accessibility APIs on -//! [`div()`] without setting an ID. -//! -//! This all happens in [`Drawable::prepaint`]. The [`A11y`] struct maintains a -//! stack of nodes during prepainting, which we can use to calculate the -//! [`NodeId`]s, and record parent-child relationships. Once all [`Element`]s in -//! a frame have been prepainted, we send the resulting [`TreeUpdate`] object to -//! the adapter and the screen reader can announce the changes. -//! -//! #### Synthetic children -//! -//! Additionally, some nodes can register "synthetic children" using -//! [`Element::a11y_synthetic_children`]. Normally, one accesskit node is pushed -//! for every [`Element`] with a role and id. However, sometimes a single -//! element may want to produce many accesskit nodes. These extra nodes are -//! referred to as "synthetic children" of the element providing a non-default -//! [`Element::a11y_synthetic_children`] implementation. -//! -//! The user is provided a builder-style API using [`A11ySubtreeBuilder`], which -//! allows them to create push nodes that are children of the current node, as -//! well as modify the current node itself. -//! -//! GPUI calls this callback *after* prepainting (and just before popping the -//! corresponding element), since this step may need prepaint information to be -//! available. In the future, we may want to add prepaint information more -//! generally to [`Element::write_a11y_info`], but for now that's not necessary. -//! -//! ### Responding to actions -//! -//! On adapter creation, we provide a callback to the adapter, which can be used -//! to dispatch actions. This callback forwards to [`A11y::action_listeners`], a -//! mapping from [`NodeId`]s to action handlers (basically just `Box`). -//! -//! This is populated in: -//! - [`Window::on_a11y_action`], which is called by: -//! - [`Interactivity::paint`], which is called by: -//! - [`StatefulInteractiveElement::on_a11y_action`], which is a public-facing API -//! -//! These are cleared at the start of a frame, and re-populated during painting. -//! -//! [`NodeId`]: accesskit::NodeId - -use crate::*; - -pub(crate) mod debug; - -use crate::{App, Bounds, FocusId, Pixels, SharedString, Window}; -use accesskit::{Action, NodeId, TreeUpdate}; -use collections::{FxHashMap, FxHashSet}; -use smallvec::SmallVec; -use std::hash::{Hash, Hasher}; -use std::sync::{ - Arc, - atomic::{AtomicBool, Ordering}, -}; - -/// The fixed AccessKit node ID used for the root of every window's a11y tree. -pub(crate) const ROOT_NODE_ID: NodeId = NodeId(0); - -/// A listener for an accessibility action on a specific node. -pub(crate) type A11yActionListener = - Box, &mut Window, &mut App) + 'static>; - -/// Per-window accessibility state. -/// -/// Manages the AccessKit tree that is built each frame and the mappings -/// needed to dispatch incoming action requests back to the right elements. -pub(crate) struct A11y { - /// Whether accessibility has been [forcibly disabled] for this window. - /// - /// [forcibly disabled]: crate::Application::new_inaccessible - force_disabled: bool, - /// Whether a11y features have been requested by the system. - /// - /// Updated by AccessKit using callbacks provided to the adapter. Can change - /// halfway through a frame. - active_flag: Arc, - /// Whether a11y features are active for *this specific frame*. - /// - /// At the start of each frame, we load [`Self::active_flag`] (using - /// [`Self::sync_active_flag`]) and use this to determine whether we - /// should construct a [`TreeUpdate`] for this frame. It's important that - /// this value is stable within a frame, because the builder API exposed by - /// this type maintains a stack of nodes and each must be pushed and popped - /// exactly once. - /// - /// At the end of the frame, we re-call [`Self::sync_active_flag`] to - /// determine whether we should actually send the finished [`TreeUpdate`]. - active_this_frame: bool, - pub(crate) nodes: A11yNodeBuilder, - pub(crate) focus_ids: FxHashMap, - pub(crate) node_bounds: FxHashMap>, - pub(crate) action_listeners: FxHashMap>, - /// The window's title, used to label the root node so assistive - /// technology can tell windows apart. - window_title: Option, - /// The focus id we most recently reported as having no accessibility node, - /// used to log at most once per focus change rather than every frame. - last_focus_without_node: Option, - /// Retains the last tree update (and, in debug builds, per-node provenance) - /// so it can be dumped via [`crate::Window::debug_a11y_tree_json`]. - debug: debug::A11yDebug, - /// Maps a view's [`EntityId`] to its `Render` type name - #[cfg(debug_assertions)] - pub(crate) view_type_names: FxHashMap, -} - -impl A11y { - pub(crate) fn new( - active_flag: Arc, - force_disabled: bool, - window_title: Option, - ) -> Self { - Self { - force_disabled, - active_flag, - active_this_frame: false, - nodes: A11yNodeBuilder::new(), - focus_ids: FxHashMap::default(), - node_bounds: FxHashMap::default(), - action_listeners: FxHashMap::default(), - window_title, - last_focus_without_node: None, - debug: debug::A11yDebug::default(), - #[cfg(debug_assertions)] - view_type_names: FxHashMap::default(), - } - } - - /// Logs (once per focus change) that the focused element is not exposed to - /// assistive technology because it has no accessibility node. When this - /// happens, screen readers fall back to announcing the whole window instead - /// of the focused element. The fix is to give the element both an - /// `.id(...)` and a `.role(...)`. - pub(crate) fn note_focus_without_node(&mut self, focus_id: FocusId, reason: &str) { - if self.last_focus_without_node != Some(focus_id) { - self.last_focus_without_node = Some(focus_id); - log::info!( - "a11y: focused element ({focus_id:?}) has no accessibility node \ - ({reason}); assistive technology will announce the whole window \ - instead. Give it both an `.id(...)` and a `.role(...)` to expose it." - ); - } - } - - pub(crate) fn set_window_title(&mut self, title: impl Into) { - self.window_title = Some(title.into()); - } - - /// Ensures that [`Self::is_active`] returns up to date information. - /// - /// See the docs for [`Self::active_flag`] and [`Self::active_this_frame`] - /// for more commentary. - pub(crate) fn sync_active_flag(&mut self) { - self.active_this_frame = !self.force_disabled && self.active_flag.load(Ordering::SeqCst); - } - - pub(crate) fn is_active(&self) -> bool { - self.active_this_frame - } - - pub(crate) fn set_focusable(&mut self, node_id: NodeId, focus_id: FocusId) { - self.focus_ids.insert(node_id, focus_id); - } - - /// Report `node_id` as the currently-focused node, if it is present in the - /// tree. - /// - /// Must only be called once per frame. - pub(crate) fn set_focus(&mut self, node_id: NodeId) { - // A focused node must have been registered as focusable this frame. - if !self.focus_ids.contains_key(&node_id) { - if cfg!(debug_assertions) { - panic!("set_focus called for a node that was not registered with set_focusable"); - } else { - log::warn!( - "a11y: set_focus called for a node that was not registered with \ - set_focusable ({node_id:?})" - ); - } - } - if self.nodes.has_node(node_id) { - // The focused element is properly exposed; reset the dedup so a - // later focus on a node-less element logs again. - self.last_focus_without_node = None; - self.nodes.set_focus(node_id); - } else { - // The element registered a focus handle and an id, but never got a - // node because it has no role. - if let Some(focus_id) = self.focus_ids.get(&node_id).copied() { - self.note_focus_without_node(focus_id, "it has an id but no role"); - } - } - } - - pub(crate) fn set_active_descendant(&mut self, node_id: NodeId) { - // The active descendant must be a descendant of the focused container, - // not the focused node itself. - if self.nodes.node_is_focused(node_id) { - if cfg!(debug_assertions) { - panic!("set_active_descendant called on the focused node"); - } else { - log::warn!("a11y: set_active_descendant called on the focused node ({node_id:?})"); - } - return; - } - if self.nodes.has_node(node_id) && self.nodes.focus_is_ancestor_of_current() { - self.nodes.set_active_descendant(node_id); - } - } - - /// Clear per-frame state and push the root node to start a new frame. - pub(crate) fn begin_frame(&mut self) { - self.focus_ids.clear(); - self.node_bounds.clear(); - self.action_listeners.clear(); - self.nodes.begin_frame(self.window_title.as_ref()); - } - - /// Finalize the tree and produce a [`TreeUpdate`] for the platform adapter. - pub(crate) fn end_frame(&mut self, frame: debug::FrameDebugInfo) -> TreeUpdate { - let update = self.nodes.finalize(); - self.debug.capture( - &update, - self.nodes.focus, - self.nodes.active_descendant, - self.window_title.as_ref(), - frame, - ); - #[cfg(debug_assertions)] - self.debug.capture_node_info(&self.nodes.node_info); - update - } - - pub(crate) fn debug_tree_json(&self) -> Option { - self.debug.to_json() - } -} - -/// Builder API for synthetic children. See the docs for -/// [`Element::a11y_synthetic_children`]. -pub struct A11ySubtreeBuilder<'a> { - parent_id: NodeId, - nodes: &'a mut A11yNodeBuilder, - /// Provenance of the real element whose `a11y_synthetic_children` is - /// running. - #[cfg(debug_assertions)] - creator: debug::NodeCreator, -} - -impl<'a> A11ySubtreeBuilder<'a> { - pub(crate) fn new(parent_id: NodeId, nodes: &'a mut A11yNodeBuilder) -> Self { - Self { - parent_id, - nodes, - #[cfg(debug_assertions)] - creator: debug::NodeCreator::default(), - } - } - - #[cfg(debug_assertions)] - pub(crate) fn with_creator(mut self, creator: debug::NodeCreator) -> Self { - self.creator = creator; - self - } - - /// Derive a [`NodeId`] for a synthetic child. - /// - /// The generated ID is based on the hash of `key`, as well as the parent's - /// ID. This means that `key`s must be unique within the same - /// [`Element::a11y_synthetic_children`] call, but may be duplicated across - /// different calls. - pub fn synthetic_node_id(&self, key: impl Hash) -> NodeId { - let mut hasher = std::hash::DefaultHasher::default(); - self.parent_id.0.hash(&mut hasher); - key.hash(&mut hasher); - NodeId(hasher.finish()) - } - - /// Append a synthetic leaf node as a child of this element's node. - /// - /// Returns `false` if a node with this id is already present in the tree, - /// in which case the node is discarded. - pub fn push_child(&mut self, id: NodeId, node: accesskit::Node) -> bool { - let pushed = self.nodes.push_leaf(id, node); - #[cfg(debug_assertions)] - if pushed { - self.nodes.record_node_info( - id, - debug::NodeDebugInfo { - synthetic: true, - view: self.creator.view, - element_id: self.creator.element_id.clone(), - source_location: self.creator.source_location, - }, - ); - } - pushed - } - - /// A mutable reference to the parent node. - pub fn parent_node(&mut self) -> &mut accesskit::Node { - self.nodes - .current_node_mut() - .expect("A11ySubtreeBuilder exists only while its element's node is on the stack") - } -} - -pub(crate) struct A11yNodeBuilder { - ids_stack: SmallVec<[NodeId; 16]>, - nodes_stack: SmallVec<[accesskit::Node; 16]>, - /// This is the exact type required by accesskit, so we can't just make it a - /// `HashMap` to remove the need for `seen_ids` - all_nodes: Vec<(NodeId, accesskit::Node)>, - seen_ids: FxHashSet, - /// The node that GPUI considers focused. Note that this may be different to - /// what is reported to accesskit - see [`Self::active_descendant`] - focus: Option, - /// If a node calls `.aria_active_descendant()`, AND an ancestor is focused, - /// override it as the focused node. This supports the "active descendant" - /// pattern, which allows a focused container to act as if a descendant is - /// focused. - active_descendant: Option, - #[cfg(debug_assertions)] - node_info: FxHashMap, -} - -impl A11yNodeBuilder { - fn new() -> Self { - Self { - ids_stack: SmallVec::new(), - nodes_stack: SmallVec::new(), - all_nodes: Vec::new(), - seen_ids: FxHashSet::default(), - focus: None, - active_descendant: None, - #[cfg(debug_assertions)] - node_info: FxHashMap::default(), - } - } - - /// Records provenance for a node already pushed this frame. Debug builds only. - #[cfg(debug_assertions)] - pub(crate) fn record_node_info(&mut self, id: NodeId, info: debug::NodeDebugInfo) { - self.node_info.insert(id, info); - } - - #[must_use] - fn can_push(&mut self, id: NodeId) -> bool { - debug_assert!(!self.ids_stack.is_empty(), "node pushed before push_root"); - - if !self.seen_ids.insert(id) { - debug_assert!( - false, - "Duplicate a11y node id: {id:?}. In a release build, this node would be silently discarded from the a11y tree." - ); - return false; - } - - true - } - - /// Push a new node onto the stack. It becomes a child of the current - /// top-of-stack node. - /// - /// Returns `true` if the node was successfully pushed. - pub(crate) fn push(&mut self, id: NodeId, node: accesskit::Node) -> bool { - if !self.can_push(id) { - return false; - } - - if let Some(parent) = self.nodes_stack.last_mut() { - parent.push_child(id); - } - self.ids_stack.push(id); - self.nodes_stack.push(node); - true - } - - /// Add a leaf node as a child of the current top-of-stack node, without - /// pushing it onto the stack. Semantically equivalent to a [`Self::push`] - /// followed by a [`Self::pop`]. - /// - /// Returns `true` if the node was successfully pushed. - pub(crate) fn push_leaf(&mut self, id: NodeId, node: accesskit::Node) -> bool { - if !self.can_push(id) { - return false; - } - - if let Some(parent) = self.nodes_stack.last_mut() { - parent.push_child(id); - } - self.all_nodes.push((id, node)); - true - } - - pub(crate) fn current_node_mut(&mut self) -> Option<&mut accesskit::Node> { - self.nodes_stack.last_mut() - } - - /// Pop the current node off the stack and finalize it into the all_nodes - /// list. - pub(crate) fn pop(&mut self) { - debug_assert!(self.ids_stack.len() > 1, "pop would remove the root node"); - - if let (Some(id), Some(node)) = (self.ids_stack.pop(), self.nodes_stack.pop()) { - self.all_nodes.push((id, node)); - } - } - - /// Push the root node to start a new frame. - fn begin_frame(&mut self, window_title: Option<&SharedString>) { - self.all_nodes.clear(); - self.ids_stack.clear(); - self.nodes_stack.clear(); - self.seen_ids.clear(); - #[cfg(debug_assertions)] - self.node_info.clear(); - let mut root_node = accesskit::Node::new(accesskit::Role::Window); - if let Some(title) = window_title { - root_node.set_label(title.to_string()); - } - - self.ids_stack.push(ROOT_NODE_ID); - self.nodes_stack.push(root_node); - self.focus = None; - self.active_descendant = None; - } - - /// Returns whether a node with the given ID has been pushed in this frame. - pub(crate) fn has_node(&self, id: NodeId) -> bool { - id == ROOT_NODE_ID || self.seen_ids.contains(&id) - } - - /// Returns whether `id` is the node currently reported as focused. - pub(crate) fn node_is_focused(&self, id: NodeId) -> bool { - self.focus == Some(id) - } - - pub(crate) fn focus_is_ancestor_of_current(&self) -> bool { - let Some(focus) = self.focus else { - return false; - }; - - // The current node is on top of the stack; everything below it is an - // ancestor. - let ancestor_count = self.ids_stack.len().saturating_sub(1); - self.ids_stack[..ancestor_count].contains(&focus) - } - - pub(crate) fn set_active_descendant(&mut self, id: NodeId) { - if self - .active_descendant - .is_some_and(|existing| existing != id) - { - if cfg!(debug_assertions) { - panic!("active descendant claimed by multiple nodes in one frame"); - } else { - log::warn!( - "a11y: multiple nodes claimed the active descendant this frame; \ - using last-wins ({id:?})" - ); - } - } - self.active_descendant = Some(id); - } - - pub(crate) fn set_focus(&mut self, id: NodeId) { - if self.focus.is_some() { - if cfg!(debug_assertions) { - panic!("set_focus called more than once in a single frame"); - } else { - log::warn!( - "a11y: set_focus called more than once in a single frame; \ - using last-wins ({id:?})" - ); - } - } - self.focus = Some(id); - } - - fn finalize(&mut self) -> TreeUpdate { - // Stack should contain only the root node - debug_assert_eq!(self.ids_stack.len(), 1); - debug_assert_eq!(self.ids_stack[0], ROOT_NODE_ID); - - if self.ids_stack.len() != 1 { - log::error!( - "a11y: Stack imbalance at end of frame: expected 1 (root), got {}. \ - Some elements may have pushed without popping.", - self.ids_stack.len() - ); - } - - // Pop remaining nodes (should just be the root). - while !self.ids_stack.is_empty() { - if let (Some(id), Some(node)) = (self.ids_stack.pop(), self.nodes_stack.pop()) { - self.all_nodes.push((id, node)); - } - } - - let focus = match self.active_descendant { - Some(id) if self.has_node(id) => id, - Some(id) => { - if cfg!(debug_assertions) { - panic!("active_descendant set to {id:?}, which is not in the tree"); - } else { - log::warn!("active_descendant set to {id:?}, which is not in the tree"); - self.focus.unwrap_or(ROOT_NODE_ID) - } - } - - _ => self.focus.unwrap_or(ROOT_NODE_ID), - }; - - let nodes = std::mem::take(&mut self.all_nodes); - let update = TreeUpdate { - nodes, - tree: Some(accesskit::Tree::new(ROOT_NODE_ID)), - tree_id: accesskit::TreeId::ROOT, - focus, - }; - - Self::repair_tree_update(update) - } - - /// Accesskit panics on invalid [`TreeUpdate`]s. This function defensively - /// checks invariants that accesskit panics on, and tries to fix them. - fn repair_tree_update(mut update: TreeUpdate) -> TreeUpdate { - let node_ids: FxHashSet = update.nodes.iter().map(|(id, _)| *id).collect(); - - // Focus must point to a node in the tree. - if !node_ids.contains(&update.focus) { - log::error!( - "a11y: Focused node {:?} is not in the tree ({} nodes). \ - Falling back to root. This is a bug in the a11y tree builder.", - update.focus, - update.nodes.len() - ); - update.focus = ROOT_NODE_ID; - } - - // Every child reference must point to a node in the update. - for (id, node) in &mut update.nodes { - let has_invalid_child = node - .children() - .iter() - .any(|child_id| !node_ids.contains(child_id)); - if has_invalid_child { - let children = node.children(); - let invalid_count = children - .iter() - .filter(|child_id| !node_ids.contains(child_id)) - .count(); - log::error!( - "a11y: Node {:?} references {} children not present in the tree. \ - Stripping invalid child references.", - id, - invalid_count - ); - let valid: Vec = children - .iter() - .copied() - .filter(|child_id| node_ids.contains(child_id)) - .collect(); - node.set_children(valid); - } - } - - update - } -} - -#[cfg(test)] -mod tests { - // Import specific items rather than glob-importing `super`, which would pull - // in gpui's own `test` attribute macro and shadow the standard one. - use super::{A11y, A11yNodeBuilder, ROOT_NODE_ID}; - use crate::FocusId; - use accesskit::{NodeId, Role}; - use std::sync::{Arc, atomic::AtomicBool}; - - fn test_node() -> accesskit::Node { - accesskit::Node::new(Role::GenericContainer) - } - - fn new_builder() -> A11yNodeBuilder { - let mut builder = A11yNodeBuilder::new(); - builder.begin_frame(None); - builder - } - - fn new_a11y() -> A11y { - let mut a11y = A11y::new(Arc::new(AtomicBool::new(true)), false, None); - a11y.begin_frame(); - a11y - } - - #[test] - fn active_descendant_honored_when_container_focused() { - let mut builder = new_builder(); - let container = NodeId(1); - let item = NodeId(2); - - assert!(builder.push(container, test_node())); - builder.set_focus(container); - assert!(builder.push(item, test_node())); - - // The item is on top of the stack; the focused container is its - // ancestor, so the claim is honored. - assert!(builder.focus_is_ancestor_of_current()); - builder.set_active_descendant(item); - - builder.pop(); // item - builder.pop(); // container - let update = builder.finalize(); - assert_eq!(update.focus, item); - } - - #[test] - fn active_descendant_honored_for_deep_descendant() { - let mut builder = new_builder(); - let container = NodeId(1); - let group = NodeId(2); - let item = NodeId(3); - - assert!(builder.push(container, test_node())); - builder.set_focus(container); - assert!(builder.push(group, test_node())); - assert!(builder.push(item, test_node())); - - // The item is a grandchild of the focused container; depth doesn't - // matter, the focused ancestor is still on the stack. - assert!(builder.focus_is_ancestor_of_current()); - builder.set_active_descendant(item); - - builder.pop(); // item - builder.pop(); // group - builder.pop(); // container - let update = builder.finalize(); - assert_eq!(update.focus, item); - } - - #[test] - fn active_descendant_ignored_when_focus_in_other_subtree() { - let mut builder = new_builder(); - let focused_container = NodeId(1); - let focused_leaf = NodeId(2); - let other_container = NodeId(3); - let other_item = NodeId(4); - - // First subtree holds real focus. - assert!(builder.push(focused_container, test_node())); - assert!(builder.push(focused_leaf, test_node())); - builder.set_focus(focused_leaf); - builder.pop(); // focused_leaf - builder.pop(); // focused_container - - // Second subtree: its item would claim the active descendant, but the - // focus is not on any of its ancestors, so the gate rejects it. - assert!(builder.push(other_container, test_node())); - assert!(builder.push(other_item, test_node())); - assert!(!builder.focus_is_ancestor_of_current()); - builder.pop(); // other_item - builder.pop(); // other_container - - let update = builder.finalize(); - assert_eq!(update.focus, focused_leaf); - } - - #[test] - fn active_descendant_ignored_when_nothing_focused() { - let mut builder = new_builder(); - let container = NodeId(1); - let item = NodeId(2); - - assert!(builder.push(container, test_node())); - assert!(builder.push(item, test_node())); - - // Nothing is focused (focus defaults to the root window node), so the - // gate rejects the claim. - assert!(!builder.focus_is_ancestor_of_current()); - builder.pop(); - builder.pop(); - - let update = builder.finalize(); - assert_eq!(update.focus, ROOT_NODE_ID); - } - - #[test] - fn regular_focus_used_when_no_active_descendant() { - let mut builder = new_builder(); - let focused = NodeId(1); - - assert!(builder.push(focused, test_node())); - builder.set_focus(focused); - builder.pop(); - - let update = builder.finalize(); - assert_eq!(update.focus, focused); - } - - #[test] - fn focus_is_ancestor_excludes_self_and_non_ancestors() { - let mut builder = new_builder(); - let container = NodeId(1); - let item = NodeId(2); - - assert!(builder.push(container, test_node())); - builder.set_focus(container); - - // With the focused container itself on top, it is not its own (strict) - // ancestor, so the gate is false. - assert!(!builder.focus_is_ancestor_of_current()); - - assert!(builder.push(item, test_node())); - // Now the focused container is a strict ancestor of the item on top. - assert!(builder.focus_is_ancestor_of_current()); - - builder.pop(); - builder.pop(); - } - - // The double-claim guard panics only in debug builds; in release it falls - // back to last-wins with a warning. - #[test] - #[cfg_attr( - debug_assertions, - should_panic(expected = "active descendant claimed by multiple nodes") - )] - fn multiple_active_descendant_claims_panic_in_debug() { - let mut builder = new_builder(); - builder.set_active_descendant(NodeId(1)); - builder.set_active_descendant(NodeId(2)); - } - - // Setting focus twice in one frame means two elements both claimed window - // focus; that panics in debug and falls back to last-wins in release. - #[test] - #[cfg_attr( - debug_assertions, - should_panic(expected = "set_focus called more than once") - )] - fn setting_focus_twice_panics_in_debug() { - let mut builder = new_builder(); - builder.set_focus(NodeId(1)); - builder.set_focus(NodeId(2)); - } - - // Focusing a node that was never registered as focusable is a bug: panic in - // debug, warn in release. - #[test] - #[cfg_attr( - debug_assertions, - should_panic(expected = "was not registered with set_focusable") - )] - fn set_focus_without_set_focusable() { - let mut a11y = new_a11y(); - let node = NodeId(1); - assert!(a11y.nodes.push(node, test_node())); - // set_focusable was never called for `node`. - a11y.set_focus(node); - } - - // The focused node cannot also be its own active descendant: panic in - // debug, warn in release. - #[test] - #[cfg_attr(debug_assertions, should_panic(expected = "on the focused node"))] - fn set_active_descendant_on_focused_node() { - let mut a11y = new_a11y(); - let node = NodeId(1); - assert!(a11y.nodes.push(node, test_node())); - a11y.set_focusable(node, FocusId::default()); - a11y.set_focus(node); - a11y.set_active_descendant(node); - } - - // Two sibling children of a focused container both claim the active - // descendant (both pass the focus gate). The second claim is a bug: panic - // in debug, last-wins + warn in release. - #[test] - #[cfg_attr( - debug_assertions, - should_panic(expected = "active descendant claimed by multiple nodes") - )] - fn two_siblings_claiming_active_descendant() { - let mut a11y = new_a11y(); - let container = NodeId(1); - let first = NodeId(2); - let second = NodeId(3); - - assert!(a11y.nodes.push(container, test_node())); - a11y.set_focusable(container, FocusId::default()); - a11y.set_focus(container); - - assert!(a11y.nodes.push(first, test_node())); - a11y.set_active_descendant(first); - a11y.nodes.pop(); // first - - assert!(a11y.nodes.push(second, test_node())); - a11y.set_active_descendant(second); - a11y.nodes.pop(); // second - - a11y.nodes.pop(); // container - } - - // Node A is focused; node C (a child of the unfocused node B) claims the - // active descendant. The final tree must still report A as focused. - #[test] - fn active_descendant_in_unfocused_subtree_keeps_real_focus() { - let mut a11y = new_a11y(); - let a = NodeId(1); - let b = NodeId(2); - let c = NodeId(3); - - assert!(a11y.nodes.push(a, test_node())); - a11y.set_focusable(a, FocusId::default()); - a11y.set_focus(a); - a11y.nodes.pop(); // a - - assert!(a11y.nodes.push(b, test_node())); - assert!(a11y.nodes.push(c, test_node())); - a11y.set_active_descendant(c); - a11y.nodes.pop(); // c - a11y.nodes.pop(); // b - - let update = a11y.end_frame(Default::default()); - assert_eq!(update.focus, a); - } -} diff --git a/crates/gpui_pre/src/window/a11y/debug.rs b/crates/gpui_pre/src/window/a11y/debug.rs deleted file mode 100644 index ae98aa5..0000000 --- a/crates/gpui_pre/src/window/a11y/debug.rs +++ /dev/null @@ -1,330 +0,0 @@ -//! Developer tooling for inspecting the accessibility tree. -//! -//! [`A11yDebug`] retains the last [`TreeUpdate`] sent to the platform adapter so -//! it can be serialized on demand (see -//! [`crate::Window::debug_a11y_tree_json`]). In `cfg(debug_assertions)` builds, -//! we capture extra info. - -use accesskit::{Action, NodeId, TreeUpdate}; -use collections::FxHashMap; - -use crate::{Pixels, SharedString, Size}; - -#[derive(Default)] -pub(crate) struct FrameDebugInfo { - pub viewport_size: Size, - pub scale_factor: f32, - pub tab_stop_count: usize, -} - -struct CapturedFrame { - rendered_at: String, - frame_number: u64, - window_title: Option, - node_count: usize, - tab_stop_count: usize, - viewport_size: Size, - scale_factor: f32, -} - -#[cfg(debug_assertions)] -#[derive(Clone, Default)] -pub(crate) struct NodeDebugInfo { - /// Whether the node was synthesized via - /// [`crate::Element::a11y_synthetic_children`] rather than created from a - /// real element with a role and ID. - pub synthetic: bool, - /// The type name of the `Render` view that was rendering when the node was - /// created. - pub view: Option<&'static str>, - /// The [`ElementId`](crate::ElementId) of the creating element (the leaf of - /// its `GlobalElementId`, not the full path). For a synthetic node, this is - /// the real element whose `a11y_synthetic_children` produced it. - pub element_id: Option, - /// Source location where the creating element was constructed. - pub source_location: Option<&'static core::panic::Location<'static>>, -} - -#[cfg(debug_assertions)] -#[derive(Clone, Default)] -pub(crate) struct NodeCreator { - pub view: Option<&'static str>, - pub element_id: Option, - pub source_location: Option<&'static core::panic::Location<'static>>, -} - -#[derive(Default)] -pub(crate) struct A11yDebug { - last_tree_update: Option, - last_gpui_focus: Option, - last_active_descendant: Option, - /// Monotonic counter incremented on each captured frame, so a re-dump makes - /// it obvious whether the tree actually refreshed. - frame_number: u64, - /// Metadata about the most recently captured frame. - last_frame: Option, - #[cfg(debug_assertions)] - last_node_info: FxHashMap, -} - -impl A11yDebug { - pub(crate) fn capture( - &mut self, - update: &TreeUpdate, - gpui_focus: Option, - active_descendant: Option, - window_title: Option<&SharedString>, - frame: FrameDebugInfo, - ) { - self.last_tree_update = Some(update.clone()); - self.last_gpui_focus = gpui_focus; - self.last_active_descendant = active_descendant; - self.frame_number += 1; - self.last_frame = Some(CapturedFrame { - rendered_at: chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, false), - frame_number: self.frame_number, - window_title: window_title.cloned(), - node_count: update.nodes.len(), - tab_stop_count: frame.tab_stop_count, - viewport_size: frame.viewport_size, - scale_factor: frame.scale_factor, - }); - } - - #[cfg(debug_assertions)] - pub(crate) fn capture_node_info(&mut self, node_info: &FxHashMap) { - self.last_node_info = node_info.clone(); - } - - /// Serialize the last tree update to a readable JSON string. Node ids are - /// replaced with short ephemeral ids (`a`, `b`, ..., `z`, `aa`, ...). - pub(crate) fn to_json(&self) -> Option { - let update = self.last_tree_update.as_ref()?; - - let mut ephemeral: FxHashMap = FxHashMap::default(); - for (index, (id, _)) in update.nodes.iter().enumerate() { - ephemeral.insert(*id, ephemeral_id(index)); - } - - let mut nodes = serde_json::Map::new(); - for (id, node) in &update.nodes { - let key = ephemeral - .get(id) - .cloned() - .unwrap_or_else(|| id.0.to_string()); - #[cfg(debug_assertions)] - let provenance = self - .last_node_info - .get(id) - .map(|info| NodeProvenance { - element_id: info.element_id.as_deref(), - view: info.view, - source_location: info.source_location.map(|loc| loc.to_string()), - // Only surface synthetic nodes; `false` is the default and - // would just be noise on every real node. - synthetic: info.synthetic.then_some(true), - }) - .unwrap_or_default(); - #[cfg(not(debug_assertions))] - let provenance = NodeProvenance::default(); - let value = node_to_json(*id, node, &ephemeral, &provenance); - nodes.insert(key, value); - } - - let frame = self.last_frame.as_ref().map(|frame| { - serde_json::json!({ - "rendered_at": frame.rendered_at, - "frame_number": frame.frame_number, - "window_title": frame.window_title.as_ref().map(|title| title.to_string()), - "node_count": frame.node_count, - "tab_stop_count": frame.tab_stop_count, - "viewport_size": { - "width": frame.viewport_size.width.0, - "height": frame.viewport_size.height.0, - }, - "scale_factor": frame.scale_factor, - }) - }); - - let root = update - .tree - .as_ref() - .map(|tree| tree.root) - .and_then(|id| ephemeral.get(&id).cloned()); - - let value = serde_json::json!({ - "root": root, - "gpui_focus": self.last_gpui_focus.and_then(|id| ephemeral.get(&id).cloned()), - "active_descendant_focus": self.last_active_descendant.and_then(|id| ephemeral.get(&id).cloned()), - "frame": frame, - "nodes": nodes, - }); - Some(serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{}".to_string())) - } -} - -#[derive(Default)] -struct NodeProvenance<'a> { - element_id: Option<&'a str>, - view: Option<&'a str>, - source_location: Option, - synthetic: Option, -} - -fn node_to_json( - id: NodeId, - node: &accesskit::Node, - ephemeral: &FxHashMap, - provenance: &NodeProvenance, -) -> serde_json::Value { - use serde_json::json; - - let mut map = serde_json::Map::new(); - map.insert("accesskit_id".into(), json!(id.0.to_string())); - - let children: Vec = node - .children() - .iter() - .map(|child| { - ephemeral - .get(child) - .cloned() - .unwrap_or_else(|| child.0.to_string()) - }) - .collect(); - if !children.is_empty() { - map.insert("children".into(), json!(children)); - } - - // Provenance (debug builds only), ordered before the accessibility section. - if let Some(element_id) = provenance.element_id { - map.insert("element_id".into(), json!(element_id)); - } - if let Some(view) = provenance.view { - map.insert("view".into(), json!(view)); - } - if let Some(source_location) = &provenance.source_location { - map.insert("source_location".into(), json!(source_location)); - } - if let Some(synthetic) = provenance.synthetic { - map.insert("synthetic".into(), json!(synthetic)); - } - - // Accessibility semantics for this node, grouped together. - let mut aria = serde_json::Map::new(); - aria.insert("role".into(), json!(format!("{:?}", node.role()))); - - // Which action types the node supports. AccessKit keeps these in a private - // bitset with no getter or iterator, so we probe each variant. `Action::n` - // (from AccessKit's `enumn` feature) maps a discriminant to its variant, - // returning `None` past the last one - so this can't drift out of sync with - // AccessKit's `Action` enum the way a hand-maintained list would. - let mut next_action = 0u8; - let on_action: Vec = std::iter::from_fn(move || { - let action = Action::n(next_action)?; - next_action += 1; - Some(action) - }) - .filter(|action| node.supports_action(*action)) - .map(|action| format!("{action:?}")) - .collect(); - if !on_action.is_empty() { - aria.insert("on_action".into(), json!(on_action)); - } - - // String properties. - if let Some(v) = node.label() { - aria.insert("label".into(), json!(v)); - } - if let Some(v) = node.description() { - aria.insert("description".into(), json!(v)); - } - if let Some(v) = node.value() { - aria.insert("value".into(), json!(v)); - } - if let Some(v) = node.keyboard_shortcut() { - aria.insert("keyboard_shortcut".into(), json!(v)); - } - if let Some(v) = node.access_key() { - aria.insert("access_key".into(), json!(v)); - } - if let Some(v) = node.placeholder() { - aria.insert("placeholder".into(), json!(v)); - } - if let Some(v) = node.tooltip() { - aria.insert("tooltip".into(), json!(v)); - } - if let Some(v) = node.role_description() { - aria.insert("role_description".into(), json!(v)); - } - - // Boolean / enum states. - if let Some(v) = node.is_selected() { - aria.insert("selected".into(), json!(v)); - } - if let Some(v) = node.is_expanded() { - aria.insert("expanded".into(), json!(v)); - } - if let Some(v) = node.toggled() { - aria.insert("toggled".into(), json!(format!("{v:?}"))); - } - if let Some(v) = node.orientation() { - aria.insert("orientation".into(), json!(format!("{v:?}"))); - } - - // Numeric properties. - if let Some(v) = node.numeric_value() { - aria.insert("numeric_value".into(), json!(v)); - } - if let Some(v) = node.min_numeric_value() { - aria.insert("min_numeric_value".into(), json!(v)); - } - if let Some(v) = node.max_numeric_value() { - aria.insert("max_numeric_value".into(), json!(v)); - } - if let Some(v) = node.numeric_value_step() { - aria.insert("numeric_value_step".into(), json!(v)); - } - - // Set / table properties. - if let Some(v) = node.level() { - aria.insert("level".into(), json!(v)); - } - if let Some(v) = node.position_in_set() { - aria.insert("position_in_set".into(), json!(v)); - } - if let Some(v) = node.size_of_set() { - aria.insert("size_of_set".into(), json!(v)); - } - if let Some(v) = node.row_index() { - aria.insert("row_index".into(), json!(v)); - } - if let Some(v) = node.column_index() { - aria.insert("column_index".into(), json!(v)); - } - if let Some(v) = node.row_count() { - aria.insert("row_count".into(), json!(v)); - } - if let Some(v) = node.column_count() { - aria.insert("column_count".into(), json!(v)); - } - - map.insert("aria".into(), serde_json::Value::Object(aria)); - - serde_json::Value::Object(map) -} - -/// Maps a 0-based index to a short id in the sequence `a, b, ..., z, aa, ab, -/// ...` (bijective base-26). -fn ephemeral_id(mut index: usize) -> String { - let mut bytes = Vec::new(); - loop { - bytes.push(b'a' + (index % 26) as u8); - if index < 26 { - break; - } - index = index / 26 - 1; - } - bytes.reverse(); - String::from_utf8(bytes).unwrap_or_default() -} diff --git a/crates/gpui_pre/src/window/prompts.rs b/crates/gpui_pre/src/window/prompts.rs deleted file mode 100644 index 980c6f6..0000000 --- a/crates/gpui_pre/src/window/prompts.rs +++ /dev/null @@ -1,232 +0,0 @@ -use std::ops::Deref; - -use futures::channel::oneshot; - -use crate::{ - AnyView, App, AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable, - InteractiveElement, IntoElement, ParentElement, PromptButton, PromptLevel, Render, - StatefulInteractiveElement, Styled, div, opaque_grey, white, -}; - -use super::Window; - -/// The event emitted when a prompt's option is selected. -/// The usize is the index of the selected option, from the actions -/// passed to the prompt. -pub struct PromptResponse(pub usize); - -/// A prompt that can be rendered in the window. -pub trait Prompt: EventEmitter + Focusable {} - -impl + Focusable> Prompt for V {} - -/// A handle to a prompt that can be used to interact with it. -pub struct PromptHandle { - sender: oneshot::Sender, -} - -impl PromptHandle { - pub(crate) fn new(sender: oneshot::Sender) -> Self { - Self { sender } - } - - /// Construct a new prompt handle from a view of the appropriate types - pub fn with_view( - self, - view: Entity, - window: &mut Window, - cx: &mut App, - ) -> RenderablePromptHandle { - let mut sender = Some(self.sender); - let previous_focus = window.focused(cx); - let window_handle = window.window_handle(); - cx.subscribe(&view, move |_: Entity, e: &PromptResponse, cx| { - if let Some(sender) = sender.take() { - sender.send(e.0).ok(); - window_handle - .update(cx, |_, window, cx| { - window.prompt.take(); - if let Some(previous_focus) = &previous_focus { - window.focus(previous_focus, cx); - } - }) - .ok(); - } - }) - .detach(); - - window.focus(&view.focus_handle(cx), cx); - - RenderablePromptHandle { - view: Box::new(view), - } - } -} - -/// A prompt handle capable of being rendered in a window. -pub struct RenderablePromptHandle { - pub(crate) view: Box, -} - -/// Use this function in conjunction with [App::set_prompt_builder] to force -/// GPUI to always use the fallback prompt renderer. -pub fn fallback_prompt_renderer( - level: PromptLevel, - message: &str, - detail: Option<&str>, - actions: &[PromptButton], - handle: PromptHandle, - window: &mut Window, - cx: &mut App, -) -> RenderablePromptHandle { - let renderer = cx.new(|cx| FallbackPromptRenderer { - _level: level, - message: message.to_string(), - detail: detail.map(ToString::to_string), - actions: actions.to_vec(), - focus: cx.focus_handle(), - }); - - handle.with_view(renderer, window, cx) -} - -/// The default GPUI fallback for rendering prompts, when the platform doesn't support it. -pub struct FallbackPromptRenderer { - _level: PromptLevel, - message: String, - detail: Option, - actions: Vec, - focus: FocusHandle, -} - -impl Render for FallbackPromptRenderer { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let prompt = div() - .cursor_default() - .track_focus(&self.focus) - .w_72() - .bg(white()) - .rounded_lg() - .overflow_hidden() - .p_3() - .child( - div() - .w_full() - .flex() - .flex_row() - .justify_around() - .child(div().overflow_hidden().child(self.message.clone())), - ) - .children(self.detail.clone().map(|detail| { - div() - .w_full() - .flex() - .flex_row() - .justify_around() - .text_sm() - .mb_2() - .child(div().child(detail)) - })) - .children(self.actions.iter().enumerate().map(|(ix, action)| { - div() - .flex() - .flex_row() - .justify_around() - .border_1() - .border_color(opaque_grey(0.2, 0.5)) - .mt_1() - .rounded_xs() - .cursor_pointer() - .text_sm() - .child(action.label().clone()) - .id(ix) - .on_click(cx.listener(move |_, _, _, cx| { - cx.emit(PromptResponse(ix)); - cx.stop_propagation(); - })) - })); - - div() - .size_full() - .child( - div() - .size_full() - .bg(opaque_grey(0.5, 0.6)) - .absolute() - .top_0() - .left_0(), - ) - .child( - div() - .size_full() - .absolute() - .top_0() - .left_0() - .flex() - .flex_col() - .justify_around() - .child( - div() - .w_full() - .flex() - .flex_row() - .justify_around() - .child(prompt), - ), - ) - } -} - -impl EventEmitter for FallbackPromptRenderer {} - -impl Focusable for FallbackPromptRenderer { - fn focus_handle(&self, _: &crate::App) -> FocusHandle { - self.focus.clone() - } -} - -pub(crate) trait PromptViewHandle { - fn any_view(&self) -> AnyView; -} - -impl PromptViewHandle for Entity { - fn any_view(&self) -> AnyView { - self.clone().into() - } -} - -pub(crate) enum PromptBuilder { - Default, - Custom( - Box< - dyn Fn( - PromptLevel, - &str, - Option<&str>, - &[PromptButton], - PromptHandle, - &mut Window, - &mut App, - ) -> RenderablePromptHandle, - >, - ), -} - -impl Deref for PromptBuilder { - type Target = dyn Fn( - PromptLevel, - &str, - Option<&str>, - &[PromptButton], - PromptHandle, - &mut Window, - &mut App, - ) -> RenderablePromptHandle; - - fn deref(&self) -> &Self::Target { - match self { - Self::Default => &fallback_prompt_renderer, - Self::Custom(f) => f.as_ref(), - } - } -} diff --git a/crates/gpui_pre/tests/action_macros.rs b/crates/gpui_pre/tests/action_macros.rs deleted file mode 100644 index 66ef6fb..0000000 --- a/crates/gpui_pre/tests/action_macros.rs +++ /dev/null @@ -1,55 +0,0 @@ -use gpui::{Action, actions}; -use gpui_macros::register_action; -use schemars::JsonSchema; -use serde::Deserialize; - -#[test] -fn test_action_macros() { - actions!( - test_only, - [ - SomeAction, - /// Documented action - SomeActionWithDocs, - ] - ); - - #[derive(PartialEq, Clone, Deserialize, JsonSchema, Action)] - #[action(namespace = test_only)] - #[serde(deny_unknown_fields)] - struct AnotherAction; - - #[derive(PartialEq, Clone, gpui::private::serde::Deserialize)] - #[serde(deny_unknown_fields)] - struct RegisterableAction {} - - register_action!(RegisterableAction); - - impl gpui::Action for RegisterableAction { - fn boxed_clone(&self) -> Box { - unimplemented!() - } - - fn partial_eq(&self, _action: &dyn gpui::Action) -> bool { - unimplemented!() - } - - fn name(&self) -> &'static str { - unimplemented!() - } - - fn name_for_type() -> &'static str - where - Self: Sized, - { - unimplemented!() - } - - fn build(_value: serde_json::Value) -> anyhow::Result> - where - Self: Sized, - { - unimplemented!() - } - } -} diff --git a/crates/gpui_pre_apple/Cargo.lock b/crates/gpui_pre_apple/Cargo.lock deleted file mode 100644 index 72ca762..0000000 --- a/crates/gpui_pre_apple/Cargo.lock +++ /dev/null @@ -1,4811 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "accesskit" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" -dependencies = [ - "enumn", - "uuid", -] - -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "aligned" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" -dependencies = [ - "as-slice", -] - -[[package]] -name = "aligned-vec" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" -dependencies = [ - "equator", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anes" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" - -[[package]] -name = "anstyle" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" - -[[package]] -name = "anyhow" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" - -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" - -[[package]] -name = "arg_enum_proc_macro" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "as-slice" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" -dependencies = [ - "stable_deref_trait", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-compression" -version = "0.4.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0" -dependencies = [ - "compression-codecs", - "compression-core", - "futures-core", - "futures-io", - "pin-project-lite", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "atomic" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "av-scenechange" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" -dependencies = [ - "aligned", - "anyhow", - "arg_enum_proc_macro", - "arrayvec", - "log", - "num-rational", - "num-traits", - "pastey", - "rayon", - "thiserror 2.0.17", - "v_frame", - "y4m", -] - -[[package]] -name = "av1-grain" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" -dependencies = [ - "anyhow", - "arrayvec", - "log", - "nom", - "num-rational", - "v_frame", -] - -[[package]] -name = "avif-serialize" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link 0.2.1", -] - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.13.1", - "cexpr", - "clang-sys", - "itertools 0.11.0", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex 1.3.0", - "syn 2.0.117", -] - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bit_field" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" - -[[package]] -name = "bitstream-io" -version = "4.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" -dependencies = [ - "core2", -] - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "borsh" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" -dependencies = [ - "cfg_aliases", -] - -[[package]] -name = "built" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "bytemuck" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - -[[package]] -name = "bzip2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" -dependencies = [ - "libbz2-rs-sys", -] - -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - -[[package]] -name = "cbindgen" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff" -dependencies = [ - "heck 0.4.1", - "indexmap", - "log", - "proc-macro2", - "quote", - "serde", - "serde_json", - "syn 2.0.117", - "tempfile", - "toml", -] - -[[package]] -name = "cc" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex 2.0.1", -] - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "cgl" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" -dependencies = [ - "libc", -] - -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link 0.2.1", -] - -[[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" -dependencies = [ - "ciborium-io", - "half", -] - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "clap" -version = "4.5.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" -dependencies = [ - "clap_builder", -] - -[[package]] -name = "clap_builder" -version = "4.5.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" -dependencies = [ - "anstyle", - "clap_lex", -] - -[[package]] -name = "clap_lex" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" - -[[package]] -name = "cocoa" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" -dependencies = [ - "bitflags 1.3.2", - "block", - "cocoa-foundation 0.1.2", - "core-foundation 0.9.4", - "core-graphics 0.23.2", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "cocoa" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" -dependencies = [ - "bitflags 2.13.1", - "block", - "cocoa-foundation 0.2.0", - "core-foundation 0.10.0", - "core-graphics 0.24.0", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" -dependencies = [ - "bitflags 1.3.2", - "block", - "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" -dependencies = [ - "bitflags 2.13.1", - "block", - "core-foundation 0.10.0", - "core-graphics-types 0.2.0", - "libc", - "objc", -] - -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - -[[package]] -name = "compression-codecs" -version = "0.4.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" -dependencies = [ - "bzip2", - "compression-core", - "flate2", - "memchr", -] - -[[package]] -name = "compression-core" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "convert_case" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" -dependencies = [ - "bitflags 2.13.1", - "core-foundation 0.10.0", - "core-graphics-types 0.2.0", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-helmer-fork" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5" -dependencies = [ - "bitflags 2.13.1", - "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.13.1", - "core-foundation 0.10.0", - "libc", -] - -[[package]] -name = "core-graphics2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4416167a69126e617f8d0a214af0e3c1dbdeffcb100ddf72dcd1a1ac9893c146" -dependencies = [ - "bitflags 2.13.1", - "block", - "cfg-if", - "core-foundation 0.10.0", - "libc", -] - -[[package]] -name = "core-video" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139679cc63eb9504bdbe37e37874b0247136177655f0008588781e90863afa62" -dependencies = [ - "block", - "core-foundation 0.10.0", - "core-graphics2", - "io-surface", - "libc", - "metal", -] - -[[package]] -name = "core2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" -dependencies = [ - "memchr", -] - -[[package]] -name = "core_maths" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" -dependencies = [ - "libm", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "criterion" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" -dependencies = [ - "anes", - "cast", - "ciborium", - "clap", - "criterion-plot", - "is-terminal", - "itertools 0.10.5", - "num-traits", - "once_cell", - "oorandom", - "plotters", - "rayon", - "regex", - "serde", - "serde_derive", - "serde_json", - "tinytemplate", - "walkdir", -] - -[[package]] -name = "criterion-plot" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" -dependencies = [ - "cast", - "itertools 0.10.5", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "ctor" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d83cb7e7a873830708d6b02a78cd36a592c6fa14bf267b68725103b85c0d77f" -dependencies = [ - "link-section", - "linktime-proc-macro", -] - -[[package]] -name = "data-url" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case 0.10.0", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.117", - "unicode-xid", -] - -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "enumn" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "equator" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" -dependencies = [ - "equator-macro", -] - -[[package]] -name = "equator-macro" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "erased-serde" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "etagere" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" -dependencies = [ - "euclid", - "svg_fmt", -] - -[[package]] -name = "euclid" -version = "0.22.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" -dependencies = [ - "num-traits", -] - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "exr" -version = "1.74.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" -dependencies = [ - "bit_field", - "half", - "lebe", - "miniz_oxide", - "rayon-core", - "smallvec", - "zune-inflate", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "fax" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" -dependencies = [ - "fax_derive", -] - -[[package]] -name = "fax_derive" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" - -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - -[[package]] -name = "flate2" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" - -[[package]] -name = "float_next_after" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" - -[[package]] -name = "flume" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" -dependencies = [ - "fastrand", - "futures-core", - "futures-sink", - "spin 0.9.8", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "fontconfig-parser" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" -dependencies = [ - "roxmltree 0.20.0", -] - -[[package]] -name = "fontdb" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" -dependencies = [ - "fontconfig-parser", - "log", - "memmap2", - "slotmap", - "tinyvec", - "ttf-parser", -] - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-concurrency" -version = "7.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6" -dependencies = [ - "fixedbitset", - "futures-core", - "futures-lite", - "pin-project", - "smallvec", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", - "wasip3", -] - -[[package]] -name = "gif" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" -dependencies = [ - "color_quant", - "weezl", -] - -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "gpui-pre" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8b0f6f593153b6eb84c336ea7ccf12a1fca186f0db99af628e72f85048c1d7b" -dependencies = [ - "accesskit", - "anyhow", - "async-channel", - "async-task", - "backtrace", - "bindgen", - "bitflags 2.13.1", - "chrono", - "core-video", - "criterion", - "ctor", - "derive_more", - "etagere", - "futures", - "futures-concurrency", - "getrandom 0.3.4", - "gpui-pre-collections", - "gpui-pre-http-client", - "gpui-pre-macros", - "gpui-pre-refineable", - "gpui-pre-scheduler", - "gpui-pre-shared-string", - "gpui-pre-sum-tree", - "gpui-pre-util", - "gpui-pre-util-macros", - "gpui-pre-ztracing", - "hdrhistogram", - "heapless", - "image", - "inventory", - "itertools 0.14.0", - "log", - "lyon", - "num_cpus", - "parking", - "parking_lot", - "pin-project", - "pollster 0.4.0", - "postage", - "profiling", - "proptest", - "rand 0.9.4", - "raw-window-handle", - "regex", - "resvg", - "schemars", - "seahash", - "serde", - "serde_json", - "slotmap", - "smallvec", - "spin 0.10.0", - "strum", - "taffy", - "thiserror 2.0.17", - "tracing", - "ttf-parser", - "url", - "usvg", - "uuid", - "waker-fn", - "web-time", - "windows 0.62.2", - "zed-scap", -] - -[[package]] -name = "gpui-pre-apple" -version = "0.3.3" -dependencies = [ - "anyhow", - "block", - "cbindgen", - "cocoa 0.26.0", - "core-foundation 0.10.0", - "core-video", - "derive_more", - "etagere", - "foreign-types", - "gpui-pre", - "gpui-pre-collections", - "image", - "log", - "metal", - "objc", - "parking_lot", -] - -[[package]] -name = "gpui-pre-collections" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89c8efa2e51e368c8538a7be1ea9a12127ca03abe6cc01d9d3474e9ac4f53016" -dependencies = [ - "gpui-pre-util", - "indexmap", - "rustc-hash", -] - -[[package]] -name = "gpui-pre-derive-refineable" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a098d319acc9f84bf159944f96c5ea43a4f4cd7ed759f984acbd15719495aa0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "gpui-pre-http-client" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3495a45a28cb800c8626d2053406114bcaca26d390a4d4b183365b5bb8fdaf02" -dependencies = [ - "anyhow", - "async-compression", - "bytes", - "derive_more", - "futures", - "http", - "http-body", - "log", - "parking_lot", - "serde", - "serde_json", - "serde_urlencoded", - "url", -] - -[[package]] -name = "gpui-pre-macros" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be2db7b5097b4d523b2bce933604bcc5acdaf679bb9a150e8299e6c07efc29c" -dependencies = [ - "heck 0.5.0", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "gpui-pre-perf" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1db0c046b93c2a29120f8ee4c04bc80a4d7d6117d1164ef349faface8943491" -dependencies = [ - "gpui-pre-collections", - "serde", - "serde_json", -] - -[[package]] -name = "gpui-pre-refineable" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864e2e54a3029481dae5b6aae3ba2905f2fb1dfe66ced913b68c9ff527f8e327" -dependencies = [ - "gpui-pre-derive-refineable", -] - -[[package]] -name = "gpui-pre-scheduler" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b58a78c4e0c032900704ea49922641c534cf8bbf1bf22eda6ba00a76e88c6c3" -dependencies = [ - "async-task", - "backtrace", - "chrono", - "flume", - "futures", - "parking_lot", - "rand 0.9.4", - "web-time", -] - -[[package]] -name = "gpui-pre-shared-string" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82fc99fe88e44173758a500522d3f4059a6541da4b471af720e3f60cf34a2bc3" -dependencies = [ - "schemars", - "serde", - "smol_str", -] - -[[package]] -name = "gpui-pre-sum-tree" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "002baed852f20cef1188d3d5e0f749025fc718e4d6cea026b9077a9a6c10d042" -dependencies = [ - "gpui-pre-ztracing", - "heapless", - "log", - "rayon", - "tracing", -] - -[[package]] -name = "gpui-pre-util" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fe779c4cb00929aafcb2b307cd1240588aebb5d1eb328c59b80f77c05a41fad" -dependencies = [ - "anyhow", - "log", - "which", -] - -[[package]] -name = "gpui-pre-util-macros" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f0ccc4bccb6a31786095d15fc9f40d6a4c6a295522e3460331dd7e929ff2f79" -dependencies = [ - "gpui-pre-perf", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "gpui-pre-zlog" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1416ea5f018e3a8a1c8be266332c443583f28f8f87379a84ab1e77622173ac0" -dependencies = [ - "anyhow", - "chrono", - "gpui-pre-collections", - "log", -] - -[[package]] -name = "gpui-pre-ztracing" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cc64a25a3cc4e4f1d8acb00074d3925339dae657c020083735738ba8af96bf7" -dependencies = [ - "gpui-pre-zlog", - "gpui-pre-ztracing-macro", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "gpui-pre-ztracing-macro" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f3ea75672348f37d94472e579f5979ee19b744d0fa5aaadc2fe129ccafa80b" - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - -[[package]] -name = "hash32" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" -dependencies = [ - "byteorder", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "hdrhistogram" -version = "7.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" -dependencies = [ - "byteorder", - "num-traits", -] - -[[package]] -name = "heapless" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" -dependencies = [ - "hash32", - "stable_deref_trait", -] - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "http" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" - -[[package]] -name = "icu_properties" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" - -[[package]] -name = "icu_provider" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "image" -version = "0.25.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" -dependencies = [ - "bytemuck", - "byteorder-lite", - "color_quant", - "exr", - "gif", - "image-webp", - "moxcms", - "num-traits", - "png 0.18.0", - "qoi", - "ravif", - "rayon", - "tiff", - "zune-core", - "zune-jpeg", -] - -[[package]] -name = "image-webp" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" -dependencies = [ - "byteorder-lite", - "quick-error 2.0.1", -] - -[[package]] -name = "imagesize" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" - -[[package]] -name = "imgref" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[package]] -name = "interpolate_name" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "inventory" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" -dependencies = [ - "rustversion", -] - -[[package]] -name = "io-surface" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "554b8c5d64ec09a3a520fe58e4d48a73e00ff32899cdcbe32a4877afd4968b8e" -dependencies = [ - "cgl", - "core-foundation 0.10.0", - "core-foundation-sys", - "leaky-cow", -] - -[[package]] -name = "is-terminal" -version = "0.4.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" -dependencies = [ - "cfg-if", - "futures-util", - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "kurbo" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" -dependencies = [ - "arrayvec", - "euclid", - "polycool", - "smallvec", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leak" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd100e01f1154f2908dfa7d02219aeab25d0b9c7fa955164192e3245255a0c73" - -[[package]] -name = "leaky-cow" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a8225d44241fd324a8af2806ba635fc7c8a7e9a7de4d5cf3ef54e71f5926fc" -dependencies = [ - "leak", -] - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "lebe" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" - -[[package]] -name = "libbz2-rs-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "libfuzzer-sys" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" -dependencies = [ - "arbitrary", - "cc", -] - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link 0.2.1", -] - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "link-section" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee1a0d6e252afe82e7bc2db42fba60e02ddf3b1accaf8cb21d96e34ba61f3d4" - -[[package]] -name = "linktime-proc-macro" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "348d0075b1fc163b26d72a7f75fc5141daf2fd1bdf128d873cbaf6785d495bdf" - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litemap" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -dependencies = [ - "serde_core", - "value-bag", -] - -[[package]] -name = "loop9" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" -dependencies = [ - "imgref", -] - -[[package]] -name = "lyon" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbcb7d54d54c8937364c9d41902d066656817dce1e03a44e5533afebd1ef4352" -dependencies = [ - "lyon_algorithms", - "lyon_tessellation", -] - -[[package]] -name = "lyon_algorithms" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c0829e28c4f336396f250d850c3987e16ce6db057ffe047ce0dd54aab6b647" -dependencies = [ - "lyon_path", - "num-traits", -] - -[[package]] -name = "lyon_geom" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e16770d760c7848b0c1c2d209101e408207a65168109509f8483837a36cf2e7" -dependencies = [ - "arrayvec", - "euclid", - "num-traits", -] - -[[package]] -name = "lyon_path" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aeca86bcfd632a15984ba029b539ffb811e0a70bf55e814ef8b0f54f506fdeb" -dependencies = [ - "lyon_geom", - "num-traits", -] - -[[package]] -name = "lyon_tessellation" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f586142e1280335b1bc89539f7c97dd80f08fc43e9ab1b74ef0a42b04aa353" -dependencies = [ - "float_next_after", - "lyon_path", - "num-traits", -] - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - -[[package]] -name = "maybe-rayon" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" -dependencies = [ - "cfg-if", - "rayon", -] - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "memmap2" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" -dependencies = [ - "libc", -] - -[[package]] -name = "metal" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15" -dependencies = [ - "bitflags 2.13.1", - "block", - "core-graphics-types 0.2.0", - "foreign-types", - "log", - "objc", - "paste", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "moxcms" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" -dependencies = [ - "num-traits", - "pxfm", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "noop_proc_macro" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" - -[[package]] -name = "ntapi" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" -dependencies = [ - "winapi", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num-bigint" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", - "objc_exception", -] - -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - -[[package]] -name = "objc_exception" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" -dependencies = [ - "cc", -] - -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link 0.2.1", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pastey" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pico-args" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" - -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "plotters" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" - -[[package]] -name = "plotters-svg" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" -dependencies = [ - "plotters-backend", -] - -[[package]] -name = "png" -version = "0.17.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "png" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" -dependencies = [ - "bitflags 2.13.1", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "pollster" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" - -[[package]] -name = "pollster" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" - -[[package]] -name = "polycool" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "postage" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" -dependencies = [ - "atomic", - "crossbeam-queue", - "futures", - "log", - "parking_lot", - "pin-project", - "pollster 0.2.5", - "static_assertions", - "thiserror 1.0.69", -] - -[[package]] -name = "potential_utf" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" -dependencies = [ - "zerovec", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - -[[package]] -name = "proc-macro-crate" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" -dependencies = [ - "toml_edit 0.23.7", -] - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "profiling" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" -dependencies = [ - "profiling-procmacros", -] - -[[package]] -name = "profiling-procmacros" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" -dependencies = [ - "quote", - "syn 2.0.117", -] - -[[package]] -name = "proptest" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" -dependencies = [ - "bit-set", - "bit-vec", - "bitflags 2.13.1", - "num-traits", - "proptest-macro", - "rand 0.9.4", - "rand_chacha 0.9.0", - "rand_xorshift", - "regex-syntax", - "rusty-fork", - "tempfile", - "unarray", -] - -[[package]] -name = "proptest-macro" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efaa288b896cb2b345da7b7f2110ab19e51565b83495b56fcec98a62f8b1f33e" -dependencies = [ - "convert_case 0.11.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pxfm" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" -dependencies = [ - "num-traits", -] - -[[package]] -name = "qoi" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - -[[package]] -name = "quick-xml" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rand_xorshift" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" -dependencies = [ - "rand_core 0.9.3", -] - -[[package]] -name = "rav1e" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" -dependencies = [ - "aligned-vec", - "arbitrary", - "arg_enum_proc_macro", - "arrayvec", - "av-scenechange", - "av1-grain", - "bitstream-io", - "built", - "cfg-if", - "interpolate_name", - "itertools 0.14.0", - "libc", - "libfuzzer-sys", - "log", - "maybe-rayon", - "new_debug_unreachable", - "noop_proc_macro", - "num-derive", - "num-traits", - "paste", - "profiling", - "rand 0.9.4", - "rand_chacha 0.9.0", - "simd_helpers", - "thiserror 2.0.17", - "v_frame", - "wasm-bindgen", -] - -[[package]] -name = "ravif" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" -dependencies = [ - "avif-serialize", - "imgref", - "loop9", - "quick-error 2.0.1", - "rav1e", - "rayon", - "rgb", -] - -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - -[[package]] -name = "rayon" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.13.1", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "resvg" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b563218631706d614e23059436526d005b50ab5f2d506b55a17eb65c5eb83419" -dependencies = [ - "gif", - "image-webp", - "log", - "pico-args", - "rgb", - "svgtypes", - "tiny-skia", - "usvg", - "zune-jpeg", -] - -[[package]] -name = "rgb" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "roxmltree" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" - -[[package]] -name = "roxmltree" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" -dependencies = [ - "memchr", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.13.1", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "rusty-fork" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" -dependencies = [ - "fnv", - "quick-error 1.2.3", - "tempfile", - "wait-timeout", -] - -[[package]] -name = "rustybuzz" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" -dependencies = [ - "bitflags 2.13.1", - "bytemuck", - "core_maths", - "log", - "smallvec", - "ttf-parser", - "unicode-bidi-mirroring", - "unicode-ccc", - "unicode-properties", - "unicode-script", -] - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schemars" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" -dependencies = [ - "dyn-clone", - "indexmap", - "ref-cast", - "schemars_derive", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.117", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "screencapturekit" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5eeeb57ac94960cfe5ff4c402be6585ae4c8d29a2cf41b276048c2e849d64e" -dependencies = [ - "screencapturekit-sys", -] - -[[package]] -name = "screencapturekit-sys" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22411b57f7d49e7fe08025198813ee6fd65e1ee5eff4ebc7880c12c82bde4c60" -dependencies = [ - "block", - "dispatch", - "objc", - "objc-foundation", - "objc_id", - "once_cell", -] - -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_fmt" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_json" -version = "1.0.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "indexmap", - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "simd_helpers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" -dependencies = [ - "quote", -] - -[[package]] -name = "simplecss" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" -dependencies = [ - "log", -] - -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "slotmap" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" -dependencies = [ - "version_check", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "smol_str" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" -dependencies = [ - "borsh", - "serde_core", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spin" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" -dependencies = [ - "lock_api", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strict-num" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" -dependencies = [ - "float-cmp", -] - -[[package]] -name = "strum" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "sval" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d94c4464e595f0284970fd9c7e9013804d035d4a61ab74b113242c874c05814d" - -[[package]] -name = "sval_buffer" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0f46e34b20a39e6a2bf02b926983149b3af6609fd1ee8a6e63f6f340f3e2164" -dependencies = [ - "sval", - "sval_ref", -] - -[[package]] -name = "sval_dynamic" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d0970e53c92ab5381d3b2db1828da8af945954d4234225f6dd9c3afbcef3f5" -dependencies = [ - "sval", -] - -[[package]] -name = "sval_fmt" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43e5e6e1613e1e7fc2e1a9fdd709622e54c122ceb067a60d170d75efd491a839" -dependencies = [ - "itoa", - "ryu", - "sval", -] - -[[package]] -name = "sval_json" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aec382f7bfa6e367b23c9611f129b94eb7daaf3d8fae45a8d0a0211eb4d4c8e6" -dependencies = [ - "itoa", - "ryu", - "sval", -] - -[[package]] -name = "sval_nested" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3049d0f99ce6297f8f7d9953b35a0103b7584d8f638de40e64edb7105fa578ae" -dependencies = [ - "sval", - "sval_buffer", - "sval_ref", -] - -[[package]] -name = "sval_ref" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f88913e77506085c0a8bf6912bb6558591a960faf5317df6c1d9b227224ca6e1" -dependencies = [ - "sval", -] - -[[package]] -name = "sval_serde" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f579fd7254f4be6cd7b450034f856b78523404655848789c451bacc6aa8b387d" -dependencies = [ - "serde_core", - "sval", - "sval_nested", -] - -[[package]] -name = "svg_fmt" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" - -[[package]] -name = "svgtypes" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d" -dependencies = [ - "kurbo", - "siphasher", -] - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "sysinfo" -version = "0.31.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" -dependencies = [ - "core-foundation-sys", - "libc", - "memchr", - "ntapi", - "rayon", - "windows 0.57.0", -] - -[[package]] -name = "taffy" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c034e05f6ee85a12daa63863c2245797715075c70649947aa0da54f3f2ab1d0f" -dependencies = [ - "arrayvec", - "serde", - "slotmap", - "smallvec", -] - -[[package]] -name = "tao-core-video-sys" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271450eb289cb4d8d0720c6ce70c72c8c858c93dd61fc625881616752e6b98f6" -dependencies = [ - "cfg-if", - "core-foundation-sys", - "libc", - "objc", -] - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.1", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" -dependencies = [ - "thiserror-impl 2.0.17", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "tiff" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" -dependencies = [ - "fax", - "flate2", - "half", - "quick-error 2.0.1", - "weezl", - "zune-jpeg", -] - -[[package]] -name = "tiny-skia" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" -dependencies = [ - "arrayref", - "arrayvec", - "bytemuck", - "cfg-if", - "log", - "png 0.17.16", - "tiny-skia-path", -] - -[[package]] -name = "tiny-skia-path" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" -dependencies = [ - "arrayref", - "bytemuck", - "strict-num", -] - -[[package]] -name = "tinystr" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime 0.6.11", - "toml_edit 0.22.27", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime 0.6.11", - "toml_write", - "winnow", -] - -[[package]] -name = "toml_edit" -version = "0.23.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" -dependencies = [ - "indexmap", - "toml_datetime 0.7.3", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - -[[package]] -name = "tracing" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tracing-core" -version = "0.1.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "ttf-parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" -dependencies = [ - "core_maths", -] - -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - -[[package]] -name = "unarray" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" - -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - -[[package]] -name = "unicode-bidi-mirroring" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" - -[[package]] -name = "unicode-ccc" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-properties" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" - -[[package]] -name = "unicode-script" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" - -[[package]] -name = "unicode-segmentation" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" - -[[package]] -name = "unicode-vo" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "usvg" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e419dff010bb12512b0ae9e3d2f318dfbdf0167fde7eb05465134d4e8756076f" -dependencies = [ - "base64", - "data-url", - "flate2", - "fontdb", - "imagesize", - "kurbo", - "log", - "pico-args", - "roxmltree 0.21.1", - "rustybuzz", - "simplecss", - "siphasher", - "strict-num", - "svgtypes", - "tiny-skia-path", - "unicode-bidi", - "unicode-script", - "unicode-vo", - "xmlwriter", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" -dependencies = [ - "getrandom 0.3.4", - "js-sys", - "serde", - "sha1_smol", - "wasm-bindgen", -] - -[[package]] -name = "v_frame" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" -dependencies = [ - "aligned-vec", - "num-traits", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "value-bag" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" -dependencies = [ - "value-bag-serde1", - "value-bag-sval2", -] - -[[package]] -name = "value-bag-serde1" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16530907bfe2999a1773ca5900a65101e092c70f642f25cc23ca0c43573262c5" -dependencies = [ - "erased-serde", - "serde_core", - "serde_fmt", -] - -[[package]] -name = "value-bag-sval2" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d00ae130edd690eaa877e4f40605d534790d1cf1d651e7685bd6a144521b251f" -dependencies = [ - "sval", - "sval_buffer", - "sval_dynamic", - "sval_fmt", - "sval_json", - "sval_ref", - "sval_serde", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wait-timeout" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" -dependencies = [ - "libc", -] - -[[package]] -name = "waker-fn" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen 0.46.0", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.117", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.13.1", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "web-sys" -version = "0.3.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "weezl" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" - -[[package]] -name = "which" -version = "8.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3ef584124b911bcc3875c2f1472e80f24361ceb789bd1c62b3e9a3df9ff43c" -dependencies = [ - "libc", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" -dependencies = [ - "windows-core 0.57.0", - "windows-targets", -] - -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections 0.2.0", - "windows-core 0.61.2", - "windows-future 0.2.1", - "windows-link 0.1.3", - "windows-numerics 0.2.0", -] - -[[package]] -name = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections 0.3.2", - "windows-core 0.62.2", - "windows-future 0.3.2", - "windows-numerics 0.3.1", -] - -[[package]] -name = "windows-capture" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a4df73e95feddb9ec1a7e9c2ca6323b8c97d5eeeff78d28f1eccdf19c882b24" -dependencies = [ - "parking_lot", - "rayon", - "thiserror 2.0.17", - "windows 0.61.3", - "windows-future 0.2.1", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core 0.61.2", -] - -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core 0.62.2", -] - -[[package]] -name = "windows-core" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" -dependencies = [ - "windows-implement 0.57.0", - "windows-interface 0.57.0", - "windows-result 0.1.2", - "windows-targets", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading 0.1.0", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core 0.62.2", - "windows-link 0.2.1", - "windows-threading 0.2.1", -] - -[[package]] -name = "windows-implement" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-interface" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", -] - -[[package]] -name = "windows-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core 0.62.2", - "windows-link 0.2.1", -] - -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winnow" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck 0.5.0", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.13.1", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "writeable" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" - -[[package]] -name = "x11" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "xcb" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f07c123b796139bfe0603e654eaf08e132e52387ba95b252c78bad3640ba37ea" -dependencies = [ - "bitflags 1.3.2", - "libc", - "quick-xml", - "x11", -] - -[[package]] -name = "xmlwriter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" - -[[package]] -name = "y4m" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" - -[[package]] -name = "yoke" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zed-scap" -version = "0.0.8-zed" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b338d705ae33a43ca00287c11129303a7a0aa57b101b72a1c08c863f698ac8" -dependencies = [ - "anyhow", - "cocoa 0.25.0", - "core-graphics-helmer-fork", - "log", - "objc", - "rand 0.8.6", - "screencapturekit", - "screencapturekit-sys", - "sysinfo", - "tao-core-video-sys", - "windows 0.61.3", - "windows-capture", - "x11", - "xcb", -] - -[[package]] -name = "zerocopy" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f212a141d820099d57ffafb9569be9617a6f27d3dc881fbee8fb56642f917a9" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" - -[[package]] -name = "zune-core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" - -[[package]] -name = "zune-inflate" -version = "0.2.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "zune-jpeg" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" -dependencies = [ - "zune-core", -] diff --git a/crates/gpui_pre_apple/Cargo.toml b/crates/gpui_pre_apple/Cargo.toml deleted file mode 100644 index 1ff1738..0000000 --- a/crates/gpui_pre_apple/Cargo.toml +++ /dev/null @@ -1,152 +0,0 @@ -# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO -# -# When uploading crates to the registry Cargo will automatically -# "normalize" Cargo.toml files for maximal compatibility -# with all versions of Cargo and also rewrite `path` dependencies -# to registry (e.g., crates.io) dependencies. -# -# If you are reading this file be aware that the original Cargo.toml -# will likely look very different (and much more reasonable). -# See Cargo.toml.orig for the original contents. - -[package] -edition = "2024" -name = "gpui-pre-apple" -version = "0.3.3" -build = "build.rs" -publish = true -autolib = false -autobins = false -autoexamples = false -autotests = false -autobenches = false -description = "Zed's `gpui_apple` crate (gpui-pre snapshot of zed@5b055fa)" -readme = false -license = "Apache-2.0" -repository = "https://github.com/zed-industries/zed" -resolver = "2" - -[package.metadata.gpui-pre] -zed-crate = "gpui_apple" -zed-version = "0.1.0" -zed-rev = "5b055fa789a8b8d38ac951a6e0cde272f66b4495" - -[features] -bench-support = ["gpui/bench-support"] -default = [] -runtime_shaders = [] -test-support = ["gpui/test-support"] - -[lib] -name = "gpui_apple" -path = "src/gpui_apple.rs" - -[dependencies.gpui] -version = "=0.3.3" -default-features = false -package = "gpui-pre" - -[target.'cfg(target_os = "macos")'.dependencies.anyhow] -version = "1.0.86" - -[target.'cfg(target_os = "macos")'.dependencies.block] -version = "0.1" - -[target.'cfg(target_os = "macos")'.dependencies.cocoa] -version = "=0.26.0" - -[target.'cfg(target_os = "macos")'.dependencies.collections] -version = "=0.3.3" -package = "gpui-pre-collections" - -[target.'cfg(target_os = "macos")'.dependencies.core-foundation] -version = "0.10" - -[target.'cfg(target_os = "macos")'.dependencies.core-video] -version = "0.5.2" -features = ["metal"] - -[target.'cfg(target_os = "macos")'.dependencies.derive_more] -version = "2.1.1" -features = [ - "add", - "add_assign", - "deref", - "deref_mut", - "display", - "from", - "from_str", - "mul", - "mul_assign", - "not", -] - -[target.'cfg(target_os = "macos")'.dependencies.etagere] -version = "0.2" - -[target.'cfg(target_os = "macos")'.dependencies.foreign-types] -version = "0.5" - -[target.'cfg(target_os = "macos")'.dependencies.image] -version = "0.25.1" -features = [ - "bmp", - "dds", - "exr", - "ff", - "gif", - "hdr", - "ico", - "jpeg", - "png", - "pnm", - "qoi", - "rayon", - "tga", - "tiff", - "webp", -] -default-features = false - -[target.'cfg(target_os = "macos")'.dependencies.log] -version = "0.4.16" -features = [ - "kv_unstable_serde", - "serde", -] - -[target.'cfg(target_os = "macos")'.dependencies.metal] -version = "0.33" - -[target.'cfg(target_os = "macos")'.dependencies.objc] -version = "0.2" - -[target.'cfg(target_os = "macos")'.dependencies.parking_lot] -version = "0.12.1" - -[target.'cfg(target_os = "macos")'.build-dependencies.cbindgen] -version = "0.28.0" -default-features = false - -[lints.clippy] -dbg_macro = "deny" -declare_interior_mutable_const = "deny" -disallowed_methods = "deny" -large_enum_variant = "allow" -let_underscore_future = "allow" -nonminimal_bool = "allow" -redundant_clone = "deny" -single_range_in_vec_init = "allow" -todo = "deny" -too_many_arguments = "allow" -type_complexity = "allow" - -[lints.clippy.style] -level = "allow" -priority = -1 - -[lints.rust.unexpected_cfgs] -level = "allow" -priority = 0 - -[workspace] diff --git a/crates/gpui_pre_apple/LICENSE-APACHE b/crates/gpui_pre_apple/LICENSE-APACHE deleted file mode 100644 index 461a0fe..0000000 --- a/crates/gpui_pre_apple/LICENSE-APACHE +++ /dev/null @@ -1,222 +0,0 @@ -Copyright 2022 - 2025 Zed Industries, Inc. - - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - - http://www.apache.org/licenses/LICENSE-2.0 - - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - - - -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 diff --git a/crates/gpui_pre_apple/build.rs b/crates/gpui_pre_apple/build.rs deleted file mode 100644 index c8f0db8..0000000 --- a/crates/gpui_pre_apple/build.rs +++ /dev/null @@ -1,190 +0,0 @@ -// Modified for gpui-pre (snapshot of zed@5b055fa): the gpui sources it reads are vendored under `vendor/gpui`. -#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")] - -fn main() { - #[cfg(target_os = "macos")] - macos_build::run(); -} - -#[cfg(target_os = "macos")] -mod macos_build { - use std::{ - env, - path::{Path, PathBuf}, - }; - - use cbindgen::Config; - - pub fn run() { - let header_path = generate_shader_bindings(); - - #[cfg(feature = "runtime_shaders")] - emit_stitched_shaders(&header_path); - #[cfg(not(feature = "runtime_shaders"))] - compile_metal_shaders(&header_path); - } - - fn generate_shader_bindings() -> PathBuf { - let output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("scene.h"); - - let gpui_dir = find_gpui_crate_dir(); - - let mut config = Config { - include_guard: Some("SCENE_H".into()), - language: cbindgen::Language::C, - no_includes: true, - ..Default::default() - }; - config.export.include.extend([ - "Bounds".into(), - "Corners".into(), - "Edges".into(), - "Size".into(), - "Pixels".into(), - "PointF".into(), - "Hsla".into(), - "ContentMask".into(), - "RoundedClip_ScaledPixels".into(), - "Uniforms".into(), - "AtlasTile".into(), - "PathRasterizationInputIndex".into(), - "PathVertex_ScaledPixels".into(), - "PathRasterizationVertex".into(), - "ShadowInputIndex".into(), - "Shadow".into(), - "QuadInputIndex".into(), - "Underline".into(), - "UnderlineInputIndex".into(), - "Quad".into(), - "BorderStyle".into(), - "SpriteInputIndex".into(), - "MonochromeSprite".into(), - "PolychromeSprite".into(), - "PathSprite".into(), - "SurfaceInputIndex".into(), - "SurfaceBounds".into(), - "TransformationMatrix".into(), - ]); - config.no_includes = true; - config.enumeration.prefix_with_name = true; - - let mut builder = cbindgen::Builder::new(); - - let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); - - // Source files from gpui that define types used in shaders - let gpui_src_paths = [ - gpui_dir.join("src/scene.rs"), - gpui_dir.join("src/clip.rs"), - gpui_dir.join("src/geometry.rs"), - gpui_dir.join("src/color.rs"), - gpui_dir.join("src/window.rs"), - gpui_dir.join("src/platform.rs"), - ]; - - // Source files from this crate - let local_src_paths = [crate_dir.join("src/metal_renderer.rs")]; - - for src_path in gpui_src_paths.iter().chain(local_src_paths.iter()) { - println!("cargo:rerun-if-changed={}", src_path.display()); - builder = builder.with_src(src_path); - } - - builder - .with_config(config) - .generate() - .expect("Unable to generate bindings") - .write_to_file(&output_path); - - output_path - } - - /// Locate the gpui crate directory relative to this crate. Resolved at - /// build-script runtime against this crate's manifest dir, so no checkout - /// path is baked into a compiled artifact (which corgi rejects). - fn find_gpui_crate_dir() -> PathBuf { - PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("vendor/gpui") - } - - /// To enable runtime compilation, we need to "stitch" the shaders file with the generated header - /// so that it is self-contained. - #[cfg(feature = "runtime_shaders")] - fn emit_stitched_shaders(header_path: &Path) { - fn stitch_header(header: &Path, shader_path: &Path) -> std::io::Result { - let header_contents = std::fs::read_to_string(header)?; - let shader_contents = std::fs::read_to_string(shader_path)?; - let stitched_contents = format!("{header_contents}\n{shader_contents}"); - let out_path = - PathBuf::from(env::var("OUT_DIR").unwrap()).join("stitched_shaders.metal"); - std::fs::write(&out_path, stitched_contents)?; - Ok(out_path) - } - let shader_source_path = "./src/shaders.metal"; - let shader_path = PathBuf::from(shader_source_path); - stitch_header(header_path, &shader_path).unwrap(); - println!("cargo:rerun-if-changed={shader_source_path}"); - } - - #[cfg(not(feature = "runtime_shaders"))] - fn compile_metal_shaders(header_path: &Path) { - use std::process::{self, Command}; - let shader_path = "./src/shaders.metal"; - let air_output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.air"); - let metallib_output_path = - PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.metallib"); - println!("cargo:rerun-if-changed={}", shader_path); - - // The metal compiler records the resolved absolute path of its input - // unconditionally. Compile a copy staged in OUT_DIR so the recorded - // location is the build's canonical output directory, never the - // checkout (corgi rejects artifacts that embed the build path). - let staged_shader_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.metal"); - std::fs::copy(shader_path, &staged_shader_path).unwrap(); - - let output = Command::new("xcrun") - .args([ - "-sdk", - "macosx", - "metal", - "-gline-tables-only", - "-mmacosx-version-min=10.15.7", - "-MO", - "-c", - ]) - .arg(&staged_shader_path) - .args(["-include", header_path.to_str().unwrap(), "-o"]) - .arg(&air_output_path) - .output() - .unwrap(); - - if !output.status.success() { - println!( - "cargo::error=metal shader compilation failed:\n{}", - String::from_utf8_lossy(&output.stderr) - ); - process::exit(1); - } - - let output = Command::new("xcrun") - .args(["-sdk", "macosx", "metallib"]) - .arg(&air_output_path) - .arg("-o") - .arg(metallib_output_path) - .output() - .unwrap(); - - if !output.status.success() { - println!( - "cargo::error=metallib compilation failed:\n{}", - String::from_utf8_lossy(&output.stderr) - ); - process::exit(1); - } - - // The .air intermediate records the compiler's working directory in - // its debug info; the metallib built from it does not. Nothing reads - // the .air after this point, so drop it rather than leave a - // checkout-path-bearing file in OUT_DIR. - std::fs::remove_file(&air_output_path).unwrap(); - } -} diff --git a/crates/gpui_pre_apple/src/gpui_apple.rs b/crates/gpui_pre_apple/src/gpui_apple.rs deleted file mode 100644 index 208d297..0000000 --- a/crates/gpui_pre_apple/src/gpui_apple.rs +++ /dev/null @@ -1,8 +0,0 @@ -#![cfg(target_os = "macos")] -//! Shared Apple platform support for GPUI. -//! -//! This crate contains the Metal renderer and GPU resource management shared -//! by GPUI's Apple platform backends. - -mod metal_atlas; -pub mod metal_renderer; diff --git a/crates/gpui_pre_apple/src/metal_atlas.rs b/crates/gpui_pre_apple/src/metal_atlas.rs deleted file mode 100644 index 9cfd445..0000000 --- a/crates/gpui_pre_apple/src/metal_atlas.rs +++ /dev/null @@ -1,379 +0,0 @@ -use anyhow::{Context as _, Result}; -use collections::FxHashMap; -use derive_more::{Deref, DerefMut}; -use etagere::BucketedAtlasAllocator; -use gpui::{ - AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTextureList, AtlasTile, Bounds, DevicePixels, - PlatformAtlas, Point, Size, -}; -use metal::Device; -use parking_lot::Mutex; -use std::borrow::Cow; - -pub struct MetalAtlas(Mutex); - -impl MetalAtlas { - pub(crate) fn new(device: Device, is_apple_gpu: bool) -> Self { - MetalAtlas(Mutex::new(MetalAtlasState { - device: AssertSend(device), - is_apple_gpu, - monochrome_textures: Default::default(), - polychrome_textures: Default::default(), - tiles_by_key: Default::default(), - })) - } - - pub(crate) fn metal_texture(&self, id: AtlasTextureId) -> metal::Texture { - self.0.lock().texture(id).metal_texture.clone() - } -} - -struct MetalAtlasState { - device: AssertSend, - is_apple_gpu: bool, - monochrome_textures: AtlasTextureList, - polychrome_textures: AtlasTextureList, - tiles_by_key: FxHashMap, -} - -impl PlatformAtlas for MetalAtlas { - fn get_or_insert_with<'a>( - &self, - key: &AtlasKey, - build: &mut dyn FnMut() -> Result, Cow<'a, [u8]>)>>, - ) -> Result> { - let mut lock = self.0.lock(); - if let Some(tile) = lock.tiles_by_key.get(key) { - Ok(Some(*tile)) - } else { - let Some((size, bytes)) = build()? else { - return Ok(None); - }; - let tile = lock - .allocate(size, key.texture_kind()) - .context("failed to allocate")?; - let texture = lock.texture(tile.texture_id); - texture.upload(tile.bounds, &bytes); - lock.tiles_by_key.insert(key.clone(), tile); - Ok(Some(tile)) - } - } - - fn remove(&self, key: &AtlasKey) { - let mut lock = self.0.lock(); - let Some(tile) = lock.tiles_by_key.remove(key) else { - return; - }; - let id = tile.texture_id; - - let textures = match id.kind { - AtlasTextureKind::Monochrome => &mut lock.monochrome_textures, - AtlasTextureKind::Polychrome => &mut lock.polychrome_textures, - AtlasTextureKind::Subpixel => unreachable!(), - }; - - let Some(texture_slot) = textures - .textures - .iter_mut() - .find(|texture| texture.as_ref().is_some_and(|v| v.id == id)) - else { - return; - }; - - if let Some(mut texture) = texture_slot.take() { - texture.allocator.deallocate(tile.tile_id.into()); - texture.decrement_ref_count(); - if texture.is_unreferenced() { - textures.free_list.push(id.index as usize); - } else { - *texture_slot = Some(texture); - } - } - } -} - -impl MetalAtlasState { - fn allocate( - &mut self, - size: Size, - texture_kind: AtlasTextureKind, - ) -> Option { - { - let textures = match texture_kind { - AtlasTextureKind::Monochrome => &mut self.monochrome_textures, - AtlasTextureKind::Polychrome => &mut self.polychrome_textures, - AtlasTextureKind::Subpixel => unreachable!(), - }; - - if let Some(tile) = textures - .iter_mut() - .rev() - .find_map(|texture| texture.allocate(size)) - { - return Some(tile); - } - } - - let texture = self.push_texture(size, texture_kind); - texture.allocate(size) - } - - fn push_texture( - &mut self, - min_size: Size, - kind: AtlasTextureKind, - ) -> &mut MetalAtlasTexture { - const DEFAULT_ATLAS_SIZE: Size = Size { - width: DevicePixels(1024), - height: DevicePixels(1024), - }; - // Max texture size on all modern Apple GPUs. Anything bigger than that crashes in validateWithDevice. - const MAX_ATLAS_SIZE: Size = Size { - width: DevicePixels(16384), - height: DevicePixels(16384), - }; - let size = min_size.min(&MAX_ATLAS_SIZE).max(&DEFAULT_ATLAS_SIZE); - let texture_descriptor = metal::TextureDescriptor::new(); - texture_descriptor.set_width(size.width.into()); - texture_descriptor.set_height(size.height.into()); - let pixel_format; - let usage; - match kind { - AtlasTextureKind::Monochrome => { - pixel_format = metal::MTLPixelFormat::A8Unorm; - usage = metal::MTLTextureUsage::ShaderRead; - } - AtlasTextureKind::Polychrome => { - pixel_format = metal::MTLPixelFormat::BGRA8Unorm; - usage = metal::MTLTextureUsage::ShaderRead; - } - AtlasTextureKind::Subpixel => unreachable!(), - } - texture_descriptor.set_pixel_format(pixel_format); - texture_descriptor.set_usage(usage); - // Shared memory mode can be used only on Apple GPU families - // https://developer.apple.com/documentation/metal/mtlresourceoptions/storagemodeshared - texture_descriptor.set_storage_mode(if self.is_apple_gpu { - metal::MTLStorageMode::Shared - } else { - metal::MTLStorageMode::Managed - }); - let metal_texture = self.device.new_texture(&texture_descriptor); - - let texture_list = match kind { - AtlasTextureKind::Monochrome => &mut self.monochrome_textures, - AtlasTextureKind::Polychrome => &mut self.polychrome_textures, - AtlasTextureKind::Subpixel => unreachable!(), - }; - - let index = texture_list.free_list.pop(); - - let atlas_texture = MetalAtlasTexture { - id: AtlasTextureId { - index: index.unwrap_or(texture_list.textures.len()) as u32, - kind, - }, - allocator: etagere::BucketedAtlasAllocator::new(size_to_etagere(size)), - metal_texture: AssertSend(metal_texture), - live_atlas_keys: 0, - }; - - if let Some(ix) = index { - texture_list.textures[ix] = Some(atlas_texture); - texture_list.textures.get_mut(ix) - } else { - texture_list.textures.push(Some(atlas_texture)); - texture_list.textures.last_mut() - } - .unwrap() - .as_mut() - .unwrap() - } - - fn texture(&self, id: AtlasTextureId) -> &MetalAtlasTexture { - let textures = match id.kind { - AtlasTextureKind::Monochrome => &self.monochrome_textures, - AtlasTextureKind::Polychrome => &self.polychrome_textures, - AtlasTextureKind::Subpixel => unreachable!(), - }; - textures[id.index as usize].as_ref().unwrap() - } -} - -struct MetalAtlasTexture { - id: AtlasTextureId, - allocator: BucketedAtlasAllocator, - metal_texture: AssertSend, - live_atlas_keys: u32, -} - -impl MetalAtlasTexture { - fn allocate(&mut self, size: Size) -> Option { - let allocation = self.allocator.allocate(size_to_etagere(size))?; - let tile = AtlasTile { - texture_id: self.id, - tile_id: allocation.id.into(), - bounds: Bounds { - origin: point_from_etagere(allocation.rectangle.min), - size, - }, - padding: 0, - }; - self.live_atlas_keys += 1; - Some(tile) - } - - fn upload(&self, bounds: Bounds, bytes: &[u8]) { - let region = metal::MTLRegion::new_2d( - bounds.origin.x.into(), - bounds.origin.y.into(), - bounds.size.width.into(), - bounds.size.height.into(), - ); - self.metal_texture.replace_region( - region, - 0, - bytes.as_ptr() as *const _, - bounds.size.width.to_bytes(self.bytes_per_pixel()) as u64, - ); - } - - fn bytes_per_pixel(&self) -> u8 { - use metal::MTLPixelFormat::*; - match self.metal_texture.pixel_format() { - A8Unorm | R8Unorm => 1, - RGBA8Unorm | BGRA8Unorm => 4, - _ => unimplemented!(), - } - } - - fn decrement_ref_count(&mut self) { - self.live_atlas_keys -= 1; - } - - fn is_unreferenced(&mut self) -> bool { - self.live_atlas_keys == 0 - } -} - -fn size_to_etagere(size: Size) -> etagere::Size { - etagere::Size::new(size.width.into(), size.height.into()) -} - -fn point_from_etagere(value: etagere::Point) -> Point { - Point { - x: DevicePixels::from(value.x), - y: DevicePixels::from(value.y), - } -} - -#[derive(Deref, DerefMut)] -struct AssertSend(T); - -unsafe impl Send for AssertSend {} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::PlatformAtlas; - use std::borrow::Cow; - - fn create_atlas() -> Option { - let device = metal::Device::system_default()?; - Some(MetalAtlas::new(device, true)) - } - - fn make_image_key(image_id: usize, frame_index: usize) -> AtlasKey { - AtlasKey::Image(gpui::RenderImageParams { - image_id: gpui::ImageId(image_id), - frame_index, - }) - } - - fn insert_tile(atlas: &MetalAtlas, key: &AtlasKey, size: Size) -> AtlasTile { - atlas - .get_or_insert_with(key, &mut || { - let byte_count = (size.width.0 as usize) * (size.height.0 as usize) * 4; - Ok(Some((size, Cow::Owned(vec![0u8; byte_count])))) - }) - .expect("allocation should succeed") - .expect("callback returns Some") - } - - #[test] - fn test_remove_clears_stale_keys_from_tiles_by_key() { - let Some(atlas) = create_atlas() else { - return; - }; - - let small = Size { - width: DevicePixels(64), - height: DevicePixels(64), - }; - - let key_a = make_image_key(1, 0); - let key_b = make_image_key(2, 0); - let key_c = make_image_key(3, 0); - - let tile_a = insert_tile(&atlas, &key_a, small); - let tile_b = insert_tile(&atlas, &key_b, small); - let tile_c = insert_tile(&atlas, &key_c, small); - - assert_eq!(tile_a.texture_id, tile_b.texture_id); - assert_eq!(tile_b.texture_id, tile_c.texture_id); - - // Remove A: texture still has B and C, so it stays. - // The key for A must be removed from tiles_by_key. - atlas.remove(&key_a); - - // Remove B: texture still has C. - atlas.remove(&key_b); - - // Remove C: texture becomes unreferenced and is deleted. - atlas.remove(&key_c); - - // Re-inserting A must allocate a fresh tile on a new texture, - // NOT return a stale tile referencing the deleted texture. - let tile_a2 = insert_tile(&atlas, &key_a, small); - - // The texture must actually exist — this would panic before the fix. - let _texture = atlas.metal_texture(tile_a2.texture_id); - } - - #[test] - fn test_remove_deallocates_tile_space_for_reuse() { - let Some(atlas) = create_atlas() else { - return; - }; - - let small = Size { - width: DevicePixels(64), - height: DevicePixels(64), - }; - let big = Size { - width: DevicePixels(700), - height: DevicePixels(700), - }; - - let keeper_key = make_image_key(1, 0); - let big_key_a = make_image_key(2, 0); - let big_key_b = make_image_key(3, 0); - - let keeper_tile = insert_tile(&atlas, &keeper_key, small); - let tile_a = insert_tile(&atlas, &big_key_a, big); - assert_eq!(keeper_tile.texture_id, tile_a.texture_id); - - atlas.remove(&big_key_a); - let tile_b = insert_tile(&atlas, &big_key_b, big); - assert_eq!(tile_b.texture_id, keeper_tile.texture_id); - } - - #[test] - fn test_remove_nonexistent_key_is_noop() { - let Some(atlas) = create_atlas() else { - return; - }; - let key = make_image_key(999, 0); - atlas.remove(&key); - } -} diff --git a/crates/gpui_pre_apple/src/metal_renderer.rs b/crates/gpui_pre_apple/src/metal_renderer.rs deleted file mode 100644 index c021ed5..0000000 --- a/crates/gpui_pre_apple/src/metal_renderer.rs +++ /dev/null @@ -1,1649 +0,0 @@ -use crate::metal_atlas::MetalAtlas; -use anyhow::{Context as _, Result}; -use block::ConcreteBlock; -use cocoa::{ - base::{NO, YES}, - foundation::{NSSize, NSUInteger}, - quartzcore::AutoresizingMask, -}; -use gpui::{ - point, size, AtlasTextureId, Background, Bounds, ContentMask, DevicePixels, PaintSurface, Path, - Point, PrimitiveBatch, ScaledPixels, Scene, Size, -}; -#[cfg(any(test, feature = "bench-support", feature = "test-support"))] -use image::RgbaImage; - -use core_foundation::base::TCFType; -use core_video::{ - metal_texture::CVMetalTextureGetTexture, metal_texture_cache::CVMetalTextureCache, - pixel_buffer::kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, -}; -use foreign_types::{ForeignType, ForeignTypeRef}; -use metal::{ - CAMetalLayer, CommandQueue, MTLGPUFamily, MTLPixelFormat, MTLResourceOptions, NSRange, -}; -use objc::{self, msg_send, sel, sel_impl}; -use parking_lot::Mutex; - -use std::{cell::Cell, ffi::c_void, mem, mem::MaybeUninit, ops::Range, ptr, slice, sync::Arc}; - -// Exported to metal -pub(crate) type PointF = gpui::Point; - -#[cfg(not(feature = "runtime_shaders"))] -const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib")); -#[cfg(feature = "runtime_shaders")] -const SHADERS_SOURCE_FILE: &str = include_str!(concat!(env!("OUT_DIR"), "/stitched_shaders.metal")); -// Use 4x MSAA, all devices support it. -// https://developer.apple.com/documentation/metal/mtldevice/1433355-supportstexturesamplecount -const PATH_SAMPLE_COUNT: u32 = 4; -/// Metal requires the offset a buffer is bound at to be 256-byte aligned. -const INSTANCE_BUFFER_ALIGNMENT: usize = 256; -const MAX_INSTANCE_BUFFER_SIZE: usize = 256 * 1024 * 1024; - -pub type Context = Arc>; -pub type Renderer = MetalRenderer; - -pub unsafe fn new_renderer( - context: self::Context, - _native_window: *mut c_void, - _native_view: *mut c_void, - _bounds: gpui::Size, - transparent: bool, -) -> Renderer { - MetalRenderer::new(context, transparent) -} - -pub struct InstanceBufferPool { - buffer_size: usize, - buffers: Vec, -} - -impl Default for InstanceBufferPool { - fn default() -> Self { - Self { - buffer_size: 2 * 1024 * 1024, - buffers: Vec::new(), - } - } -} - -pub(crate) struct InstanceBuffer { - metal_buffer: metal::Buffer, - size: usize, -} - -impl InstanceBufferPool { - pub(crate) fn reset(&mut self, buffer_size: usize) { - self.buffer_size = buffer_size; - self.buffers.clear(); - } - - pub(crate) fn acquire( - &mut self, - device: &metal::Device, - unified_memory: bool, - ) -> InstanceBuffer { - let buffer = self.buffers.pop().unwrap_or_else(|| { - let options = if unified_memory { - MTLResourceOptions::StorageModeShared - // Buffers are write only which can benefit from the combined cache - // https://developer.apple.com/documentation/metal/mtlresourceoptions/cpucachemodewritecombined - | MTLResourceOptions::CPUCacheModeWriteCombined - } else { - MTLResourceOptions::StorageModeManaged - }; - - device.new_buffer(self.buffer_size as u64, options) - }); - InstanceBuffer { - metal_buffer: buffer, - size: self.buffer_size, - } - } - - pub(crate) fn release(&mut self, buffer: InstanceBuffer) { - if buffer.size == self.buffer_size { - self.buffers.push(buffer.metal_buffer) - } - } -} - -pub struct MetalRenderer { - device: metal::Device, - layer: Option, - is_apple_gpu: bool, - is_unified_memory: bool, - presents_with_transaction: bool, - /// For headless rendering, tracks whether output should be opaque - opaque: bool, - command_queue: CommandQueue, - paths_rasterization_pipeline_state: metal::RenderPipelineState, - path_sprites_pipeline_state: metal::RenderPipelineState, - shadows_pipeline_state: metal::RenderPipelineState, - quads_pipeline_state: metal::RenderPipelineState, - underlines_pipeline_state: metal::RenderPipelineState, - monochrome_sprites_pipeline_state: metal::RenderPipelineState, - polychrome_sprites_pipeline_state: metal::RenderPipelineState, - surfaces_pipeline_state: metal::RenderPipelineState, - unit_vertices: metal::Buffer, - #[allow(clippy::arc_with_non_send_sync)] - instance_buffer_pool: Arc>, - sprite_atlas: Arc, - core_video_texture_cache: core_video::metal_texture_cache::CVMetalTextureCache, - path_intermediate_texture: Option, - path_intermediate_msaa_texture: Option, - path_sample_count: u32, - /// Offscreen render target reused across `render_scene` calls when - /// rendering headlessly without reading pixels back. - #[cfg(any(test, feature = "bench-support", feature = "test-support"))] - headless_render_target: Option, -} - -#[repr(C)] -pub struct PathRasterizationVertex { - pub xy_position: Point, - pub st_position: Point, - pub color: Background, - pub bounds: Bounds, - pub content_mask: ContentMask, -} - -impl MetalRenderer { - /// Creates a new MetalRenderer with a CAMetalLayer for window-based rendering. - pub fn new(instance_buffer_pool: Arc>, transparent: bool) -> Self { - let device = Self::create_device(); - - let layer = metal::MetalLayer::new(); - layer.set_device(&device); - layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm); - // Support direct-to-display rendering if the window is not transparent - // https://developer.apple.com/documentation/metal/managing-your-game-window-for-metal-in-macos - layer.set_opaque(!transparent); - layer.set_maximum_drawable_count(3); - // Allow texture reading for visual tests (captures screenshots without ScreenCaptureKit) - #[cfg(any(test, feature = "test-support"))] - layer.set_framebuffer_only(false); - unsafe { - let _: () = msg_send![&*layer, setAllowsNextDrawableTimeout: NO]; - let _: () = msg_send![&*layer, setNeedsDisplayOnBoundsChange: YES]; - let _: () = msg_send![ - &*layer, - setAutoresizingMask: AutoresizingMask::WIDTH_SIZABLE - | AutoresizingMask::HEIGHT_SIZABLE - ]; - } - - Self::new_internal(device, Some(layer), !transparent, instance_buffer_pool) - } - - /// Creates a new headless MetalRenderer for offscreen rendering without a window. - /// - /// This renderer can render scenes to images without requiring a CAMetalLayer, - /// window, or AppKit. Use `render_scene_to_image()` to render scenes. - #[cfg(any(test, feature = "bench-support", feature = "test-support"))] - pub fn new_headless(instance_buffer_pool: Arc>) -> Self { - let device = Self::create_device(); - Self::new_internal(device, None, true, instance_buffer_pool) - } - - fn create_device() -> metal::Device { - // Prefer low‐power integrated GPUs on Intel Mac. On Apple - // Silicon, there is only ever one GPU, so this is equivalent to - // `metal::Device::system_default()`. - if let Some(d) = metal::Device::all() - .into_iter() - .min_by_key(|d| (d.is_removable(), !d.is_low_power())) - { - d - } else { - // For some reason `all()` can return an empty list, see https://github.com/zed-industries/zed/issues/37689 - // In that case, we fall back to the system default device. - log::error!( - "Unable to enumerate Metal devices; attempting to use system default device" - ); - metal::Device::system_default().unwrap_or_else(|| { - log::error!("unable to access a compatible graphics device"); - std::process::exit(1); - }) - } - } - - fn new_internal( - device: metal::Device, - layer: Option, - opaque: bool, - instance_buffer_pool: Arc>, - ) -> Self { - #[cfg(feature = "runtime_shaders")] - let library = device - .new_library_with_source(&SHADERS_SOURCE_FILE, &metal::CompileOptions::new()) - .expect("error building metal library"); - #[cfg(not(feature = "runtime_shaders"))] - let library = device - .new_library_with_data(SHADERS_METALLIB) - .expect("error building metal library"); - - fn to_float2_bits(point: PointF) -> u64 { - let mut output = point.y.to_bits() as u64; - output <<= 32; - output |= point.x.to_bits() as u64; - output - } - - // Shared memory can be used only if CPU and GPU share the same memory space. - // https://developer.apple.com/documentation/metal/setting-resource-storage-modes - let is_unified_memory = device.has_unified_memory(); - // Apple GPU families support memoryless textures, which can significantly reduce - // memory usage by keeping render targets in on-chip tile memory instead of - // allocating backing store in system memory. - // https://developer.apple.com/documentation/metal/mtlgpufamily - let is_apple_gpu = device.supports_family(MTLGPUFamily::Apple1); - - let unit_vertices = [ - to_float2_bits(point(0., 0.)), - to_float2_bits(point(1., 0.)), - to_float2_bits(point(0., 1.)), - to_float2_bits(point(0., 1.)), - to_float2_bits(point(1., 0.)), - to_float2_bits(point(1., 1.)), - ]; - let unit_vertices = device.new_buffer_with_data( - unit_vertices.as_ptr() as *const c_void, - mem::size_of_val(&unit_vertices) as u64, - if is_unified_memory { - MTLResourceOptions::StorageModeShared - | MTLResourceOptions::CPUCacheModeWriteCombined - } else { - MTLResourceOptions::StorageModeManaged - }, - ); - - let paths_rasterization_pipeline_state = build_path_rasterization_pipeline_state( - &device, - &library, - "paths_rasterization", - "path_rasterization_vertex", - "path_rasterization_fragment", - MTLPixelFormat::BGRA8Unorm, - PATH_SAMPLE_COUNT, - ); - let path_sprites_pipeline_state = build_path_sprite_pipeline_state( - &device, - &library, - "path_sprites", - "path_sprite_vertex", - "path_sprite_fragment", - MTLPixelFormat::BGRA8Unorm, - ); - let shadows_pipeline_state = build_pipeline_state( - &device, - &library, - "shadows", - "shadow_vertex", - "shadow_fragment", - MTLPixelFormat::BGRA8Unorm, - ); - let quads_pipeline_state = build_pipeline_state( - &device, - &library, - "quads", - "quad_vertex", - "quad_fragment", - MTLPixelFormat::BGRA8Unorm, - ); - let underlines_pipeline_state = build_pipeline_state( - &device, - &library, - "underlines", - "underline_vertex", - "underline_fragment", - MTLPixelFormat::BGRA8Unorm, - ); - let monochrome_sprites_pipeline_state = build_pipeline_state( - &device, - &library, - "monochrome_sprites", - "monochrome_sprite_vertex", - "monochrome_sprite_fragment", - MTLPixelFormat::BGRA8Unorm, - ); - let polychrome_sprites_pipeline_state = build_pipeline_state( - &device, - &library, - "polychrome_sprites", - "polychrome_sprite_vertex", - "polychrome_sprite_fragment", - MTLPixelFormat::BGRA8Unorm, - ); - let surfaces_pipeline_state = build_pipeline_state( - &device, - &library, - "surfaces", - "surface_vertex", - "surface_fragment", - MTLPixelFormat::BGRA8Unorm, - ); - - let command_queue = device.new_command_queue(); - let sprite_atlas = Arc::new(MetalAtlas::new(device.clone(), is_apple_gpu)); - let core_video_texture_cache = - CVMetalTextureCache::new(None, device.clone(), None).unwrap(); - - Self { - device, - layer, - presents_with_transaction: false, - is_apple_gpu, - is_unified_memory, - opaque, - command_queue, - paths_rasterization_pipeline_state, - path_sprites_pipeline_state, - shadows_pipeline_state, - quads_pipeline_state, - underlines_pipeline_state, - monochrome_sprites_pipeline_state, - polychrome_sprites_pipeline_state, - surfaces_pipeline_state, - unit_vertices, - instance_buffer_pool, - sprite_atlas, - core_video_texture_cache, - path_intermediate_texture: None, - path_intermediate_msaa_texture: None, - path_sample_count: PATH_SAMPLE_COUNT, - #[cfg(any(test, feature = "bench-support", feature = "test-support"))] - headless_render_target: None, - } - } - - pub fn layer(&self) -> Option<&metal::MetalLayerRef> { - self.layer.as_ref().map(|l| l.as_ref()) - } - - pub fn layer_ptr(&self) -> *mut CAMetalLayer { - self.layer - .as_ref() - .map(|l| l.as_ptr()) - .unwrap_or(ptr::null_mut()) - } - - pub fn sprite_atlas(&self) -> &Arc { - &self.sprite_atlas - } - - pub fn set_presents_with_transaction(&mut self, presents_with_transaction: bool) { - self.presents_with_transaction = presents_with_transaction; - if let Some(layer) = &self.layer { - layer.set_presents_with_transaction(presents_with_transaction); - } - } - - pub fn update_drawable_size(&mut self, size: Size) { - if let Some(layer) = &self.layer { - let ns_size = NSSize { - width: size.width.0 as f64, - height: size.height.0 as f64, - }; - unsafe { - let _: () = msg_send![ - layer.as_ref(), - setDrawableSize: ns_size - ]; - } - } - self.update_path_intermediate_textures(size); - } - - fn update_path_intermediate_textures(&mut self, size: Size) { - // We are uncertain when this happens, but sometimes size can be 0 here. Most likely before - // the layout pass on window creation. Zero-sized texture creation causes SIGABRT. - // https://github.com/zed-industries/zed/issues/36229 - if size.width.0 <= 0 || size.height.0 <= 0 { - self.path_intermediate_texture = None; - self.path_intermediate_msaa_texture = None; - return; - } - - let texture_descriptor = metal::TextureDescriptor::new(); - texture_descriptor.set_width(size.width.0 as u64); - texture_descriptor.set_height(size.height.0 as u64); - texture_descriptor.set_pixel_format(metal::MTLPixelFormat::BGRA8Unorm); - texture_descriptor.set_storage_mode(metal::MTLStorageMode::Private); - texture_descriptor - .set_usage(metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead); - self.path_intermediate_texture = Some(self.device.new_texture(&texture_descriptor)); - - if self.path_sample_count > 1 { - // https://developer.apple.com/documentation/metal/choosing-a-resource-storage-mode-for-apple-gpus - // Rendering MSAA textures are done in a single pass, so we can use memory-less storage on Apple Silicon - let storage_mode = if self.is_apple_gpu { - metal::MTLStorageMode::Memoryless - } else { - metal::MTLStorageMode::Private - }; - - let msaa_descriptor = texture_descriptor; - msaa_descriptor.set_texture_type(metal::MTLTextureType::D2Multisample); - msaa_descriptor.set_storage_mode(storage_mode); - msaa_descriptor.set_sample_count(self.path_sample_count as _); - self.path_intermediate_msaa_texture = Some(self.device.new_texture(&msaa_descriptor)); - } else { - self.path_intermediate_msaa_texture = None; - } - } - - pub fn update_transparency(&mut self, transparent: bool) { - self.opaque = !transparent; - if let Some(layer) = &self.layer { - layer.set_opaque(!transparent); - } - } - - pub fn destroy(&self) { - // nothing to do - } - - pub fn draw(&mut self, scene: &Scene) { - let layer = match &self.layer { - Some(l) => l.clone(), - None => { - log::error!( - "draw() called on headless renderer - use render_scene_to_image() instead" - ); - return; - } - }; - let viewport_size = layer.drawable_size(); - let viewport_size: Size = size( - (viewport_size.width.ceil() as i32).into(), - (viewport_size.height.ceil() as i32).into(), - ); - let drawable = if let Some(drawable) = layer.next_drawable() { - drawable - } else { - log::error!( - "failed to retrieve next drawable, drawable size: {:?}", - viewport_size - ); - return; - }; - - let command_buffer = match self.render_frame(scene, drawable.texture(), viewport_size) { - Ok(command_buffer) => command_buffer, - Err(error) => { - log::error!("failed to render: {error:#}"); - return; - } - }; - - if self.presents_with_transaction { - command_buffer.commit(); - command_buffer.wait_until_scheduled(); - drawable.present(); - } else { - command_buffer.present_drawable(drawable); - command_buffer.commit(); - } - } - - fn render_frame( - &mut self, - scene: &Scene, - texture: &metal::TextureRef, - viewport_size: Size, - ) -> Result { - let mut writer = InstanceBufferWriter::new( - &self.device, - &self.instance_buffer_pool, - self.is_unified_memory, - ); - let instance_bindings = write_instances(scene, &mut writer).with_context(|| { - format!( - "scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces", - scene.paths.len(), - scene.shadows.len(), - scene.quads.len(), - scene.underlines.len(), - scene.monochrome_sprites.len(), - scene.polychrome_sprites.len(), - scene.surfaces.len(), - ) - })?; - let command_buffer = self.draw_primitives_to_texture( - scene, - &instance_bindings, - &mut writer, - texture, - viewport_size, - )?; - - let instance_buffer_pool = self.instance_buffer_pool.clone(); - let instance_buffer = Cell::new(Some(writer.finish())); - let block = ConcreteBlock::new(move |_| { - if let Some(instance_buffer) = instance_buffer.take() { - instance_buffer_pool.lock().release(instance_buffer); - } - }); - let block = block.copy(); - command_buffer.add_completed_handler(&block); - - Ok(command_buffer) - } - - /// Renders the scene to a texture and returns the pixel data as an RGBA image. - /// This does not present the frame to screen - useful for visual testing - /// where we want to capture what would be rendered without displaying it. - /// - /// Note: This requires a layer-backed renderer. For headless rendering, - /// use `render_scene_to_image()` instead. - #[cfg(any(test, feature = "test-support"))] - pub fn render_to_image(&mut self, scene: &Scene) -> Result { - let layer = self - .layer - .clone() - .ok_or_else(|| anyhow::anyhow!("render_to_image requires a layer-backed renderer"))?; - let viewport_size = layer.drawable_size(); - let viewport_size: Size = size( - (viewport_size.width.ceil() as i32).into(), - (viewport_size.height.ceil() as i32).into(), - ); - let drawable = layer - .next_drawable() - .ok_or_else(|| anyhow::anyhow!("Failed to get drawable for render_to_image"))?; - - let command_buffer = self.render_frame(scene, drawable.texture(), viewport_size)?; - - // Commit and wait for completion without presenting - command_buffer.commit(); - command_buffer.wait_until_completed(); - - read_texture_to_image(drawable.texture()) - } - - /// Renders a scene to an image without requiring a window or CAMetalLayer. - /// - /// This is the primary method for headless rendering. It creates an offscreen - /// texture, renders the scene to it, and returns the pixel data as an RGBA image. - #[cfg(any(test, feature = "bench-support", feature = "test-support"))] - pub fn render_scene_to_image( - &mut self, - scene: &Scene, - size: Size, - ) -> Result { - if size.width.0 <= 0 || size.height.0 <= 0 { - anyhow::bail!("Invalid size for render_scene_to_image: {:?}", size); - } - - // Update path intermediate textures for this size - self.update_path_intermediate_textures(size); - - // Create an offscreen texture as render target - let texture_descriptor = metal::TextureDescriptor::new(); - texture_descriptor.set_width(size.width.0 as u64); - texture_descriptor.set_height(size.height.0 as u64); - texture_descriptor.set_pixel_format(MTLPixelFormat::BGRA8Unorm); - texture_descriptor - .set_usage(metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead); - texture_descriptor.set_storage_mode(metal::MTLStorageMode::Managed); - let target_texture = self.device.new_texture(&texture_descriptor); - - let command_buffer = self.render_frame(scene, &target_texture, size)?; - - // On discrete GPUs (non-unified memory), Managed textures require an - // explicit blit synchronize before the CPU can read back the rendered - // data. Without this, get_bytes returns stale zeros. - if !self.is_unified_memory { - let blit = command_buffer.new_blit_command_encoder(); - blit.synchronize_resource(&target_texture); - blit.end_encoding(); - } - - // Commit and wait for completion - command_buffer.commit(); - command_buffer.wait_until_completed(); - - read_texture_to_image(&target_texture) - } - - /// Renders a scene to a reused offscreen texture without reading pixels - /// back or blocking on GPU completion. - /// - /// This mirrors the CPU cost of presenting a frame to a window (scene - /// encoding, instance buffer writes, command submission) and is used by - /// headless benchmark rendering, where the produced pixels are never - /// inspected. - #[cfg(any(test, feature = "bench-support", feature = "test-support"))] - pub fn render_scene(&mut self, scene: &Scene, size: Size) -> Result<()> { - if size.width.0 <= 0 || size.height.0 <= 0 { - anyhow::bail!("Invalid size for render_scene: {:?}", size); - } - - self.update_path_intermediate_textures(size); - - let needs_new_target = self.headless_render_target.as_ref().is_none_or(|texture| { - texture.width() != size.width.0 as u64 || texture.height() != size.height.0 as u64 - }); - if needs_new_target { - let texture_descriptor = metal::TextureDescriptor::new(); - texture_descriptor.set_width(size.width.0 as u64); - texture_descriptor.set_height(size.height.0 as u64); - texture_descriptor.set_pixel_format(MTLPixelFormat::BGRA8Unorm); - texture_descriptor.set_usage( - metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead, - ); - texture_descriptor.set_storage_mode(metal::MTLStorageMode::Private); - self.headless_render_target = Some(self.device.new_texture(&texture_descriptor)); - } - let target_texture = self - .headless_render_target - .clone() - .expect("just ensured the render target exists"); - - let command_buffer = self.render_frame(scene, &target_texture, size)?; - - // Commit without waiting, mirroring presentation to a real window where - // the CPU doesn't block on the GPU. - command_buffer.commit(); - Ok(()) - } - - fn draw_primitives_to_texture( - &mut self, - scene: &Scene, - instance_bindings: &InstanceBindings, - writer: &mut InstanceBufferWriter, - texture: &metal::TextureRef, - viewport_size: Size, - ) -> Result { - let command_queue = self.command_queue.clone(); - let command_buffer = command_queue.new_command_buffer(); - let alpha = if self.opaque { 1. } else { 0. }; - - let mut command_encoder = new_command_encoder_for_texture( - command_buffer, - texture, - viewport_size, - Some(metal::MTLClearColor::new(0., 0., 0., alpha)), - ); - - command_encoder.set_fragment_buffer( - 8, - Some(&instance_bindings.clips.buffer), - instance_bindings.clips.offset as u64, - ); - for batch in scene.batches() { - match batch { - PrimitiveBatch::Shadows(range) => { - self.draw_shadows(range, instance_bindings, viewport_size, command_encoder) - } - PrimitiveBatch::Quads(range) => { - self.draw_quads(range, instance_bindings, viewport_size, command_encoder) - } - PrimitiveBatch::Paths(range) => { - let paths = &scene.paths[range]; - command_encoder.end_encoding(); - - let did_draw = self.draw_paths_to_intermediate( - paths, - writer, - viewport_size, - command_buffer, - &instance_bindings.clips, - )?; - - command_encoder = new_command_encoder_for_texture( - command_buffer, - texture, - viewport_size, - None, - ); - - command_encoder.set_fragment_buffer( - 8, - Some(&instance_bindings.clips.buffer), - instance_bindings.clips.offset as u64, - ); - if did_draw { - if let Err(error) = self.draw_paths_from_intermediate( - paths, - writer, - viewport_size, - command_encoder, - ) { - command_encoder.end_encoding(); - return Err(error); - } - } - } - PrimitiveBatch::Underlines(range) => { - self.draw_underlines(range, instance_bindings, viewport_size, command_encoder) - } - PrimitiveBatch::MonochromeSprites { texture_id, range } => self - .draw_monochrome_sprites( - texture_id, - range, - instance_bindings, - viewport_size, - command_encoder, - ), - PrimitiveBatch::PolychromeSprites { texture_id, range } => self - .draw_polychrome_sprites( - texture_id, - range, - instance_bindings, - viewport_size, - command_encoder, - ), - PrimitiveBatch::Surfaces(range) => self.draw_surfaces( - &scene.surfaces[range.clone()], - range.start, - instance_bindings, - viewport_size, - command_encoder, - ), - PrimitiveBatch::SubpixelSprites { .. } => unreachable!(), - } - } - - command_encoder.end_encoding(); - - Ok(command_buffer.to_owned()) - } - - fn draw_paths_to_intermediate( - &self, - paths: &[Path], - writer: &mut InstanceBufferWriter, - viewport_size: Size, - command_buffer: &metal::CommandBufferRef, - clips: &InstanceBinding, - ) -> Result { - if paths.is_empty() { - return Ok(false); - } - let intermediate_texture = self - .path_intermediate_texture - .as_ref() - .context("missing path intermediate texture")?; - - let mut vertices = Vec::new(); - for path in paths { - vertices.extend(path.vertices.iter().map(|v| PathRasterizationVertex { - xy_position: v.xy_position, - st_position: v.st_position, - color: path.color, - bounds: path.bounds.intersect(&path.content_mask.bounds), - content_mask: path.content_mask, - })); - } - let vertex_instance_bindings = writer.write(&vertices)?; - - let render_pass_descriptor = metal::RenderPassDescriptor::new(); - let color_attachment = render_pass_descriptor - .color_attachments() - .object_at(0) - .unwrap(); - color_attachment.set_load_action(metal::MTLLoadAction::Clear); - color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 0.)); - - if let Some(msaa_texture) = &self.path_intermediate_msaa_texture { - color_attachment.set_texture(Some(msaa_texture)); - color_attachment.set_resolve_texture(Some(intermediate_texture)); - color_attachment.set_store_action(metal::MTLStoreAction::MultisampleResolve); - } else { - color_attachment.set_texture(Some(intermediate_texture)); - color_attachment.set_store_action(metal::MTLStoreAction::Store); - } - - let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor); - command_encoder.set_fragment_buffer(8, Some(&clips.buffer), clips.offset as u64); - command_encoder.set_render_pipeline_state(&self.paths_rasterization_pipeline_state); - command_encoder.set_vertex_buffer( - PathRasterizationInputIndex::Vertices as u64, - Some(&vertex_instance_bindings.buffer), - vertex_instance_bindings.offset as u64, - ); - command_encoder.set_vertex_bytes( - PathRasterizationInputIndex::ViewportSize as u64, - mem::size_of_val(&viewport_size) as u64, - &viewport_size as *const Size as *const _, - ); - command_encoder.set_fragment_buffer( - PathRasterizationInputIndex::Vertices as u64, - Some(&vertex_instance_bindings.buffer), - vertex_instance_bindings.offset as u64, - ); - command_encoder.draw_primitives( - metal::MTLPrimitiveType::Triangle, - 0, - vertices.len() as u64, - ); - - command_encoder.end_encoding(); - Ok(true) - } - - fn draw_shadows( - &self, - shadows: Range, - instance_bindings: &InstanceBindings, - viewport_size: Size, - command_encoder: &metal::RenderCommandEncoderRef, - ) { - if shadows.is_empty() { - return; - } - - command_encoder.set_render_pipeline_state(&self.shadows_pipeline_state); - command_encoder.set_vertex_buffer( - ShadowInputIndex::Vertices as u64, - Some(&self.unit_vertices), - 0, - ); - command_encoder.set_vertex_buffer( - ShadowInputIndex::Shadows as u64, - Some(&instance_bindings.shadows.buffer), - instance_bindings.shadows.offset as u64, - ); - command_encoder.set_fragment_buffer( - ShadowInputIndex::Shadows as u64, - Some(&instance_bindings.shadows.buffer), - instance_bindings.shadows.offset as u64, - ); - command_encoder.set_vertex_bytes( - ShadowInputIndex::ViewportSize as u64, - mem::size_of_val(&viewport_size) as u64, - &viewport_size as *const Size as *const _, - ); - - command_encoder.draw_primitives_instanced_base_instance( - metal::MTLPrimitiveType::Triangle, - 0, - 6, - shadows.len() as u64, - shadows.start as u64, - ); - } - - fn draw_quads( - &self, - quads: Range, - instance_bindings: &InstanceBindings, - viewport_size: Size, - command_encoder: &metal::RenderCommandEncoderRef, - ) { - if quads.is_empty() { - return; - } - - command_encoder.set_render_pipeline_state(&self.quads_pipeline_state); - command_encoder.set_vertex_buffer( - QuadInputIndex::Vertices as u64, - Some(&self.unit_vertices), - 0, - ); - command_encoder.set_vertex_buffer( - QuadInputIndex::Quads as u64, - Some(&instance_bindings.quads.buffer), - instance_bindings.quads.offset as u64, - ); - command_encoder.set_fragment_buffer( - QuadInputIndex::Quads as u64, - Some(&instance_bindings.quads.buffer), - instance_bindings.quads.offset as u64, - ); - command_encoder.set_vertex_bytes( - QuadInputIndex::ViewportSize as u64, - mem::size_of_val(&viewport_size) as u64, - &viewport_size as *const Size as *const _, - ); - - command_encoder.draw_primitives_instanced_base_instance( - metal::MTLPrimitiveType::Triangle, - 0, - 6, - quads.len() as u64, - quads.start as u64, - ); - } - - fn draw_paths_from_intermediate( - &self, - paths: &[Path], - writer: &mut InstanceBufferWriter, - viewport_size: Size, - command_encoder: &metal::RenderCommandEncoderRef, - ) -> Result<()> { - let Some(first_path) = paths.first() else { - return Ok(()); - }; - let intermediate_texture = self - .path_intermediate_texture - .as_ref() - .context("missing path intermediate texture")?; - - command_encoder.set_render_pipeline_state(&self.path_sprites_pipeline_state); - command_encoder.set_vertex_buffer( - SpriteInputIndex::Vertices as u64, - Some(&self.unit_vertices), - 0, - ); - command_encoder.set_vertex_bytes( - SpriteInputIndex::ViewportSize as u64, - mem::size_of_val(&viewport_size) as u64, - &viewport_size as *const Size as *const _, - ); - - command_encoder.set_fragment_texture( - SpriteInputIndex::AtlasTexture as u64, - Some(intermediate_texture), - ); - - // When copying paths from the intermediate texture to the drawable, - // each pixel must only be copied once, in case of transparent paths. - // - // If all paths have the same draw order, then their bounds are all - // disjoint, so we can copy each path's bounds individually. If this - // batch combines different draw orders, we perform a single copy - // for a minimal spanning rect. - let sprites; - if paths.last().unwrap().order == first_path.order { - sprites = paths - .iter() - .map(|path| PathSprite { - bounds: path.clipped_bounds(), - }) - .collect(); - } else { - let mut bounds = first_path.clipped_bounds(); - for path in paths.iter().skip(1) { - bounds = bounds.union(&path.clipped_bounds()); - } - sprites = vec![PathSprite { bounds }]; - } - - let sprite_instance_bindings = writer.write(&sprites)?; - command_encoder.set_vertex_buffer( - SpriteInputIndex::Sprites as u64, - Some(&sprite_instance_bindings.buffer), - sprite_instance_bindings.offset as u64, - ); - - command_encoder.draw_primitives_instanced( - metal::MTLPrimitiveType::Triangle, - 0, - 6, - sprites.len() as u64, - ); - Ok(()) - } - - fn draw_underlines( - &self, - underlines: Range, - instance_bindings: &InstanceBindings, - viewport_size: Size, - command_encoder: &metal::RenderCommandEncoderRef, - ) { - if underlines.is_empty() { - return; - } - - command_encoder.set_render_pipeline_state(&self.underlines_pipeline_state); - command_encoder.set_vertex_buffer( - UnderlineInputIndex::Vertices as u64, - Some(&self.unit_vertices), - 0, - ); - command_encoder.set_vertex_buffer( - UnderlineInputIndex::Underlines as u64, - Some(&instance_bindings.underlines.buffer), - instance_bindings.underlines.offset as u64, - ); - command_encoder.set_fragment_buffer( - UnderlineInputIndex::Underlines as u64, - Some(&instance_bindings.underlines.buffer), - instance_bindings.underlines.offset as u64, - ); - command_encoder.set_vertex_bytes( - UnderlineInputIndex::ViewportSize as u64, - mem::size_of_val(&viewport_size) as u64, - &viewport_size as *const Size as *const _, - ); - - command_encoder.draw_primitives_instanced_base_instance( - metal::MTLPrimitiveType::Triangle, - 0, - 6, - underlines.len() as u64, - underlines.start as u64, - ); - } - - fn draw_monochrome_sprites( - &self, - texture_id: AtlasTextureId, - sprites: Range, - instance_bindings: &InstanceBindings, - viewport_size: Size, - command_encoder: &metal::RenderCommandEncoderRef, - ) { - if sprites.is_empty() { - return; - } - - let texture = self.sprite_atlas.metal_texture(texture_id); - let texture_size = size( - DevicePixels(texture.width() as i32), - DevicePixels(texture.height() as i32), - ); - command_encoder.set_render_pipeline_state(&self.monochrome_sprites_pipeline_state); - command_encoder.set_vertex_buffer( - SpriteInputIndex::Vertices as u64, - Some(&self.unit_vertices), - 0, - ); - command_encoder.set_vertex_buffer( - SpriteInputIndex::Sprites as u64, - Some(&instance_bindings.monochrome_sprites.buffer), - instance_bindings.monochrome_sprites.offset as u64, - ); - command_encoder.set_vertex_bytes( - SpriteInputIndex::ViewportSize as u64, - mem::size_of_val(&viewport_size) as u64, - &viewport_size as *const Size as *const _, - ); - command_encoder.set_vertex_bytes( - SpriteInputIndex::AtlasTextureSize as u64, - mem::size_of_val(&texture_size) as u64, - &texture_size as *const Size as *const _, - ); - command_encoder.set_fragment_buffer( - SpriteInputIndex::Sprites as u64, - Some(&instance_bindings.monochrome_sprites.buffer), - instance_bindings.monochrome_sprites.offset as u64, - ); - command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture)); - - command_encoder.draw_primitives_instanced_base_instance( - metal::MTLPrimitiveType::Triangle, - 0, - 6, - sprites.len() as u64, - sprites.start as u64, - ); - } - - fn draw_polychrome_sprites( - &self, - texture_id: AtlasTextureId, - sprites: Range, - instance_bindings: &InstanceBindings, - viewport_size: Size, - command_encoder: &metal::RenderCommandEncoderRef, - ) { - if sprites.is_empty() { - return; - } - - let texture = self.sprite_atlas.metal_texture(texture_id); - let texture_size = size( - DevicePixels(texture.width() as i32), - DevicePixels(texture.height() as i32), - ); - command_encoder.set_render_pipeline_state(&self.polychrome_sprites_pipeline_state); - command_encoder.set_vertex_buffer( - SpriteInputIndex::Vertices as u64, - Some(&self.unit_vertices), - 0, - ); - command_encoder.set_vertex_buffer( - SpriteInputIndex::Sprites as u64, - Some(&instance_bindings.polychrome_sprites.buffer), - instance_bindings.polychrome_sprites.offset as u64, - ); - command_encoder.set_vertex_bytes( - SpriteInputIndex::ViewportSize as u64, - mem::size_of_val(&viewport_size) as u64, - &viewport_size as *const Size as *const _, - ); - command_encoder.set_vertex_bytes( - SpriteInputIndex::AtlasTextureSize as u64, - mem::size_of_val(&texture_size) as u64, - &texture_size as *const Size as *const _, - ); - command_encoder.set_fragment_buffer( - SpriteInputIndex::Sprites as u64, - Some(&instance_bindings.polychrome_sprites.buffer), - instance_bindings.polychrome_sprites.offset as u64, - ); - command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture)); - - command_encoder.draw_primitives_instanced_base_instance( - metal::MTLPrimitiveType::Triangle, - 0, - 6, - sprites.len() as u64, - sprites.start as u64, - ); - } - - fn draw_surfaces( - &mut self, - surfaces: &[PaintSurface], - first_surface: usize, - instance_bindings: &InstanceBindings, - viewport_size: Size, - command_encoder: &metal::RenderCommandEncoderRef, - ) { - if surfaces.is_empty() { - return; - } - - command_encoder.set_render_pipeline_state(&self.surfaces_pipeline_state); - command_encoder.set_vertex_buffer( - SurfaceInputIndex::Vertices as u64, - Some(&self.unit_vertices), - 0, - ); - command_encoder.set_vertex_buffer( - SurfaceInputIndex::Surfaces as u64, - Some(&instance_bindings.surfaces.buffer), - instance_bindings.surfaces.offset as u64, - ); - command_encoder.set_vertex_bytes( - SurfaceInputIndex::ViewportSize as u64, - mem::size_of_val(&viewport_size) as u64, - &viewport_size as *const Size as *const _, - ); - - for (index, surface) in surfaces.iter().enumerate() { - let texture_size = size( - DevicePixels::from(surface.image_buffer.get_width() as i32), - DevicePixels::from(surface.image_buffer.get_height() as i32), - ); - - assert_eq!( - surface.image_buffer.get_pixel_format(), - kCVPixelFormatType_420YpCbCr8BiPlanarFullRange - ); - - let y_texture = self - .core_video_texture_cache - .create_texture_from_image( - surface.image_buffer.as_concrete_TypeRef(), - None, - MTLPixelFormat::R8Unorm, - surface.image_buffer.get_width_of_plane(0), - surface.image_buffer.get_height_of_plane(0), - 0, - ) - .unwrap(); - let cb_cr_texture = self - .core_video_texture_cache - .create_texture_from_image( - surface.image_buffer.as_concrete_TypeRef(), - None, - MTLPixelFormat::RG8Unorm, - surface.image_buffer.get_width_of_plane(1), - surface.image_buffer.get_height_of_plane(1), - 1, - ) - .unwrap(); - - command_encoder.set_vertex_bytes( - SurfaceInputIndex::TextureSize as u64, - mem::size_of_val(&texture_size) as u64, - &texture_size as *const Size as *const _, - ); - // let y_texture = y_texture.get_texture().unwrap(). - command_encoder.set_fragment_texture(SurfaceInputIndex::YTexture as u64, unsafe { - let texture = CVMetalTextureGetTexture(y_texture.as_concrete_TypeRef()); - Some(metal::TextureRef::from_ptr(texture as *mut _)) - }); - command_encoder.set_fragment_texture(SurfaceInputIndex::CbCrTexture as u64, unsafe { - let texture = CVMetalTextureGetTexture(cb_cr_texture.as_concrete_TypeRef()); - Some(metal::TextureRef::from_ptr(texture as *mut _)) - }); - - command_encoder.draw_primitives_instanced_base_instance( - metal::MTLPrimitiveType::Triangle, - 0, - 6, - 1, - (first_surface + index) as u64, - ); - } - } -} - -fn new_command_encoder_for_texture<'a>( - command_buffer: &'a metal::CommandBufferRef, - texture: &'a metal::TextureRef, - viewport_size: Size, - clear_color: Option, -) -> &'a metal::RenderCommandEncoderRef { - let render_pass_descriptor = metal::RenderPassDescriptor::new(); - let color_attachment = render_pass_descriptor - .color_attachments() - .object_at(0) - .unwrap(); - color_attachment.set_texture(Some(texture)); - color_attachment.set_store_action(metal::MTLStoreAction::Store); - if let Some(clear_color) = clear_color { - color_attachment.set_load_action(metal::MTLLoadAction::Clear); - color_attachment.set_clear_color(clear_color); - } else { - color_attachment.set_load_action(metal::MTLLoadAction::Load); - } - - let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor); - command_encoder.set_viewport(metal::MTLViewport { - originX: 0.0, - originY: 0.0, - width: i32::from(viewport_size.width) as f64, - height: i32::from(viewport_size.height) as f64, - znear: 0.0, - zfar: 1.0, - }); - command_encoder -} - -#[cfg(any(test, feature = "bench-support", feature = "test-support"))] -fn read_texture_to_image(texture: &metal::TextureRef) -> Result { - let width = texture.width() as u32; - let height = texture.height() as u32; - let bytes_per_row = width as usize * 4; - let mut pixels = vec![0u8; height as usize * bytes_per_row]; - - let region = metal::MTLRegion { - origin: metal::MTLOrigin { x: 0, y: 0, z: 0 }, - size: metal::MTLSize { - width: width as u64, - height: height as u64, - depth: 1, - }, - }; - texture.get_bytes( - pixels.as_mut_ptr() as *mut std::ffi::c_void, - bytes_per_row as u64, - region, - 0, - ); - - // Convert BGRA to RGBA (swap B and R channels) - for chunk in pixels.chunks_exact_mut(4) { - chunk.swap(0, 2); - } - - RgbaImage::from_raw(width, height, pixels).context("failed to create RgbaImage from pixel data") -} - -fn build_pipeline_state( - device: &metal::DeviceRef, - library: &metal::LibraryRef, - label: &str, - vertex_fn_name: &str, - fragment_fn_name: &str, - pixel_format: metal::MTLPixelFormat, -) -> metal::RenderPipelineState { - let vertex_fn = library - .get_function(vertex_fn_name, None) - .expect("error locating vertex function"); - let fragment_fn = library - .get_function(fragment_fn_name, None) - .expect("error locating fragment function"); - - let descriptor = metal::RenderPipelineDescriptor::new(); - descriptor.set_label(label); - descriptor.set_vertex_function(Some(vertex_fn.as_ref())); - descriptor.set_fragment_function(Some(fragment_fn.as_ref())); - let color_attachment = descriptor.color_attachments().object_at(0).unwrap(); - color_attachment.set_pixel_format(pixel_format); - color_attachment.set_blending_enabled(true); - color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add); - color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add); - color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::SourceAlpha); - color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One); - color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha); - color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::One); - - device - .new_render_pipeline_state(&descriptor) - .expect("could not create render pipeline state") -} - -fn build_path_sprite_pipeline_state( - device: &metal::DeviceRef, - library: &metal::LibraryRef, - label: &str, - vertex_fn_name: &str, - fragment_fn_name: &str, - pixel_format: metal::MTLPixelFormat, -) -> metal::RenderPipelineState { - let vertex_fn = library - .get_function(vertex_fn_name, None) - .expect("error locating vertex function"); - let fragment_fn = library - .get_function(fragment_fn_name, None) - .expect("error locating fragment function"); - - let descriptor = metal::RenderPipelineDescriptor::new(); - descriptor.set_label(label); - descriptor.set_vertex_function(Some(vertex_fn.as_ref())); - descriptor.set_fragment_function(Some(fragment_fn.as_ref())); - let color_attachment = descriptor.color_attachments().object_at(0).unwrap(); - color_attachment.set_pixel_format(pixel_format); - color_attachment.set_blending_enabled(true); - color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add); - color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add); - color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::One); - color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One); - color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha); - color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::One); - - device - .new_render_pipeline_state(&descriptor) - .expect("could not create render pipeline state") -} - -fn build_path_rasterization_pipeline_state( - device: &metal::DeviceRef, - library: &metal::LibraryRef, - label: &str, - vertex_fn_name: &str, - fragment_fn_name: &str, - pixel_format: metal::MTLPixelFormat, - path_sample_count: u32, -) -> metal::RenderPipelineState { - let vertex_fn = library - .get_function(vertex_fn_name, None) - .expect("error locating vertex function"); - let fragment_fn = library - .get_function(fragment_fn_name, None) - .expect("error locating fragment function"); - - let descriptor = metal::RenderPipelineDescriptor::new(); - descriptor.set_label(label); - descriptor.set_vertex_function(Some(vertex_fn.as_ref())); - descriptor.set_fragment_function(Some(fragment_fn.as_ref())); - if path_sample_count > 1 { - descriptor.set_raster_sample_count(path_sample_count as _); - descriptor.set_alpha_to_coverage_enabled(false); - } - let color_attachment = descriptor.color_attachments().object_at(0).unwrap(); - color_attachment.set_pixel_format(pixel_format); - color_attachment.set_blending_enabled(true); - color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add); - color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add); - color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::One); - color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One); - color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha); - color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha); - - device - .new_render_pipeline_state(&descriptor) - .expect("could not create render pipeline state") -} - -#[derive(Clone)] -struct InstanceBinding { - buffer: metal::Buffer, - offset: usize, -} - -struct InstanceBindings { - clips: InstanceBinding, - quads: InstanceBinding, - shadows: InstanceBinding, - underlines: InstanceBinding, - monochrome_sprites: InstanceBinding, - polychrome_sprites: InstanceBinding, - surfaces: InstanceBinding, -} - -fn write_instances(scene: &Scene, writer: &mut InstanceBufferWriter) -> Result { - let empty_clip = gpui::RoundedClip::::default(); - let clips = if scene.rounded_clips.is_empty() { - std::slice::from_ref(&empty_clip) - } else { - &scene.rounded_clips - }; - Ok(InstanceBindings { - clips: writer.write(clips)?, - quads: writer.write(&scene.quads)?, - shadows: writer.write(&scene.shadows)?, - underlines: writer.write(&scene.underlines)?, - monochrome_sprites: writer.write(&scene.monochrome_sprites)?, - polychrome_sprites: writer.write(&scene.polychrome_sprites)?, - surfaces: writer.write_iter(scene.surfaces.iter().map(|surface| SurfaceBounds { - bounds: surface.bounds, - content_mask: surface.content_mask, - }))?, - }) -} - -struct InstanceBufferWriter { - device: metal::Device, - pool: Arc>, - unified_memory: bool, - filled: Vec<(InstanceBuffer, usize)>, - current: InstanceBuffer, - offset: usize, -} - -impl InstanceBufferWriter { - fn new( - device: &metal::Device, - pool: &Arc>, - unified_memory: bool, - ) -> Self { - let current = pool.lock().acquire(device, unified_memory); - Self { - device: device.clone(), - pool: pool.clone(), - unified_memory, - filled: Vec::new(), - current, - offset: 0, - } - } - - fn allocate(&mut self, count: usize) -> Result<(InstanceBinding, &mut [MaybeUninit])> { - let size = mem::size_of::() * count; - let mut offset = self.offset.next_multiple_of(INSTANCE_BUFFER_ALIGNMENT); - if offset + size > self.current.size { - self.grow(size)?; - offset = 0; - } - self.offset = offset + size; - - let binding = InstanceBinding { - buffer: self.current.metal_buffer.clone(), - offset, - }; - // Safety: the reservation lies within a buffer this frame owns - // exclusively, and never overlaps one handed out earlier. - let values = unsafe { - let start = (self.current.metal_buffer.contents() as *mut u8).add(offset); - slice::from_raw_parts_mut(start.cast::>(), count) - }; - Ok((binding, values)) - } - - fn write(&mut self, values: &[T]) -> Result { - let (binding, destination) = self.allocate::(values.len())?; - unsafe { - ptr::copy_nonoverlapping( - values.as_ptr(), - destination.as_mut_ptr().cast::(), - values.len(), - ); - } - Ok(binding) - } - - fn write_iter( - &mut self, - values: impl ExactSizeIterator, - ) -> Result { - let (binding, destination) = self.allocate::(values.len())?; - for (slot, value) in destination.iter_mut().zip(values) { - slot.write(value); - } - Ok(binding) - } - - fn grow(&mut self, required: usize) -> Result<()> { - let mut pool = self.pool.lock(); - let buffer_size = (pool.buffer_size * 2) - .max(required.next_power_of_two()) - .min(MAX_INSTANCE_BUFFER_SIZE); - anyhow::ensure!( - buffer_size >= required, - "instance buffer needs {required} bytes, above the maximum of {MAX_INSTANCE_BUFFER_SIZE}" - ); - anyhow::ensure!( - buffer_size > self.current.size, - "frame instance data exceeds the {MAX_INSTANCE_BUFFER_SIZE}-byte maximum" - ); - if buffer_size != pool.buffer_size { - log::info!("increased instance buffer size to {buffer_size}"); - pool.reset(buffer_size); - } - let buffer = pool.acquire(&self.device, self.unified_memory); - drop(pool); - - let filled = mem::replace(&mut self.current, buffer); - self.filled.push((filled, self.offset)); - self.offset = 0; - Ok(()) - } - - fn finish(self) -> InstanceBuffer { - let Self { - unified_memory, - filled, - current, - offset, - .. - } = self; - - if !unified_memory { - for (buffer, written) in &filled { - if *written == 0 { - continue; - } - buffer.metal_buffer.did_modify_range(NSRange { - location: 0, - length: *written as NSUInteger, - }); - } - if offset > 0 { - current.metal_buffer.did_modify_range(NSRange { - location: 0, - length: offset as NSUInteger, - }); - } - } - - // Metal retains encoded resources until the command buffer completes. - // Only the final, largest buffer is worth keeping in the pool. - drop(filled); - current - } -} - -#[repr(C)] -enum ShadowInputIndex { - Vertices = 0, - Shadows = 1, - ViewportSize = 2, -} - -#[repr(C)] -enum QuadInputIndex { - Vertices = 0, - Quads = 1, - ViewportSize = 2, -} - -#[repr(C)] -enum UnderlineInputIndex { - Vertices = 0, - Underlines = 1, - ViewportSize = 2, -} - -#[repr(C)] -enum SpriteInputIndex { - Vertices = 0, - Sprites = 1, - ViewportSize = 2, - AtlasTextureSize = 3, - AtlasTexture = 4, -} - -#[repr(C)] -enum SurfaceInputIndex { - Vertices = 0, - Surfaces = 1, - ViewportSize = 2, - TextureSize = 3, - YTexture = 4, - CbCrTexture = 5, -} - -#[repr(C)] -enum PathRasterizationInputIndex { - Vertices = 0, - ViewportSize = 1, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -#[repr(C)] -pub struct PathSprite { - pub bounds: Bounds, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -#[repr(C)] -pub struct SurfaceBounds { - pub bounds: Bounds, - pub content_mask: ContentMask, -} - -#[cfg(any(test, feature = "bench-support", feature = "test-support"))] -pub struct MetalHeadlessRenderer { - renderer: MetalRenderer, -} - -#[cfg(any(test, feature = "bench-support", feature = "test-support"))] -impl MetalHeadlessRenderer { - pub fn new() -> Self { - let instance_buffer_pool = Arc::new(Mutex::new(InstanceBufferPool::default())); - let renderer = MetalRenderer::new_headless(instance_buffer_pool); - Self { renderer } - } -} - -#[cfg(any(test, feature = "bench-support", feature = "test-support"))] -impl gpui::PlatformHeadlessRenderer for MetalHeadlessRenderer { - fn render_scene_to_image( - &mut self, - scene: &Scene, - size: Size, - ) -> anyhow::Result { - self.renderer.render_scene_to_image(scene, size) - } - - fn render_scene(&mut self, scene: &Scene, size: Size) -> anyhow::Result<()> { - self.renderer.render_scene(scene, size) - } - - fn sprite_atlas(&self) -> Arc { - self.renderer.sprite_atlas().clone() - } -} diff --git a/crates/gpui_pre_apple/src/shaders.metal b/crates/gpui_pre_apple/src/shaders.metal deleted file mode 100644 index 9fd9701..0000000 --- a/crates/gpui_pre_apple/src/shaders.metal +++ /dev/null @@ -1,1384 +0,0 @@ -#include -#include - -using namespace metal; - -float content_mask_coverage(float2 position, ContentMask_ScaledPixels mask, - constant RoundedClip_ScaledPixels *clips); -float4 hsla_to_rgba(Hsla hsla); -float3 srgb_to_linear(float3 color); -float3 linear_to_srgb(float3 color); -float4 srgb_to_oklab(float4 color); -float4 oklab_to_srgb(float4 color); -float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds, - constant Size_DevicePixels *viewport_size); -float4 to_device_position_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds, - TransformationMatrix transformation, - constant Size_DevicePixels *input_viewport_size); - -float2 to_tile_position(float2 unit_vertex, AtlasTile tile, - constant Size_DevicePixels *atlas_size); -float4 distance_from_clip_rect(float2 unit_vertex, Bounds_ScaledPixels bounds, - ContentMask_ScaledPixels mask); -float4 distance_from_clip_rect_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds, - ContentMask_ScaledPixels mask, TransformationMatrix transformation); -float corner_dash_velocity(float dv1, float dv2); -float dash_alpha(float t, float period, float length, float dash_velocity, - float antialias_threshold); -float quarter_ellipse_sdf(float2 point, float2 radii); -float pick_corner_radius(float2 center_to_point, Corners_ScaledPixels corner_radii); -float quad_sdf(float2 point, Bounds_ScaledPixels bounds, - Corners_ScaledPixels corner_radii); -float quad_sdf_impl(float2 center_to_point, float corner_radius); -float gaussian(float x, float sigma); -float2 erf(float2 x); -float blur_along_x(float x, float y, float sigma, float corner, - float2 half_size); -float4 over(float4 below, float4 above); -float radians(float degrees); -float4 fill_color(Background background, float2 position, Bounds_ScaledPixels bounds, - float4 solid_color, float4 color0, float4 color1); - -struct GradientColor { - float4 solid; - float4 color0; - float4 color1; -}; -GradientColor prepare_fill_color(uint tag, uint color_space, Hsla solid, Hsla color0, Hsla color1); - -struct QuadVertexOutput { - uint quad_id [[flat]]; - float4 position [[position]]; - float4 border_color [[flat]]; - float4 background_solid [[flat]]; - float4 background_color0 [[flat]]; - float4 background_color1 [[flat]]; - float clip_distance [[clip_distance]][4]; -}; - -struct QuadFragmentInput { - uint quad_id [[flat]]; - float4 position [[position]]; - float4 border_color [[flat]]; - float4 background_solid [[flat]]; - float4 background_color0 [[flat]]; - float4 background_color1 [[flat]]; -}; - -vertex QuadVertexOutput quad_vertex(uint unit_vertex_id [[vertex_id]], - uint quad_id [[instance_id]], - constant float2 *unit_vertices - [[buffer(QuadInputIndex_Vertices)]], - constant Quad *quads - [[buffer(QuadInputIndex_Quads)]], - constant Size_DevicePixels *viewport_size - [[buffer(QuadInputIndex_ViewportSize)]]) { - float2 unit_vertex = unit_vertices[unit_vertex_id]; - Quad quad = quads[quad_id]; - float4 device_position = - to_device_position(unit_vertex, quad.bounds, viewport_size); - float4 clip_distance = distance_from_clip_rect(unit_vertex, quad.bounds, - quad.content_mask); - float4 border_color = hsla_to_rgba(quad.border_color); - - GradientColor gradient = prepare_fill_color( - quad.background.tag, - quad.background.color_space, - quad.background.solid, - quad.background.colors[0].color, - quad.background.colors[1].color - ); - - return QuadVertexOutput{ - quad_id, - device_position, - border_color, - gradient.solid, - gradient.color0, - gradient.color1, - {clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}}; -} - -fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]], - constant Quad *quads - [[buffer(QuadInputIndex_Quads)]], - constant RoundedClip_ScaledPixels *clips [[buffer(8)]]) { - Quad quad = quads[input.quad_id]; - float coverage = content_mask_coverage(input.position.xy, quad.content_mask, clips); - if (coverage <= 0.0) { - return float4(0.0); - } - float4 background_color = fill_color(quad.background, input.position.xy, quad.bounds, - input.background_solid, input.background_color0, input.background_color1); - - bool unrounded = quad.corner_radii.top_left == 0.0 && - quad.corner_radii.bottom_left == 0.0 && - quad.corner_radii.top_right == 0.0 && - quad.corner_radii.bottom_right == 0.0; - - // Fast path when the quad is not rounded and doesn't have any border - if (quad.border_widths.top == 0.0 && - quad.border_widths.left == 0.0 && - quad.border_widths.right == 0.0 && - quad.border_widths.bottom == 0.0 && - unrounded) { - return background_color * float4(1.0, 1.0, 1.0, coverage); - } - - float2 size = float2(quad.bounds.size.width, quad.bounds.size.height); - float2 half_size = size / 2.0; - float2 point = input.position.xy - float2(quad.bounds.origin.x, quad.bounds.origin.y); - float2 center_to_point = point - half_size; - - // Signed distance field threshold for inclusion of pixels. 0.5 is the - // minimum distance between the center of the pixel and the edge. - const float antialias_threshold = 0.5; - - // Radius of the nearest corner - float corner_radius = pick_corner_radius(center_to_point, quad.corner_radii); - - // Width of the nearest borders - float2 border = float2( - center_to_point.x < 0.0 ? quad.border_widths.left : quad.border_widths.right, - center_to_point.y < 0.0 ? quad.border_widths.top : quad.border_widths.bottom - ); - - // 0-width borders are reduced so that `inner_sdf >= antialias_threshold`. - // The purpose of this is to not draw antialiasing pixels in this case. - float2 reduced_border = float2( - border.x == 0.0 ? -antialias_threshold : border.x, - border.y == 0.0 ? -antialias_threshold : border.y); - - // Vector from the corner of the quad bounds to the point, after mirroring - // the point into the bottom right quadrant. Both components are <= 0. - float2 corner_to_point = fabs(center_to_point) - half_size; - - // Vector from the point to the center of the rounded corner's circle, also - // mirrored into bottom right quadrant. - float2 corner_center_to_point = corner_to_point + corner_radius; - - // Whether the nearest point on the border is rounded - bool is_near_rounded_corner = - corner_center_to_point.x >= 0.0 && - corner_center_to_point.y >= 0.0; - - // Vector from straight border inner corner to point. - // - // 0-width borders are turned into width -1 so that inner_sdf is > 1.0 near - // the border. Without this, antialiasing pixels would be drawn. - float2 straight_border_inner_corner_to_point = corner_to_point + reduced_border; - - // Whether the point is beyond the inner edge of the straight border - bool is_beyond_inner_straight_border = - straight_border_inner_corner_to_point.x > 0.0 || - straight_border_inner_corner_to_point.y > 0.0; - - - // Whether the point is far enough inside the quad, such that the pixels are - // not affected by the straight border. - bool is_within_inner_straight_border = - straight_border_inner_corner_to_point.x < -antialias_threshold && - straight_border_inner_corner_to_point.y < -antialias_threshold; - - // Fast path for points that must be part of the background - if (is_within_inner_straight_border && !is_near_rounded_corner) { - return background_color * float4(1.0, 1.0, 1.0, coverage); - } - - // Signed distance of the point to the outside edge of the quad's border - float outer_sdf = quad_sdf_impl(corner_center_to_point, corner_radius); - - // Approximate signed distance of the point to the inside edge of the quad's - // border. It is negative outside this edge (within the border), and - // positive inside. - // - // This is not always an accurate signed distance: - // * The rounded portions with varying border width use an approximation of - // nearest-point-on-ellipse. - // * When it is quickly known to be outside the edge, -1.0 is used. - float inner_sdf = 0.0; - if (corner_center_to_point.x <= 0.0 || corner_center_to_point.y <= 0.0) { - // Fast paths for straight borders - inner_sdf = -max(straight_border_inner_corner_to_point.x, - straight_border_inner_corner_to_point.y); - } else if (is_beyond_inner_straight_border) { - // Fast path for points that must be outside the inner edge - inner_sdf = -1.0; - } else if (reduced_border.x == reduced_border.y) { - // Fast path for circular inner edge. - inner_sdf = -(outer_sdf + reduced_border.x); - } else { - float2 ellipse_radii = max(float2(0.0), float2(corner_radius) - reduced_border); - inner_sdf = quarter_ellipse_sdf(corner_center_to_point, ellipse_radii); - } - - // Negative when inside the border - float border_sdf = max(inner_sdf, outer_sdf); - - float4 color = background_color; - if (border_sdf < antialias_threshold) { - float4 border_color = input.border_color; - - // Dashed border logic when border_style == 1 - if (quad.border_style == 1) { - // Position along the perimeter in "dash space", where each dash - // period has length 1 - float t = 0.0; - - // Total number of dash periods, so that the dash spacing can be - // adjusted to evenly divide it - float max_t = 0.0; - - // Border width is proportional to dash size. This is the behavior - // used by browsers, but also avoids dashes from different segments - // overlapping when dash size is smaller than the border width. - // - // Dash pattern: (2 * border width) dash, (1 * border width) gap - const float dash_length_per_width = 2.0; - const float dash_gap_per_width = 1.0; - const float dash_period_per_width = dash_length_per_width + dash_gap_per_width; - - // Since the dash size is determined by border width, the density of - // dashes varies. Multiplying a pixel distance by this returns a - // position in dash space - it has units (dash period / pixels). So - // a dash velocity of (1 / 10) is 1 dash every 10 pixels. - float dash_velocity = 0.0; - - // Dividing this by the border width gives the dash velocity - const float dv_numerator = 1.0 / dash_period_per_width; - - if (unrounded) { - // When corners aren't rounded, the dashes are separately laid - // out on each straight line, rather than around the whole - // perimeter. This way each line starts and ends with a dash. - bool is_horizontal = corner_center_to_point.x < corner_center_to_point.y; - - // Choosing the right border width for dashed borders. - // TODO: A better solution exists taking a look at the whole file. - // this does not fix single dashed borders at the corners - float2 dashed_border = float2( - fmax(quad.border_widths.bottom, quad.border_widths.top), - fmax(quad.border_widths.right, quad.border_widths.left)); - - float border_width = is_horizontal ? dashed_border.x : dashed_border.y; - dash_velocity = dv_numerator / border_width; - t = is_horizontal ? point.x : point.y; - t *= dash_velocity; - max_t = is_horizontal ? size.x : size.y; - max_t *= dash_velocity; - } else { - // When corners are rounded, the dashes are laid out clockwise - // around the whole perimeter. - - float r_tr = quad.corner_radii.top_right; - float r_br = quad.corner_radii.bottom_right; - float r_bl = quad.corner_radii.bottom_left; - float r_tl = quad.corner_radii.top_left; - - float w_t = quad.border_widths.top; - float w_r = quad.border_widths.right; - float w_b = quad.border_widths.bottom; - float w_l = quad.border_widths.left; - - // Straight side dash velocities - float dv_t = w_t <= 0.0 ? 0.0 : dv_numerator / w_t; - float dv_r = w_r <= 0.0 ? 0.0 : dv_numerator / w_r; - float dv_b = w_b <= 0.0 ? 0.0 : dv_numerator / w_b; - float dv_l = w_l <= 0.0 ? 0.0 : dv_numerator / w_l; - - // Straight side lengths in dash space - float s_t = (size.x - r_tl - r_tr) * dv_t; - float s_r = (size.y - r_tr - r_br) * dv_r; - float s_b = (size.x - r_br - r_bl) * dv_b; - float s_l = (size.y - r_bl - r_tl) * dv_l; - - float corner_dash_velocity_tr = corner_dash_velocity(dv_t, dv_r); - float corner_dash_velocity_br = corner_dash_velocity(dv_b, dv_r); - float corner_dash_velocity_bl = corner_dash_velocity(dv_b, dv_l); - float corner_dash_velocity_tl = corner_dash_velocity(dv_t, dv_l); - - // Corner lengths in dash space - float c_tr = r_tr * (M_PI_F / 2.0) * corner_dash_velocity_tr; - float c_br = r_br * (M_PI_F / 2.0) * corner_dash_velocity_br; - float c_bl = r_bl * (M_PI_F / 2.0) * corner_dash_velocity_bl; - float c_tl = r_tl * (M_PI_F / 2.0) * corner_dash_velocity_tl; - - // Cumulative dash space upto each segment - float upto_tr = s_t; - float upto_r = upto_tr + c_tr; - float upto_br = upto_r + s_r; - float upto_b = upto_br + c_br; - float upto_bl = upto_b + s_b; - float upto_l = upto_bl + c_bl; - float upto_tl = upto_l + s_l; - max_t = upto_tl + c_tl; - - if (is_near_rounded_corner) { - float radians = atan2(corner_center_to_point.y, corner_center_to_point.x); - float corner_t = radians * corner_radius; - - if (center_to_point.x >= 0.0) { - if (center_to_point.y < 0.0) { - dash_velocity = corner_dash_velocity_tr; - // Subtracted because radians is pi/2 to 0 when - // going clockwise around the top right corner, - // since the y axis has been flipped - t = upto_r - corner_t * dash_velocity; - } else { - dash_velocity = corner_dash_velocity_br; - // Added because radians is 0 to pi/2 when going - // clockwise around the bottom-right corner - t = upto_br + corner_t * dash_velocity; - } - } else { - if (center_to_point.y >= 0.0) { - dash_velocity = corner_dash_velocity_bl; - // Subtracted because radians is pi/1 to 0 when - // going clockwise around the bottom-left corner, - // since the x axis has been flipped - t = upto_l - corner_t * dash_velocity; - } else { - dash_velocity = corner_dash_velocity_tl; - // Added because radians is 0 to pi/2 when going - // clockwise around the top-left corner, since both - // axis were flipped - t = upto_tl + corner_t * dash_velocity; - } - } - } else { - // Straight borders - bool is_horizontal = corner_center_to_point.x < corner_center_to_point.y; - if (is_horizontal) { - if (center_to_point.y < 0.0) { - dash_velocity = dv_t; - t = (point.x - r_tl) * dash_velocity; - } else { - dash_velocity = dv_b; - t = upto_bl - (point.x - r_bl) * dash_velocity; - } - } else { - if (center_to_point.x < 0.0) { - dash_velocity = dv_l; - t = upto_tl - (point.y - r_tl) * dash_velocity; - } else { - dash_velocity = dv_r; - t = upto_r + (point.y - r_tr) * dash_velocity; - } - } - } - } - - float dash_length = dash_length_per_width / dash_period_per_width; - float desired_dash_gap = dash_gap_per_width / dash_period_per_width; - - // Straight borders should start and end with a dash, so max_t is - // reduced to cause this. - max_t -= unrounded ? dash_length : 0.0; - if (max_t >= 1.0) { - // Adjust dash gap to evenly divide max_t - float dash_count = floor(max_t); - float dash_period = max_t / dash_count; - border_color.a *= dash_alpha(t, dash_period, dash_length, dash_velocity, - antialias_threshold); - } else if (unrounded) { - // When there isn't enough space for the full gap between the - // two start / end dashes of a straight border, reduce gap to - // make them fit. - float dash_gap = max_t - dash_length; - if (dash_gap > 0.0) { - float dash_period = dash_length + dash_gap; - border_color.a *= dash_alpha(t, dash_period, dash_length, dash_velocity, - antialias_threshold); - } - } - } - - // Blend the border on top of the background and then linearly interpolate - // between the two as we slide inside the background. - float4 blended_border = over(background_color, border_color); - color = mix(background_color, blended_border, - saturate(antialias_threshold - inner_sdf)); - } - - return color * float4(1.0, 1.0, 1.0, min(coverage, saturate(antialias_threshold - outer_sdf))); -} - -// Returns the dash velocity of a corner given the dash velocity of the two -// sides, by returning the slower velocity (larger dashes). -// -// Since 0 is used for dash velocity when the border width is 0 (instead of -// +inf), this returns the other dash velocity in that case. -// -// An alternative to this might be to appropriately interpolate the dash -// velocity around the corner, but that seems overcomplicated. -float corner_dash_velocity(float dv1, float dv2) { - if (dv1 == 0.0) { - return dv2; - } else if (dv2 == 0.0) { - return dv1; - } else { - return min(dv1, dv2); - } -} - -// Returns alpha used to render antialiased dashes. -// `t` is within the dash when `fmod(t, period) < length`. -float dash_alpha( - float t, float period, float length, float dash_velocity, - float antialias_threshold) { - float half_period = period / 2.0; - float half_length = length / 2.0; - // Value in [-half_period, half_period] - // The dash is in [-half_length, half_length] - float centered = fmod(t + half_period - half_length, period) - half_period; - // Signed distance for the dash, negative values are inside the dash - float signed_distance = abs(centered) - half_length; - // Antialiased alpha based on the signed distance - return saturate(antialias_threshold - signed_distance / dash_velocity); -} - -// This approximates distance to the nearest point to a quarter ellipse in a way -// that is sufficient for anti-aliasing when the ellipse is not very eccentric. -// The components of `point` are expected to be positive. -// -// Negative on the outside and positive on the inside. -float quarter_ellipse_sdf(float2 point, float2 radii) { - // Scale the space to treat the ellipse like a unit circle - float2 circle_vec = point / radii; - float unit_circle_sdf = length(circle_vec) - 1.0; - // Approximate up-scaling of the length by using the average of the radii. - // - // TODO: A better solution would be to use the gradient of the implicit - // function for an ellipse to approximate a scaling factor. - return unit_circle_sdf * (radii.x + radii.y) * -0.5; -} - -struct ShadowVertexOutput { - float4 position [[position]]; - float4 color [[flat]]; - uint shadow_id [[flat]]; - float clip_distance [[clip_distance]][4]; -}; - -struct ShadowFragmentInput { - float4 position [[position]]; - float4 color [[flat]]; - uint shadow_id [[flat]]; -}; - -vertex ShadowVertexOutput shadow_vertex( - uint unit_vertex_id [[vertex_id]], uint shadow_id [[instance_id]], - constant float2 *unit_vertices [[buffer(ShadowInputIndex_Vertices)]], - constant Shadow *shadows [[buffer(ShadowInputIndex_Shadows)]], - constant Size_DevicePixels *viewport_size - [[buffer(ShadowInputIndex_ViewportSize)]]) { - float2 unit_vertex = unit_vertices[unit_vertex_id]; - Shadow shadow = shadows[shadow_id]; - - Bounds_ScaledPixels bounds; - if (shadow.inset != 0u) { - bounds = shadow.element_bounds; - } else { - // Leave room for the gaussian tail outside the shadow rect. - float margin = 3. * shadow.blur_radius; - bounds = shadow.bounds; - bounds.origin.x -= margin; - bounds.origin.y -= margin; - bounds.size.width += 2. * margin; - bounds.size.height += 2. * margin; - } - - float4 device_position = - to_device_position(unit_vertex, bounds, viewport_size); - float4 clip_distance = - distance_from_clip_rect(unit_vertex, bounds, shadow.content_mask); - float4 color = hsla_to_rgba(shadow.color); - - return ShadowVertexOutput{ - device_position, - color, - shadow_id, - {clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}}; -} - -fragment float4 shadow_fragment(ShadowFragmentInput input [[stage_in]], - constant Shadow *shadows - [[buffer(ShadowInputIndex_Shadows)]], - constant RoundedClip_ScaledPixels *clips [[buffer(8)]]) { - Shadow shadow = shadows[input.shadow_id]; - float coverage = content_mask_coverage(input.position.xy, shadow.content_mask, clips); - if (coverage <= 0.0) { - return float4(0.0); - } - - float2 origin = float2(shadow.bounds.origin.x, shadow.bounds.origin.y); - float2 size = float2(shadow.bounds.size.width, shadow.bounds.size.height); - float2 half_size = size / 2.; - float2 center = origin + half_size; - float2 point = input.position.xy - center; - float corner_radius; - if (point.x < 0.) { - if (point.y < 0.) { - corner_radius = shadow.corner_radii.top_left; - } else { - corner_radius = shadow.corner_radii.bottom_left; - } - } else { - if (point.y < 0.) { - corner_radius = shadow.corner_radii.top_right; - } else { - corner_radius = shadow.corner_radii.bottom_right; - } - } - - float alpha; - if (shadow.blur_radius == 0.) { - float distance = quad_sdf(input.position.xy, shadow.bounds, shadow.corner_radii); - alpha = saturate(0.5 - distance); - } else { - // The signal is only non-zero in a limited range, so don't waste samples - float low = point.y - half_size.y; - float high = point.y + half_size.y; - float start = clamp(-3. * shadow.blur_radius, low, high); - float end = clamp(3. * shadow.blur_radius, low, high); - - // Accumulate samples (we can get away with surprisingly few samples) - float step = (end - start) / 4.; - float y = start + step * 0.5; - alpha = 0.; - for (int i = 0; i < 4; i++) { - alpha += blur_along_x(point.x, point.y - y, shadow.blur_radius, - corner_radius, half_size) * - gaussian(y, shadow.blur_radius) * step; - y += step; - } - } - - if (shadow.inset != 0u) { - // The inset shadow is the complement of the (blurred) hole rect, clipped to the element. - // `saturate(0.5 - d)` gives a 1-pixel antialiased edge: d <= -0.5 -> 1, d >= 0.5 -> 0. - alpha = 1. - alpha; - float element_distance = quad_sdf(input.position.xy, shadow.element_bounds, - shadow.element_corner_radii); - alpha *= saturate(0.5 - element_distance); - } - - return input.color * float4(1., 1., 1., alpha * coverage); -} - -struct UnderlineVertexOutput { - float4 position [[position]]; - float4 color [[flat]]; - uint underline_id [[flat]]; - float clip_distance [[clip_distance]][4]; -}; - -struct UnderlineFragmentInput { - float4 position [[position]]; - float4 color [[flat]]; - uint underline_id [[flat]]; -}; - -vertex UnderlineVertexOutput underline_vertex( - uint unit_vertex_id [[vertex_id]], uint underline_id [[instance_id]], - constant float2 *unit_vertices [[buffer(UnderlineInputIndex_Vertices)]], - constant Underline *underlines [[buffer(UnderlineInputIndex_Underlines)]], - constant Size_DevicePixels *viewport_size - [[buffer(ShadowInputIndex_ViewportSize)]]) { - float2 unit_vertex = unit_vertices[unit_vertex_id]; - Underline underline = underlines[underline_id]; - float4 device_position = - to_device_position(unit_vertex, underline.bounds, viewport_size); - float4 clip_distance = distance_from_clip_rect(unit_vertex, underline.bounds, - underline.content_mask); - float4 color = hsla_to_rgba(underline.color); - return UnderlineVertexOutput{ - device_position, - color, - underline_id, - {clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}}; -} - -fragment float4 underline_fragment(UnderlineFragmentInput input [[stage_in]], - constant Underline *underlines - [[buffer(UnderlineInputIndex_Underlines)]], - constant RoundedClip_ScaledPixels *clips [[buffer(8)]]) { - const float WAVE_FREQUENCY = 2.0; - const float WAVE_HEIGHT_RATIO = 0.8; - - Underline underline = underlines[input.underline_id]; - float coverage = content_mask_coverage(input.position.xy, underline.content_mask, clips); - if (coverage <= 0.0) { - return float4(0.0); - } - if (underline.wavy) { - float half_thickness = underline.thickness * 0.5; - float2 origin = - float2(underline.bounds.origin.x, underline.bounds.origin.y); - - float2 st = ((input.position.xy - origin) / underline.bounds.size.height) - - float2(0., 0.5); - float frequency = (M_PI_F * WAVE_FREQUENCY * underline.thickness) / underline.bounds.size.height; - float amplitude = (underline.thickness * WAVE_HEIGHT_RATIO) / underline.bounds.size.height; - - float sine = sin(st.x * frequency) * amplitude; - float dSine = cos(st.x * frequency) * amplitude * frequency; - float distance = (st.y - sine) / sqrt(1. + dSine * dSine); - float distance_in_pixels = distance * underline.bounds.size.height; - float distance_from_top_border = distance_in_pixels - half_thickness; - float distance_from_bottom_border = distance_in_pixels + half_thickness; - float alpha = saturate( - 0.5 - max(-distance_from_bottom_border, distance_from_top_border)); - return input.color * float4(1., 1., 1., alpha * coverage); - } else { - return input.color * float4(1., 1., 1., coverage); - } -} - -struct MonochromeSpriteVertexOutput { - float4 position [[position]]; - float2 tile_position; - float4 color [[flat]]; - uint sprite_id [[flat]]; - float4 clip_distance; -}; - -struct MonochromeSpriteFragmentInput { - float4 position [[position]]; - float2 tile_position; - float4 color [[flat]]; - uint sprite_id [[flat]]; - float4 clip_distance; -}; - -vertex MonochromeSpriteVertexOutput monochrome_sprite_vertex( - uint unit_vertex_id [[vertex_id]], uint sprite_id [[instance_id]], - constant float2 *unit_vertices [[buffer(SpriteInputIndex_Vertices)]], - constant MonochromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]], - constant Size_DevicePixels *viewport_size - [[buffer(SpriteInputIndex_ViewportSize)]], - constant Size_DevicePixels *atlas_size - [[buffer(SpriteInputIndex_AtlasTextureSize)]]) { - float2 unit_vertex = unit_vertices[unit_vertex_id]; - MonochromeSprite sprite = sprites[sprite_id]; - float4 device_position = - to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation, viewport_size); - float4 clip_distance = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, - sprite.content_mask, sprite.transformation); - float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size); - float4 color = hsla_to_rgba(sprite.color); - return MonochromeSpriteVertexOutput{ - device_position, - tile_position, - color, - sprite_id, - {clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}}; -} - -fragment float4 monochrome_sprite_fragment( - MonochromeSpriteFragmentInput input [[stage_in]], - constant MonochromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]], - texture2d atlas_texture [[texture(SpriteInputIndex_AtlasTexture)]], - constant RoundedClip_ScaledPixels *clips [[buffer(8)]]) { - if (any(input.clip_distance < float4(0.0))) { - return float4(0.0); - } - - MonochromeSprite sprite = sprites[input.sprite_id]; - float coverage = content_mask_coverage(input.position.xy, sprite.content_mask, clips); - if (coverage <= 0.0) { - return float4(0.0); - } - - constexpr sampler atlas_texture_sampler(mag_filter::linear, - min_filter::linear); - float4 sample = - atlas_texture.sample(atlas_texture_sampler, input.tile_position); - float4 color = input.color; - color.a *= sample.a * coverage; - return color; -} - -struct PolychromeSpriteVertexOutput { - float4 position [[position]]; - float2 tile_position; - uint sprite_id [[flat]]; - float clip_distance [[clip_distance]][4]; -}; - -struct PolychromeSpriteFragmentInput { - float4 position [[position]]; - float2 tile_position; - uint sprite_id [[flat]]; -}; - -vertex PolychromeSpriteVertexOutput polychrome_sprite_vertex( - uint unit_vertex_id [[vertex_id]], uint sprite_id [[instance_id]], - constant float2 *unit_vertices [[buffer(SpriteInputIndex_Vertices)]], - constant PolychromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]], - constant Size_DevicePixels *viewport_size - [[buffer(SpriteInputIndex_ViewportSize)]], - constant Size_DevicePixels *atlas_size - [[buffer(SpriteInputIndex_AtlasTextureSize)]]) { - - float2 unit_vertex = unit_vertices[unit_vertex_id]; - PolychromeSprite sprite = sprites[sprite_id]; - float4 device_position = - to_device_position(unit_vertex, sprite.bounds, viewport_size); - float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds, - sprite.content_mask); - float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size); - return PolychromeSpriteVertexOutput{ - device_position, - tile_position, - sprite_id, - {clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}}; -} - -fragment float4 polychrome_sprite_fragment( - PolychromeSpriteFragmentInput input [[stage_in]], - constant PolychromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]], - texture2d atlas_texture [[texture(SpriteInputIndex_AtlasTexture)]], - constant RoundedClip_ScaledPixels *clips [[buffer(8)]]) { - PolychromeSprite sprite = sprites[input.sprite_id]; - float coverage = content_mask_coverage(input.position.xy, sprite.content_mask, clips); - if (coverage <= 0.0) { - return float4(0.0); - } - constexpr sampler atlas_texture_sampler(mag_filter::linear, - min_filter::linear); - float4 sample = - atlas_texture.sample(atlas_texture_sampler, input.tile_position); - float distance = - quad_sdf(input.position.xy, sprite.bounds, sprite.corner_radii); - - float4 color = sample; - if (sprite.grayscale) { - float grayscale = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b; - color.r = grayscale; - color.g = grayscale; - color.b = grayscale; - } - color.a *= sprite.opacity * min(coverage, saturate(0.5 - distance)); - return color; -} - -struct PathRasterizationVertexOutput { - float4 position [[position]]; - float2 st_position; - uint vertex_id [[flat]]; - float clip_rect_distance [[clip_distance]][4]; -}; - -struct PathRasterizationFragmentInput { - float4 position [[position]]; - float2 st_position; - uint vertex_id [[flat]]; -}; - -vertex PathRasterizationVertexOutput path_rasterization_vertex( - uint vertex_id [[vertex_id]], - constant PathRasterizationVertex *vertices [[buffer(PathRasterizationInputIndex_Vertices)]], - constant Size_DevicePixels *atlas_size [[buffer(PathRasterizationInputIndex_ViewportSize)]] -) { - PathRasterizationVertex v = vertices[vertex_id]; - float2 vertex_position = float2(v.xy_position.x, v.xy_position.y); - float4 position = float4( - vertex_position * float2(2. / atlas_size->width, -2. / atlas_size->height) + float2(-1., 1.), - 0., - 1. - ); - return PathRasterizationVertexOutput{ - position, - float2(v.st_position.x, v.st_position.y), - vertex_id, - { - v.xy_position.x - v.bounds.origin.x, - v.bounds.origin.x + v.bounds.size.width - v.xy_position.x, - v.xy_position.y - v.bounds.origin.y, - v.bounds.origin.y + v.bounds.size.height - v.xy_position.y - } - }; -} - -fragment float4 path_rasterization_fragment( - PathRasterizationFragmentInput input [[stage_in]], - constant PathRasterizationVertex *vertices [[buffer(PathRasterizationInputIndex_Vertices)]] -, - constant RoundedClip_ScaledPixels *clips [[buffer(8)]]) { - float2 dx = dfdx(input.st_position); - float2 dy = dfdy(input.st_position); - - PathRasterizationVertex v = vertices[input.vertex_id]; - float coverage = content_mask_coverage(input.position.xy, v.content_mask, clips); - if (coverage <= 0.0) { - return float4(0.0); - } - Background background = v.color; - Bounds_ScaledPixels path_bounds = v.bounds; - float alpha; - if (length(float2(dx.x, dy.x)) < 0.001) { - alpha = 1.0; - } else { - float2 gradient = float2( - (2. * input.st_position.x) * dx.x - dx.y, - (2. * input.st_position.x) * dy.x - dy.y - ); - float f = (input.st_position.x * input.st_position.x) - input.st_position.y; - float distance = f / length(gradient); - alpha = saturate(0.5 - distance); - } - - GradientColor gradient_color = prepare_fill_color( - background.tag, - background.color_space, - background.solid, - background.colors[0].color, - background.colors[1].color - ); - - float4 color = fill_color( - background, - input.position.xy, - path_bounds, - gradient_color.solid, - gradient_color.color0, - gradient_color.color1 - ); - return float4(color.rgb * color.a * alpha, alpha * color.a) * coverage; -} - -struct PathSpriteVertexOutput { - float4 position [[position]]; - float2 texture_coords; -}; - -vertex PathSpriteVertexOutput path_sprite_vertex( - uint unit_vertex_id [[vertex_id]], - uint sprite_id [[instance_id]], - constant float2 *unit_vertices [[buffer(SpriteInputIndex_Vertices)]], - constant PathSprite *sprites [[buffer(SpriteInputIndex_Sprites)]], - constant Size_DevicePixels *viewport_size [[buffer(SpriteInputIndex_ViewportSize)]] -) { - float2 unit_vertex = unit_vertices[unit_vertex_id]; - PathSprite sprite = sprites[sprite_id]; - // Don't apply content mask because it was already accounted for when - // rasterizing the path. - float4 device_position = - to_device_position(unit_vertex, sprite.bounds, viewport_size); - - float2 screen_position = float2(sprite.bounds.origin.x, sprite.bounds.origin.y) + unit_vertex * float2(sprite.bounds.size.width, sprite.bounds.size.height); - float2 texture_coords = screen_position / float2(viewport_size->width, viewport_size->height); - - return PathSpriteVertexOutput{ - device_position, - texture_coords - }; -} - -fragment float4 path_sprite_fragment( - PathSpriteVertexOutput input [[stage_in]], - texture2d intermediate_texture [[texture(SpriteInputIndex_AtlasTexture)]] -) { - constexpr sampler intermediate_texture_sampler(mag_filter::linear, min_filter::linear); - return intermediate_texture.sample(intermediate_texture_sampler, input.texture_coords); -} - -struct SurfaceVertexOutput { - float4 position [[position]]; - float2 texture_position; - float clip_distance [[clip_distance]][4]; - float4 clip_mask_bounds [[flat]]; - float4 clip_mask_radii [[flat]]; - uint clip_index [[flat]]; -}; - -struct SurfaceFragmentInput { - float4 position [[position]]; - float2 texture_position; - float4 clip_mask_bounds [[flat]]; - float4 clip_mask_radii [[flat]]; - uint clip_index [[flat]]; -}; - -vertex SurfaceVertexOutput surface_vertex( - uint unit_vertex_id [[vertex_id]], uint surface_id [[instance_id]], - constant float2 *unit_vertices [[buffer(SurfaceInputIndex_Vertices)]], - constant SurfaceBounds *surfaces [[buffer(SurfaceInputIndex_Surfaces)]], - constant Size_DevicePixels *viewport_size - [[buffer(SurfaceInputIndex_ViewportSize)]], - constant Size_DevicePixels *texture_size - [[buffer(SurfaceInputIndex_TextureSize)]]) { - float2 unit_vertex = unit_vertices[unit_vertex_id]; - SurfaceBounds surface = surfaces[surface_id]; - float4 device_position = - to_device_position(unit_vertex, surface.bounds, viewport_size); - float4 clip_distance = distance_from_clip_rect(unit_vertex, surface.bounds, - surface.content_mask); - // We are going to copy the whole texture, so the texture position corresponds - // to the current vertex of the unit triangle. - float2 texture_position = unit_vertex; - return SurfaceVertexOutput{ - device_position, - texture_position, - {clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}, - float4(surface.content_mask.bounds.origin.x, surface.content_mask.bounds.origin.y, - surface.content_mask.bounds.size.width, surface.content_mask.bounds.size.height), - float4(surface.content_mask.corner_radii.top_left, - surface.content_mask.corner_radii.top_right, - surface.content_mask.corner_radii.bottom_right, - surface.content_mask.corner_radii.bottom_left), - surface.content_mask.clip_index}; -} - -fragment float4 surface_fragment(SurfaceFragmentInput input [[stage_in]], - texture2d y_texture - [[texture(SurfaceInputIndex_YTexture)]], - texture2d cb_cr_texture - [[texture(SurfaceInputIndex_CbCrTexture)]], - constant RoundedClip_ScaledPixels *clips [[buffer(8)]]) { - Bounds_ScaledPixels clip_bounds = Bounds_ScaledPixels{ - {input.clip_mask_bounds.x, input.clip_mask_bounds.y}, - {input.clip_mask_bounds.z, input.clip_mask_bounds.w}}; - Corners_ScaledPixels clip_radii = Corners_ScaledPixels{ - input.clip_mask_radii.x, input.clip_mask_radii.y, - input.clip_mask_radii.z, input.clip_mask_radii.w}; - ContentMask_ScaledPixels mask = {clip_bounds, clip_radii, input.clip_index, 0}; - float coverage = content_mask_coverage(input.position.xy, mask, clips); - if (coverage <= 0.0) { - return float4(0.0); - } - constexpr sampler texture_sampler(mag_filter::linear, min_filter::linear); - const float4x4 ycbcrToRGBTransform = - float4x4(float4(+1.0000f, +1.0000f, +1.0000f, +0.0000f), - float4(+0.0000f, -0.3441f, +1.7720f, +0.0000f), - float4(+1.4020f, -0.7141f, +0.0000f, +0.0000f), - float4(-0.7010f, +0.5291f, -0.8860f, +1.0000f)); - float4 ycbcr = float4( - y_texture.sample(texture_sampler, input.texture_position).r, - cb_cr_texture.sample(texture_sampler, input.texture_position).rg, 1.0); - - return (ycbcrToRGBTransform * ycbcr) * float4(1., 1., 1., coverage); -} - -float4 hsla_to_rgba(Hsla hsla) { - float h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range - float s = hsla.s; - float l = hsla.l; - float a = hsla.a; - - float c = (1.0 - fabs(2.0 * l - 1.0)) * s; - float x = c * (1.0 - fabs(fmod(h, 2.0) - 1.0)); - float m = l - c / 2.0; - - float r = 0.0; - float g = 0.0; - float b = 0.0; - - if (h >= 0.0 && h < 1.0) { - r = c; - g = x; - b = 0.0; - } else if (h >= 1.0 && h < 2.0) { - r = x; - g = c; - b = 0.0; - } else if (h >= 2.0 && h < 3.0) { - r = 0.0; - g = c; - b = x; - } else if (h >= 3.0 && h < 4.0) { - r = 0.0; - g = x; - b = c; - } else if (h >= 4.0 && h < 5.0) { - r = x; - g = 0.0; - b = c; - } else { - r = c; - g = 0.0; - b = x; - } - - float4 rgba; - rgba.x = (r + m); - rgba.y = (g + m); - rgba.z = (b + m); - rgba.w = a; - return rgba; -} - -float3 srgb_to_linear(float3 color) { - return pow(color, float3(2.2)); -} - -float3 linear_to_srgb(float3 color) { - return pow(color, float3(1.0 / 2.2)); -} - -// Converts a sRGB color to the Oklab color space. -// Reference: https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab -float4 srgb_to_oklab(float4 color) { - // Convert non-linear sRGB to linear sRGB - color = float4(srgb_to_linear(color.rgb), color.a); - - float l = 0.4122214708 * color.r + 0.5363325363 * color.g + 0.0514459929 * color.b; - float m = 0.2119034982 * color.r + 0.6806995451 * color.g + 0.1073969566 * color.b; - float s = 0.0883024619 * color.r + 0.2817188376 * color.g + 0.6299787005 * color.b; - - float l_ = pow(l, 1.0/3.0); - float m_ = pow(m, 1.0/3.0); - float s_ = pow(s, 1.0/3.0); - - return float4( - 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_, - 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_, - 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_, - color.a - ); -} - -// Converts an Oklab color to the sRGB color space. -float4 oklab_to_srgb(float4 color) { - float l_ = color.r + 0.3963377774 * color.g + 0.2158037573 * color.b; - float m_ = color.r - 0.1055613458 * color.g - 0.0638541728 * color.b; - float s_ = color.r - 0.0894841775 * color.g - 1.2914855480 * color.b; - - float l = l_ * l_ * l_; - float m = m_ * m_ * m_; - float s = s_ * s_ * s_; - - float3 linear_rgb = float3( - 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, - -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, - -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s - ); - - // Convert linear sRGB to non-linear sRGB - return float4(linear_to_srgb(linear_rgb), color.a); -} - -float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds, - constant Size_DevicePixels *input_viewport_size) { - float2 position = - unit_vertex * float2(bounds.size.width, bounds.size.height) + - float2(bounds.origin.x, bounds.origin.y); - float2 viewport_size = float2((float)input_viewport_size->width, - (float)input_viewport_size->height); - float2 device_position = - position / viewport_size * float2(2., -2.) + float2(-1., 1.); - return float4(device_position, 0., 1.); -} - -float4 to_device_position_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds, - TransformationMatrix transformation, - constant Size_DevicePixels *input_viewport_size) { - float2 position = - unit_vertex * float2(bounds.size.width, bounds.size.height) + - float2(bounds.origin.x, bounds.origin.y); - - // Apply the transformation matrix to the position via matrix multiplication. - float2 transformed_position = float2(0, 0); - transformed_position[0] = position[0] * transformation.rotation_scale[0][0] + position[1] * transformation.rotation_scale[0][1]; - transformed_position[1] = position[0] * transformation.rotation_scale[1][0] + position[1] * transformation.rotation_scale[1][1]; - - // Add in the translation component of the transformation matrix. - transformed_position[0] += transformation.translation[0]; - transformed_position[1] += transformation.translation[1]; - - float2 viewport_size = float2((float)input_viewport_size->width, - (float)input_viewport_size->height); - float2 device_position = - transformed_position / viewport_size * float2(2., -2.) + float2(-1., 1.); - return float4(device_position, 0., 1.); -} - - -float2 to_tile_position(float2 unit_vertex, AtlasTile tile, - constant Size_DevicePixels *atlas_size) { - float2 tile_origin = float2(tile.bounds.origin.x, tile.bounds.origin.y); - float2 tile_size = float2(tile.bounds.size.width, tile.bounds.size.height); - return (tile_origin + unit_vertex * tile_size) / - float2((float)atlas_size->width, (float)atlas_size->height); -} - -// Selects corner radius based on quadrant. -float pick_corner_radius(float2 center_to_point, Corners_ScaledPixels corner_radii) { - if (center_to_point.x < 0.) { - if (center_to_point.y < 0.) { - return corner_radii.top_left; - } else { - return corner_radii.bottom_left; - } - } else { - if (center_to_point.y < 0.) { - return corner_radii.top_right; - } else { - return corner_radii.bottom_right; - } - } -} - -// Signed distance of the point to the quad's border - positive outside the -// border, and negative inside. -float quad_sdf(float2 point, Bounds_ScaledPixels bounds, - Corners_ScaledPixels corner_radii) { - float2 half_size = float2(bounds.size.width, bounds.size.height) / 2.0; - float2 center = float2(bounds.origin.x, bounds.origin.y) + half_size; - float2 center_to_point = point - center; - float corner_radius = pick_corner_radius(center_to_point, corner_radii); - float2 corner_to_point = fabs(center_to_point) - half_size; - float2 corner_center_to_point = corner_to_point + corner_radius; - return quad_sdf_impl(corner_center_to_point, corner_radius); -} - -// Implementation of quad signed distance field -float quad_sdf_impl(float2 corner_center_to_point, float corner_radius) { - if (corner_radius == 0.0) { - // Fast path for unrounded corners - return max(corner_center_to_point.x, corner_center_to_point.y); - } else { - // Signed distance of the point from a quad that is inset by corner_radius - // It is negative inside this quad, and positive outside - float signed_distance_to_inset_quad = - // 0 inside the inset quad, and positive outside - length(max(float2(0.0), corner_center_to_point)) + - // 0 outside the inset quad, and negative inside - min(0.0, max(corner_center_to_point.x, corner_center_to_point.y)); - - return signed_distance_to_inset_quad - corner_radius; - } -} - -// A standard gaussian function, used for weighting samples -float gaussian(float x, float sigma) { - return exp(-(x * x) / (2. * sigma * sigma)) / (sqrt(2. * M_PI_F) * sigma); -} - -// This approximates the error function, needed for the gaussian integral -float2 erf(float2 x) { - float2 s = sign(x); - float2 a = abs(x); - float2 r1 = 1. + (0.278393 + (0.230389 + (0.000972 + 0.078108 * a) * a) * a) * a; - float2 r2 = r1 * r1; - return s - s / (r2 * r2); -} - -float blur_along_x(float x, float y, float sigma, float corner, - float2 half_size) { - float delta = min(half_size.y - corner - abs(y), 0.); - float curved = - half_size.x - corner + sqrt(max(0., corner * corner - delta * delta)); - float2 integral = - 0.5 + 0.5 * erf((x + float2(-curved, curved)) * (sqrt(0.5) / sigma)); - return integral.y - integral.x; -} - -float4 distance_from_clip_rect(float2 unit_vertex, Bounds_ScaledPixels bounds, - ContentMask_ScaledPixels mask) { - float2 position = - unit_vertex * float2(bounds.size.width, bounds.size.height) + - float2(bounds.origin.x, bounds.origin.y); - float4 rect_distance = float4( - position.x - mask.bounds.origin.x, - mask.bounds.origin.x + mask.bounds.size.width - position.x, - position.y - mask.bounds.origin.y, - mask.bounds.origin.y + mask.bounds.size.height - position.y); - return rect_distance; -} - -float4 distance_from_clip_rect_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds, - ContentMask_ScaledPixels mask, TransformationMatrix transformation) { - float2 position = - unit_vertex * float2(bounds.size.width, bounds.size.height) + - float2(bounds.origin.x, bounds.origin.y); - float2 transformed_position = float2(0, 0); - transformed_position[0] = position[0] * transformation.rotation_scale[0][0] + position[1] * transformation.rotation_scale[0][1]; - transformed_position[1] = position[0] * transformation.rotation_scale[1][0] + position[1] * transformation.rotation_scale[1][1]; - transformed_position[0] += transformation.translation[0]; - transformed_position[1] += transformation.translation[1]; - - float4 rect_distance = float4( - transformed_position.x - mask.bounds.origin.x, - mask.bounds.origin.x + mask.bounds.size.width - transformed_position.x, - transformed_position.y - mask.bounds.origin.y, - mask.bounds.origin.y + mask.bounds.size.height - transformed_position.y); - return rect_distance; -} - -float4 over(float4 below, float4 above) { - float4 result; - float alpha = above.a + below.a * (1.0 - above.a); - result.rgb = - (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha; - result.a = alpha; - return result; -} - -GradientColor prepare_fill_color(uint tag, uint color_space, Hsla solid, - Hsla color0, Hsla color1) { - GradientColor out; - if (tag == 0 || tag == 2 || tag == 3) { - out.solid = hsla_to_rgba(solid); - } else if (tag == 1) { - out.color0 = hsla_to_rgba(color0); - out.color1 = hsla_to_rgba(color1); - - // Prepare color space in vertex for avoid conversion - // in fragment shader for performance reasons - if (color_space == 1) { - // Oklab - out.color0 = srgb_to_oklab(out.color0); - out.color1 = srgb_to_oklab(out.color1); - } - } - - return out; -} - -float2x2 rotate2d(float angle) { - float s = sin(angle); - float c = cos(angle); - return float2x2(c, -s, s, c); -} - -float4 fill_color(Background background, - float2 position, - Bounds_ScaledPixels bounds, - float4 solid_color, float4 color0, float4 color1) { - float4 color; - - switch (background.tag) { - case 0: - color = solid_color; - break; - case 1: { - // -90 degrees to match the CSS gradient angle. - float gradient_angle = background.gradient_angle_or_pattern_height; - float radians = (fmod(gradient_angle, 360.0) - 90.0) * (M_PI_F / 180.0); - float2 direction = float2(cos(radians), sin(radians)); - - // Expand the short side to be the same as the long side - if (bounds.size.width > bounds.size.height) { - direction.y *= bounds.size.height / bounds.size.width; - } else { - direction.x *= bounds.size.width / bounds.size.height; - } - - // Get the t value for the linear gradient with the color stop percentages. - float2 half_size = float2(bounds.size.width, bounds.size.height) / 2.; - float2 center = float2(bounds.origin.x, bounds.origin.y) + half_size; - float2 center_to_point = position - center; - float t = dot(center_to_point, direction) / length(direction); - // Check the direction to determine whether to use x or y - if (abs(direction.x) > abs(direction.y)) { - t = (t + half_size.x) / bounds.size.width; - } else { - t = (t + half_size.y) / bounds.size.height; - } - - // Adjust t based on the stop percentages - t = (t - background.colors[0].percentage) - / (background.colors[1].percentage - - background.colors[0].percentage); - t = clamp(t, 0.0, 1.0); - - switch (background.color_space) { - case 0: - color = mix(color0, color1, t); - break; - case 1: { - float4 oklab_color = mix(color0, color1, t); - color = oklab_to_srgb(oklab_color); - break; - } - } - - // Dither to reduce banding in gradients (especially dark/alpha). - // Triangular-distributed noise breaks up 8-bit quantization steps. - // ±2/255 for RGB (enough for dark-on-dark compositing), - // ±3/255 for alpha (needs more because alpha × dark color = tiny steps). - { - float2 seed = position * 0.6180339887; // golden ratio spread - float r1 = fract(sin(dot(seed, float2(12.9898, 78.233))) * 43758.5453); - float r2 = fract(sin(dot(seed, float2(39.3460, 11.135))) * 24634.6345); - float tri = r1 + r2 - 1.0; // triangular PDF, range [-1, +1] - color.rgb += tri * 2.0 / 255.0; - color.a += tri * 3.0 / 255.0; - } - - break; - } - case 2: { - float gradient_angle_or_pattern_height = background.gradient_angle_or_pattern_height; - float pattern_width = (gradient_angle_or_pattern_height / 65535.0f) / 255.0f; - float pattern_interval = fmod(gradient_angle_or_pattern_height, 65535.0f) / 255.0f; - float pattern_height = pattern_width + pattern_interval; - float stripe_angle = M_PI_F / 4.0; - float pattern_period = pattern_height * sin(stripe_angle); - float2x2 rotation = rotate2d(stripe_angle); - float2 relative_position = position - float2(bounds.origin.x, bounds.origin.y); - float2 rotated_point = rotation * relative_position; - float pattern = fmod(rotated_point.x, pattern_period); - float distance = min(pattern, pattern_period - pattern) - pattern_period * (pattern_width / pattern_height) / 2.0f; - color = solid_color; - color.a *= saturate(0.5 - distance); - break; - } - case 3: { - // checkerboard - float size = background.gradient_angle_or_pattern_height; - float2 relative_position = position - float2(bounds.origin.x, bounds.origin.y); - - float x_index = floor(relative_position.x / size); - float y_index = floor(relative_position.y / size); - float should_be_colored = fmod(x_index + y_index, 2.0); - - color = solid_color; - color.a *= saturate(should_be_colored); - break; - } - } - - return color; -} - - -// Distance uses each corner's own region, including elliptical border insets. -float clip_corner_distance(float2 point, float2 radii) { - if (any(radii <= float2(0.0)) || any(point >= radii)) return -1e20; - float2 p = point - radii; - float k0 = length(p / radii); - float k1 = length(p / (radii * radii)); - if (k1 == 0.0) return -min(radii.x, radii.y); - return k0 * (k0 - 1.0) / k1; -} - -float rounded_clip_distance(float2 position, RoundedClip_ScaledPixels clip) { - float2 tl = position - float2(clip.bounds.origin.x, clip.bounds.origin.y); - float2 br = float2(clip.bounds.size.width, clip.bounds.size.height) - tl; - float distance = max(max(-tl.x, -tl.y), max(-br.x, -br.y)); - distance = max(distance, clip_corner_distance(tl, float2(clip.radii_x.top_left, clip.radii_y.top_left))); - distance = max(distance, clip_corner_distance(float2(br.x, tl.y), float2(clip.radii_x.top_right, clip.radii_y.top_right))); - distance = max(distance, clip_corner_distance(br, float2(clip.radii_x.bottom_right, clip.radii_y.bottom_right))); - distance = max(distance, clip_corner_distance(float2(tl.x, br.y), float2(clip.radii_x.bottom_left, clip.radii_y.bottom_left))); - return distance; -} - -float content_mask_coverage(float2 position, ContentMask_ScaledPixels mask, - constant RoundedClip_ScaledPixels *clips) { - float distance = -1e20; - if (mask.clip_index == 0) { - RoundedClip_ScaledPixels clip = {mask.bounds, mask.corner_radii, mask.corner_radii, 0, 0}; - distance = rounded_clip_distance(position, clip); - } else { - uint index = mask.clip_index; - do { - RoundedClip_ScaledPixels clip = clips[index - 1]; - distance = max(distance, rounded_clip_distance(position, clip)); - index = clip.parent; - } while (index != 0); - } - return saturate(0.5 - distance); -} diff --git a/crates/gpui_pre_apple/vendor/gpui/src/clip.rs b/crates/gpui_pre_apple/vendor/gpui/src/clip.rs deleted file mode 100644 index 4bb79d9..0000000 --- a/crates/gpui_pre_apple/vendor/gpui/src/clip.rs +++ /dev/null @@ -1,188 +0,0 @@ -//! Rounded clipping keeps each ancestor's geometry separate from culling bounds. - -use crate::{px, Bounds, ContentMask, Corners, Pixels, Point, ScaledPixels}; -use smallvec::SmallVec; -use std::fmt::Debug; - -/// One immutable rounded rectangle in a scene's clip chain. -/// Separate horizontal and vertical radii preserve corners inset by unequal borders. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -#[repr(C)] -pub struct RoundedClip { - /// Original geometry, never replaced by an intersection's bounding box. - pub bounds: Bounds